view connectors/google.c @ 279:b3b547563ec7

Add Google connector service and agent wiki Implement the C/Seobeo Google Drive and Gmail connector with encrypted OAuth storage, Zenbu authentication, browser testing, AI tool discovery, chunked HTTP decoding, and Bazel coverage. Consolidate repository guidance into progressive wiki documentation and enforce arena-first allocation for new first-party C code. Co-authored-by: Copilot <[email protected]> Copilot-Session: 84c338fd-0939-4bb3-b7f3-1062eb213e5d
author MrJuneJune <me@mrjunejune.com>
date Mon, 17 Aug 2026 22:22:36 -0700
parents
children
line wrap: on
line source

#include "connectors/connector.h"

#include <stdio.h>
#include <string.h>
#include <time.h>

static boolean json_is_bounded(const char *json, size_t length)
{
  if (!json || length > CONNECTOR_MAX_JSON_BYTES)
    return FALSE;
  int32 depth = 0;
  boolean string = FALSE, escaped = FALSE;
  for (size_t i = 0; i < length; ++i) {
    char c = json[i];
    if (string) {
      if (escaped)
        escaped = FALSE;
      else if (c == '\\')
        escaped = TRUE;
      else if (c == '"')
        string = FALSE;
    } else if (c == '"')
      string = TRUE;
    else if (c == '{' || c == '[') {
      if (++depth > 32)
        return FALSE;
    } else if (c == '}' || c == ']') {
      if (--depth < 0)
        return FALSE;
    }
  }
  return !string && depth == 0;
}

static Connector_Status default_transport(
    const Connector_HTTP_Request *request, const char *access_token,
    Connector_HTTP_Response *response, Dowa_Arena *arena, void *context)
{
  (void)context;
  if (!request || !response || !arena)
    return CONNECTOR_INVALID;
  Seobeo_Client_Request *client = Seobeo_Client_Request_Create(request->url);
  if (!client)
    return CONNECTOR_PROVIDER_ERROR;
  Seobeo_Client_Request_Set_Method(client, request->method);
  Seobeo_Client_Request_Set_Timeout_Milliseconds(client, 15000);
  if (access_token && access_token[0]) {
    char authorization[2300];
    int32 length = snprintf(
        authorization, sizeof(authorization), "Authorization: Bearer %s",
        access_token);
    if (length <= 0 || (size_t)length >= sizeof(authorization)) {
      Seobeo_Client_Request_Destroy(client);
      return CONNECTOR_INVALID;
    }
    Seobeo_Client_Request_Add_Header_Array(client, authorization);
  }
  if (request->content_type) {
    char header[256];
    int32 length = snprintf(
        header, sizeof(header), "Content-Type: %s", request->content_type);
    if (length <= 0 || (size_t)length >= sizeof(header)) {
      Seobeo_Client_Request_Destroy(client);
      return CONNECTOR_INVALID;
    }
    Seobeo_Client_Request_Add_Header_Array(client, header);
  }
  if (request->body && request->body_length)
    Seobeo_Client_Request_Set_Body(
        client, request->body, request->body_length);
  if (request->download_path)
    Seobeo_Client_Request_Set_Download_Path(client, request->download_path);
  Seobeo_Client_Response *provider = Seobeo_Client_Request_Execute(client);
  if (!provider) {
    Seobeo_Client_Request_Destroy(client);
    return CONNECTOR_PROVIDER_ERROR;
  }
  response->status_code = provider->status_code;
  response->body_length = provider->body_length;
  if (provider->body && !request->download_path) {
    response->body = Dowa_Arena_Allocate(arena, provider->body_length + 1);
    if (!response->body) {
      Seobeo_Client_Response_Destroy(provider);
      Seobeo_Client_Request_Destroy(client);
      return CONNECTOR_ERROR;
    }
    memcpy(response->body, provider->body, provider->body_length);
    response->body[provider->body_length] = '\0';
  } else
    response->body = NULL;
  Seobeo_Client_Response_Destroy(provider);
  Seobeo_Client_Request_Destroy(client);
  return CONNECTOR_OK;
}

static Connector_Status transport(
    const Connector_Google_Config *config,
    const Connector_HTTP_Request *request, const char *access_token,
    Connector_HTTP_Response *response, Dowa_Arena *arena)
{
  Connector_HTTP_Transport implementation =
      config->transport ? config->transport : default_transport;
  return implementation(
      request, access_token, response, arena, config->transport_context);
}

static char *url_join(
    Dowa_Arena *arena, const char *base, const char *path, const char *query)
{
  if (!arena || !base)
    return NULL;
  size_t length = strlen(base) + (path ? strlen(path) : 0) +
      (query && query[0] ? strlen(query) + 1 : 0) + 1;
  char *url = Dowa_Arena_Allocate(arena, length);
  if (!url)
    return NULL;
  snprintf(
      url, length, "%s%s%s%s", base, path ? path : "",
      query && query[0] ? "?" : "", query && query[0] ? query : "");
  return url;
}

char *Connector_Google_Authorization_URL(
    const Connector_Google_Config *config, const Connector_OAuth_Start *start,
    Dowa_Arena *arena)
{
  if (!config || !start || !arena)
    return NULL;
  char client[1024], redirect[2048];
  if (!Connector_Form_Encode(config->client_id, client, sizeof(client)) ||
      !Connector_Form_Encode(
          config->redirect_uri, redirect, sizeof(redirect)))
    return NULL;
  const char *scopes =
      "openid%20email%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdrive"
      "%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fgmail.readonly"
      "%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fgmail.compose";
  size_t length = strlen(config->oauth_authorize_url) + strlen(client) +
      strlen(redirect) + strlen(start->state) + strlen(start->code_challenge) +
      strlen(scopes) + 256;
  char *url = Dowa_Arena_Allocate(arena, length);
  if (!url)
    return NULL;
  snprintf(
      url, length,
      "%s?client_id=%s&redirect_uri=%s&response_type=code&scope=%s"
      "&access_type=offline&prompt=consent&state=%s"
      "&code_challenge=%s&code_challenge_method=S256",
      config->oauth_authorize_url, client, redirect, scopes, start->state,
      start->code_challenge);
  return url;
}

static Connector_Status token_request(
    const Connector_Google_Config *config, const char *body,
    Connector_HTTP_Response *response, Connector_Provider_Error *error,
    Dowa_Arena *arena)
{
  Connector_HTTP_Request request = {
    .method = "POST",
    .url = config->oauth_token_url,
    .content_type = "application/x-www-form-urlencoded",
    .body = body,
    .body_length = strlen(body),
    .download_path = NULL
  };
  Connector_Status status = transport(
      config, &request, NULL, response, arena);
  if (status != CONNECTOR_OK) {
    if (error)
      snprintf(error->code, sizeof(error->code), "transport_error");
    return status;
  }
  if (response->status_code >= 200 && response->status_code < 300)
    return CONNECTOR_OK;
  if (error) {
    error->http_status = response->status_code;
    snprintf(error->code, sizeof(error->code), "token_endpoint_error");
    if (json_is_bounded(response->body, response->body_length)) {
      Dowa_JSON_Value parsed = Dowa_JSON_Parse(
          response->body, (int32)response->body_length, arena);
      if (parsed.type == DOWA_JSON_OBJECT) {
        char *code = Dowa_JSON_Get_String(parsed.object_val, "error");
        char *description =
            Dowa_JSON_Get_String(parsed.object_val, "error_description");
        if (code)
          snprintf(error->code, sizeof(error->code), "%s", code);
        if (description)
          snprintf(
              error->description, sizeof(error->description), "%s",
              description);
      }
    }
  }
  return CONNECTOR_PROVIDER_ERROR;
}

static boolean copy_json_string(
    Dowa_JSON_Entry *object, const char *key, char *output, size_t output_size)
{
  char *value = Dowa_JSON_Get_String(object, key);
  if (!value || strlen(value) >= output_size)
    return FALSE;
  strcpy(output, value);
  return TRUE;
}

Connector_Status Connector_Google_Exchange_Code(
    const Connector_Google_Config *config, const char *code,
    const char *verifier, Connector_Account *account,
    Connector_Provider_Error *error, Dowa_Arena *arena)
{
  if (!config || !code || !verifier || !account || !arena)
    return CONNECTOR_INVALID;
  if (error)
    memset(error, 0, sizeof(*error));
  char encoded_code[4096], encoded_verifier[512], encoded_client[1024];
  char encoded_secret[2048], encoded_redirect[2048];
  if (!Connector_Form_Encode(code, encoded_code, sizeof(encoded_code))) {
    if (error)
      snprintf(error->code, sizeof(error->code), "encode_code_failed");
    return CONNECTOR_INVALID;
  }
  if (!Connector_Form_Encode(
          verifier, encoded_verifier, sizeof(encoded_verifier))) {
    if (error)
      snprintf(error->code, sizeof(error->code), "encode_verifier_failed");
    return CONNECTOR_INVALID;
  }
  if (!Connector_Form_Encode(
          config->client_id, encoded_client, sizeof(encoded_client))) {
    if (error)
      snprintf(error->code, sizeof(error->code), "encode_client_id_failed");
    return CONNECTOR_INVALID;
  }
  if (!Connector_Form_Encode(
          config->client_secret, encoded_secret, sizeof(encoded_secret))) {
    if (error)
      snprintf(error->code, sizeof(error->code), "encode_client_secret_failed");
    return CONNECTOR_INVALID;
  }
  if (!Connector_Form_Encode(
          config->redirect_uri, encoded_redirect, sizeof(encoded_redirect))) {
    if (error)
      snprintf(error->code, sizeof(error->code), "encode_redirect_uri_failed");
    return CONNECTOR_INVALID;
  }
  char body[12288];
  int32 body_length = snprintf(
      body, sizeof(body),
      "code=%s&client_id=%s&client_secret=%s&redirect_uri=%s"
      "&code_verifier=%s&grant_type=authorization_code",
      encoded_code, encoded_client, encoded_secret, encoded_redirect,
      encoded_verifier);
  if (body_length <= 0 || (size_t)body_length >= sizeof(body)) {
    if (error)
      snprintf(error->code, sizeof(error->code), "token_request_too_large");
    return CONNECTOR_INVALID;
  }
  Connector_HTTP_Response token = {0};
  Connector_Status status = token_request(config, body, &token, error, arena);
  if (status != CONNECTOR_OK)
    return status;
  if (error)
    error->http_status = token.status_code;
  if (!token.body || token.body_length == 0) {
    if (error) {
      snprintf(error->code, sizeof(error->code), "empty_token_response");
      snprintf(
          error->description, sizeof(error->description),
          "Google token endpoint returned no response body");
    }
    return CONNECTOR_PROVIDER_ERROR;
  }
  if (!json_is_bounded(token.body, token.body_length)) {
    if (error) {
      snprintf(error->code, sizeof(error->code), "invalid_token_body");
      snprintf(
          error->description, sizeof(error->description),
          "Google token response was not bounded JSON (%zu bytes)",
          token.body_length);
    }
    return CONNECTOR_PROVIDER_ERROR;
  }
  Dowa_JSON_Value parsed = Dowa_JSON_Parse(
      token.body, (int32)token.body_length, arena);
  if (parsed.type != DOWA_JSON_OBJECT) {
    if (error) {
      snprintf(error->code, sizeof(error->code), "invalid_token_json");
      snprintf(
          error->description, sizeof(error->description),
          "Google token response JSON could not be parsed (%zu bytes)",
          token.body_length);
    }
    return CONNECTOR_PROVIDER_ERROR;
  }
  Dowa_JSON_Entry *object = parsed.object_val;
  memset(account, 0, sizeof(*account));
  strcpy(account->provider, "google");
  if (!copy_json_string(
          object, "access_token", account->access_token,
          sizeof(account->access_token))) {
    if (error)
      snprintf(error->code, sizeof(error->code), "invalid_token_response");
    return CONNECTOR_PROVIDER_ERROR;
  }
  char *refresh = Dowa_JSON_Get_String(object, "refresh_token");
  if (refresh && strlen(refresh) < sizeof(account->refresh_token))
    strcpy(account->refresh_token, refresh);
  char *scope = Dowa_JSON_Get_String(object, "scope");
  if (scope && strlen(scope) < sizeof(account->scopes))
    strcpy(account->scopes, scope);
  double expires = Dowa_JSON_Get_Number(object, "expires_in");
  account->expires_at = (int64)time(NULL) + (int64)expires;

  Connector_HTTP_Request identity_request = {
    .method = "GET", .url = config->identity_url
  };
  Connector_HTTP_Response identity = {0};
  status = transport(
      config, &identity_request, account->access_token, &identity, arena);
  if (status != CONNECTOR_OK || identity.status_code < 200 ||
      identity.status_code >= 300 ||
      !json_is_bounded(identity.body, identity.body_length)) {
    if (error) {
      error->http_status = identity.status_code;
      snprintf(error->code, sizeof(error->code), "userinfo_failed");
      snprintf(
          error->description, sizeof(error->description),
          "Google user-info lookup failed after token exchange");
    }
    return CONNECTOR_PROVIDER_ERROR;
  }
  parsed = Dowa_JSON_Parse(
      identity.body, (int32)identity.body_length, arena);
  if (parsed.type != DOWA_JSON_OBJECT)
    return CONNECTOR_PROVIDER_ERROR;
  object = parsed.object_val;
  if (!copy_json_string(
          object, "sub", account->provider_subject,
          sizeof(account->provider_subject)) ||
      !copy_json_string(object, "email", account->email, sizeof(account->email))) {
    if (error)
      snprintf(error->code, sizeof(error->code), "invalid_userinfo_response");
    return CONNECTOR_PROVIDER_ERROR;
  }
  int32 written = snprintf(
      account->account_id, sizeof(account->account_id), "google:%s",
      account->provider_subject);
  return written > 0 && (size_t)written < sizeof(account->account_id)
      ? CONNECTOR_OK : CONNECTOR_PROVIDER_ERROR;
}

Connector_Status Connector_Google_Refresh(
    const Connector_Google_Config *config, Connector_Account *account,
    Dowa_Arena *arena)
{
  if (!config || !account || !account->refresh_token[0] || !arena)
    return CONNECTOR_INVALID;
  char refresh[4096], client[1024], secret[2048], body[8192];
  if (!Connector_Form_Encode(
          account->refresh_token, refresh, sizeof(refresh)) ||
      !Connector_Form_Encode(config->client_id, client, sizeof(client)) ||
      !Connector_Form_Encode(
          config->client_secret, secret, sizeof(secret)))
    return CONNECTOR_INVALID;
  int32 length = snprintf(
      body, sizeof(body),
      "refresh_token=%s&client_id=%s&client_secret=%s"
      "&grant_type=refresh_token",
      refresh, client, secret);
  if (length <= 0 || (size_t)length >= sizeof(body))
    return CONNECTOR_INVALID;
  Connector_HTTP_Response token = {0};
  Connector_Status status = token_request(config, body, &token, NULL, arena);
  if (status != CONNECTOR_OK || !json_is_bounded(token.body, token.body_length))
    return status == CONNECTOR_OK ? CONNECTOR_PROVIDER_ERROR : status;
  Dowa_JSON_Value parsed = Dowa_JSON_Parse(
      token.body, (int32)token.body_length, arena);
  if (parsed.type != DOWA_JSON_OBJECT ||
      !copy_json_string(
          parsed.object_val, "access_token", account->access_token,
          sizeof(account->access_token)))
    return CONNECTOR_PROVIDER_ERROR;
  account->expires_at = (int64)time(NULL) +
      (int64)Dowa_JSON_Get_Number(parsed.object_val, "expires_in");
  return CONNECTOR_OK;
}

Connector_Status Connector_Google_Revoke(
    const Connector_Google_Config *config, const char *token, Dowa_Arena *arena)
{
  if (!config || !token || !arena)
    return CONNECTOR_INVALID;
  char encoded[4096], body[4200];
  if (!Connector_Form_Encode(token, encoded, sizeof(encoded)))
    return CONNECTOR_INVALID;
  snprintf(body, sizeof(body), "token=%s", encoded);
  Connector_HTTP_Request request = {
    .method = "POST", .url = config->oauth_revoke_url,
    .content_type = "application/x-www-form-urlencoded",
    .body = body, .body_length = strlen(body)
  };
  Connector_HTTP_Response response = {0};
  Connector_Status status = transport(config, &request, NULL, &response, arena);
  return status == CONNECTOR_OK && response.status_code >= 200 &&
      response.status_code < 300 ? CONNECTOR_OK : CONNECTOR_PROVIDER_ERROR;
}

Connector_Status Connector_Google_Execute(
    const Connector_Google_Config *config, Connector_Operation operation,
    const Connector_Provider_Request *request, const char *access_token,
    Connector_Provider_Response *response, Dowa_Arena *arena)
{
  if (!config || !request || !access_token || !response || !arena ||
      !Connector_Operation_Is_Allowed(operation) ||
      request->body_length > CONNECTOR_MAX_JSON_BYTES)
    return CONNECTOR_INVALID;
  const char *base = operation <= CONNECTOR_OP_DRIVE_UPDATE
      ? ((operation == CONNECTOR_OP_DRIVE_UPLOAD ||
          operation == CONNECTOR_OP_DRIVE_UPDATE) && request->body_length
             ? config->drive_upload_url : config->drive_api_url)
      : config->gmail_api_url;
  char *url = url_join(arena, base, request->path, request->query);
  if (!url)
    return CONNECTOR_ERROR;
  Connector_HTTP_Request provider_request = {
    .method = request->method,
    .url = url,
    .content_type = request->content_type,
    .body = request->body,
    .body_length = request->body_length,
    .download_path = request->download_path
  };
  Connector_HTTP_Response provider_response = {0};
  Connector_Status status = transport(
      config, &provider_request, access_token, &provider_response, arena);
  if (status != CONNECTOR_OK)
    return status;
  response->provider_status = provider_response.status_code;
  response->body = provider_response.body;
  response->body_length = provider_response.body_length;
  response->status = provider_response.status_code >= 200 &&
      provider_response.status_code < 300
      ? CONNECTOR_OK : CONNECTOR_PROVIDER_ERROR;
  return response->status;
}