Mercurial
view connectors/google.c @ 281:c57149ad216e default tip
Copilot-Session: f68442b1-fa8f-46a0-9689-81710613bbd4
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Tue, 18 Aug 2026 22:18:15 -0700 |
| parents | b3b547563ec7 |
| 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; }