Skip to content

Commit 0bee8a9

Browse files
committed
feat(network)!: replace URL connection string with JSON in cloudsync_network_init
Breaking change: cloudsync_network_init now accepts a JSON object instead of a URL string. The JSON format supports multi-org CloudSync by adding projectID and organizationID fields, and inserts projectID into the endpoint path. An X-CloudSync-Org header is automatically sent with every request. New JSON format: {"address":"https://host:443","database":"db.sqlite","projectID":"abc","organizationID":"org","apikey":"KEY"} New endpoint format: {scheme}://{host}{port}/v2/cloudsync/{projectID}/{database}/{siteId}/{action} BREAKING CHANGE: URL connection strings are no longer accepted. Integration tests now require PROJECT_ID, ORGANIZATION_ID, and DATABASE environment variables.
1 parent 00692a2 commit 0bee8a9

File tree

4 files changed

+137
-194
lines changed

4 files changed

+137
-194
lines changed

src/network.c

Lines changed: 104 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ SQLITE_EXTENSION_INIT3
5151
struct network_data {
5252
char site_id[UUID_STR_MAXLEN];
5353
char *authentication; // apikey or token
54+
char *org_id; // organization ID for X-CloudSync-Org header
5455
char *check_endpoint;
5556
char *upload_endpoint;
5657
char *apply_endpoint;
@@ -85,6 +86,10 @@ char *network_data_get_siteid (network_data *data) {
8586
return data->site_id;
8687
}
8788

89+
char *network_data_get_orgid (network_data *data) {
90+
return data->org_id;
91+
}
92+
8893
bool network_data_set_endpoints (network_data *data, char *auth, char *check, char *upload, char *apply, char *status) {
8994
// sanity check
9095
if (!check || !upload) return false;
@@ -145,8 +150,9 @@ bool network_data_set_endpoints (network_data *data, char *auth, char *check, ch
145150

146151
void network_data_free (network_data *data) {
147152
if (!data) return;
148-
153+
149154
if (data->authentication) cloudsync_memory_free(data->authentication);
155+
if (data->org_id) cloudsync_memory_free(data->org_id);
150156
if (data->check_endpoint) cloudsync_memory_free(data->check_endpoint);
151157
if (data->upload_endpoint) cloudsync_memory_free(data->upload_endpoint);
152158
if (data->apply_endpoint) cloudsync_memory_free(data->apply_endpoint);
@@ -219,6 +225,14 @@ NETWORK_RESULT network_receive_buffer (network_data *data, const char *endpoint,
219225
headers = tmp;
220226
}
221227

228+
if (data->org_id) {
229+
char org_header[512];
230+
snprintf(org_header, sizeof(org_header), "%s: %s", CLOUDSYNC_HEADER_ORG, data->org_id);
231+
struct curl_slist *tmp = curl_slist_append(headers, org_header);
232+
if (!tmp) {rc = CURLE_OUT_OF_MEMORY; goto cleanup;}
233+
headers = tmp;
234+
}
235+
222236
if (json_payload) {
223237
struct curl_slist *tmp = curl_slist_append(headers, "Content-Type: application/json");
224238
if (!tmp) {rc = CURLE_OUT_OF_MEMORY; goto cleanup;}
@@ -331,7 +345,15 @@ bool network_send_buffer (network_data *data, const char *endpoint, const char *
331345
if (!tmp) {rc = CURLE_OUT_OF_MEMORY; goto cleanup;}
332346
headers = tmp;
333347
}
334-
348+
349+
if (data->org_id) {
350+
char org_header[512];
351+
snprintf(org_header, sizeof(org_header), "%s: %s", CLOUDSYNC_HEADER_ORG, data->org_id);
352+
struct curl_slist *tmp = curl_slist_append(headers, org_header);
353+
if (!tmp) {rc = CURLE_OUT_OF_MEMORY; goto cleanup;}
354+
headers = tmp;
355+
}
356+
335357
// Set headers if needed (S3 pre-signed URLs usually do not require additional headers)
336358
tmp = curl_slist_append(headers, "Content-Type: application/octet-stream");
337359
if (!tmp) {rc = CURLE_OUT_OF_MEMORY; goto cleanup;}
@@ -578,144 +600,95 @@ int network_extract_query_param (const char *query, const char *key, char *outpu
578600
return -3; // Key not found
579601
}
580602

581-
#if !defined(CLOUDSYNC_OMIT_CURL) || defined(SQLITE_WASM_EXTRA_INIT)
582603
bool network_compute_endpoints (sqlite3_context *context, network_data *data, const char *conn_string) {
583-
// compute endpoints
604+
// JSON format: {"address":"https://host:port","database":"db.sqlite","projectID":"abc","organizationID":"org","apikey":"KEY"}
584605
bool result = false;
585-
586-
char *scheme = NULL;
587-
char *host = NULL;
588-
char *port = NULL;
589-
char *database = NULL;
590-
char *query = NULL;
591-
606+
size_t conn_len = strlen(conn_string);
607+
608+
char *address = json_extract_string(conn_string, conn_len, "address");
609+
char *database = json_extract_string(conn_string, conn_len, "database");
610+
char *project_id = json_extract_string(conn_string, conn_len, "projectID");
611+
char *org_id = json_extract_string(conn_string, conn_len, "organizationID");
612+
char *apikey = json_extract_string(conn_string, conn_len, "apikey");
613+
char *token = json_extract_string(conn_string, conn_len, "token");
614+
592615
char *authentication = NULL;
593616
char *check_endpoint = NULL;
594617
char *upload_endpoint = NULL;
595618
char *apply_endpoint = NULL;
596619
char *status_endpoint = NULL;
597620

598-
char *conn_string_https = NULL;
599-
600-
#ifndef SQLITE_WASM_EXTRA_INIT
601-
CURLUcode rc = CURLUE_OUT_OF_MEMORY;
602-
CURLU *url = curl_url();
603-
if (!url) goto finalize;
604-
#endif
605-
606-
conn_string_https = cloudsync_string_replace_prefix(conn_string, "sqlitecloud://", "https://");
607-
if (!conn_string_https) goto finalize;
608-
609-
#ifndef SQLITE_WASM_EXTRA_INIT
610-
// set URL: https://UUID.g5.sqlite.cloud:443/chinook.sqlite?apikey=hWDanFolRT9WDK0p54lufNrIyfgLZgtMw6tb6fbPmpo
611-
rc = curl_url_set(url, CURLUPART_URL, conn_string_https, 0);
612-
if (rc != CURLUE_OK) goto finalize;
613-
614-
// https (MANDATORY)
615-
rc = curl_url_get(url, CURLUPART_SCHEME, &scheme, 0);
616-
if (rc != CURLUE_OK) goto finalize;
617-
618-
// UUID.g5.sqlite.cloud (MANDATORY)
619-
rc = curl_url_get(url, CURLUPART_HOST, &host, 0);
620-
if (rc != CURLUE_OK) goto finalize;
621-
622-
// 443 (OPTIONAL)
623-
rc = curl_url_get(url, CURLUPART_PORT, &port, 0);
624-
if (rc != CURLUE_OK && rc != CURLUE_NO_PORT) goto finalize;
625-
char *port_or_default = port && strcmp(port, "8860") != 0 ? port : CLOUDSYNC_DEFAULT_ENDPOINT_PORT;
626-
627-
// /chinook.sqlite (MANDATORY)
628-
rc = curl_url_get(url, CURLUPART_PATH, &database, 0);
629-
if (rc != CURLUE_OK) goto finalize;
630-
631-
// apikey=hWDanFolRT9WDK0p54lufNrIyfgLZgtMw6tb6fbPmpo (OPTIONAL)
632-
rc = curl_url_get(url, CURLUPART_QUERY, &query, 0);
633-
if (rc != CURLUE_OK && rc != CURLUE_NO_QUERY) goto finalize;
634-
#else
635-
// Parse: scheme://host[:port]/path?query
636-
const char *p = strstr(conn_string_https, "://");
637-
if (!p) goto finalize;
638-
scheme = substr(conn_string_https, p);
639-
p += 3;
640-
const char *host_start = p;
641-
const char *host_end = strpbrk(host_start, ":/?");
642-
if (!host_end) goto finalize;
643-
host = substr(host_start, host_end);
644-
p = host_end;
645-
if (*p == ':') {
646-
++p;
647-
const char *port_end = strpbrk(p, "/?");
648-
if (!port_end) goto finalize;
649-
port = substr(p, port_end);
650-
p = port_end;
651-
}
652-
if (*p == '/') {
653-
const char *path_start = p;
654-
const char *path_end = strchr(path_start, '?');
655-
if (!path_end) path_end = path_start + strlen(path_start);
656-
database = substr(path_start, path_end);
657-
p = path_end;
621+
// validate mandatory fields
622+
if (!address || !database || !project_id || !org_id) {
623+
sqlite3_result_error(context, "JSON must contain address, database, projectID, and organizationID", -1);
624+
sqlite3_result_error_code(context, SQLITE_ERROR);
625+
goto finalize;
658626
}
659-
if (*p == '?') {
660-
query = strdup(p);
627+
628+
// parse address: scheme://host[:port]
629+
const char *scheme_end = strstr(address, "://");
630+
if (!scheme_end) {
631+
sqlite3_result_error(context, "address must include scheme (e.g. https://host:port)", -1);
632+
sqlite3_result_error_code(context, SQLITE_ERROR);
633+
goto finalize;
661634
}
662-
if (!scheme || !host || !database) goto finalize;
663-
char *port_or_default = port && strcmp(port, "8860") != 0 ? port : CLOUDSYNC_DEFAULT_ENDPOINT_PORT;
664-
#endif
665-
666-
if (query != NULL) {
667-
char value[CLOUDSYNC_SESSION_TOKEN_MAXSIZE];
668-
if (!authentication && network_extract_query_param(query, "apikey", value, sizeof(value)) == 0) {
669-
authentication = network_authentication_token("apikey", value);
670-
}
671-
if (!authentication && network_extract_query_param(query, "token", value, sizeof(value)) == 0) {
672-
authentication = network_authentication_token("token", value);
673-
}
635+
636+
size_t scheme_len = scheme_end - address;
637+
const char *host_start = scheme_end + 3;
638+
const char *port_sep = strchr(host_start, ':');
639+
const char *host_end = port_sep ? port_sep : host_start + strlen(host_start);
640+
const char *port_str = port_sep ? port_sep + 1 : CLOUDSYNC_DEFAULT_ENDPOINT_PORT;
641+
642+
// build authentication from apikey or token
643+
if (apikey) {
644+
authentication = network_authentication_token("apikey", apikey);
645+
} else if (token) {
646+
authentication = network_authentication_token("token", token);
674647
}
675-
676-
size_t requested = strlen(scheme) + strlen(host) + strlen(port_or_default) + strlen(CLOUDSYNC_ENDPOINT_PREFIX) + strlen(database) + 64;
648+
649+
// build endpoints: {scheme}://{host}:{port}/v2/cloudsync/{projectID}/{database}/{siteId}/{action}
650+
size_t requested = scheme_len + 3 + (host_end - host_start) + 1 + strlen(port_str) + 1
651+
+ strlen(CLOUDSYNC_ENDPOINT_PREFIX) + 1 + strlen(project_id) + 1
652+
+ strlen(database) + 1 + UUID_STR_MAXLEN + 1 + 16;
677653
check_endpoint = (char *)cloudsync_memory_zeroalloc(requested);
678654
upload_endpoint = (char *)cloudsync_memory_zeroalloc(requested);
679655
apply_endpoint = (char *)cloudsync_memory_zeroalloc(requested);
680656
status_endpoint = (char *)cloudsync_memory_zeroalloc(requested);
681657

682-
if ((!upload_endpoint) || (!check_endpoint) || (!apply_endpoint) || (!status_endpoint)) goto finalize;
658+
if (!check_endpoint || !upload_endpoint || !apply_endpoint || !status_endpoint) {
659+
sqlite3_result_error_code(context, SQLITE_NOMEM);
660+
goto finalize;
661+
}
683662

684-
snprintf(check_endpoint, requested, "%s://%s:%s/%s%s/%s/%s", scheme, host, port_or_default, CLOUDSYNC_ENDPOINT_PREFIX, database, data->site_id, CLOUDSYNC_ENDPOINT_CHECK);
685-
snprintf(upload_endpoint, requested, "%s://%s:%s/%s%s/%s/%s", scheme, host, port_or_default, CLOUDSYNC_ENDPOINT_PREFIX, database, data->site_id, CLOUDSYNC_ENDPOINT_UPLOAD);
686-
snprintf(apply_endpoint, requested, "%s://%s:%s/%s%s/%s/%s", scheme, host, port_or_default, CLOUDSYNC_ENDPOINT_PREFIX, database, data->site_id, CLOUDSYNC_ENDPOINT_APPLY);
687-
snprintf(status_endpoint, requested, "%s://%s:%s/%s%s/%s/%s", scheme, host, port_or_default, CLOUDSYNC_ENDPOINT_PREFIX, database, data->site_id, CLOUDSYNC_ENDPOINT_STATUS);
663+
// format: scheme://host:port/v2/cloudsync/projectID/database/siteId/action
664+
snprintf(check_endpoint, requested, "%.*s://%.*s:%s/%s/%s/%s/%s/%s",
665+
(int)scheme_len, address, (int)(host_end - host_start), host_start, port_str,
666+
CLOUDSYNC_ENDPOINT_PREFIX, project_id, database, data->site_id, CLOUDSYNC_ENDPOINT_CHECK);
667+
snprintf(upload_endpoint, requested, "%.*s://%.*s:%s/%s/%s/%s/%s/%s",
668+
(int)scheme_len, address, (int)(host_end - host_start), host_start, port_str,
669+
CLOUDSYNC_ENDPOINT_PREFIX, project_id, database, data->site_id, CLOUDSYNC_ENDPOINT_UPLOAD);
670+
snprintf(apply_endpoint, requested, "%.*s://%.*s:%s/%s/%s/%s/%s/%s",
671+
(int)scheme_len, address, (int)(host_end - host_start), host_start, port_str,
672+
CLOUDSYNC_ENDPOINT_PREFIX, project_id, database, data->site_id, CLOUDSYNC_ENDPOINT_APPLY);
673+
snprintf(status_endpoint, requested, "%.*s://%.*s:%s/%s/%s/%s/%s/%s",
674+
(int)scheme_len, address, (int)(host_end - host_start), host_start, port_str,
675+
CLOUDSYNC_ENDPOINT_PREFIX, project_id, database, data->site_id, CLOUDSYNC_ENDPOINT_STATUS);
688676

689677
result = true;
690-
678+
691679
finalize:
692-
if (result == false) {
693-
// store proper result code/message
694-
#ifndef SQLITE_WASM_EXTRA_INIT
695-
if (rc != CURLUE_OK) sqlite3_result_error(context, curl_url_strerror(rc), -1);
696-
sqlite3_result_error_code(context, (rc != CURLUE_OK) ? SQLITE_ERROR : SQLITE_NOMEM);
697-
#else
698-
sqlite3_result_error(context, "URL parse error", -1);
699-
sqlite3_result_error_code(context, SQLITE_ERROR);
700-
#endif
701-
702-
// cleanup memory managed by the extension
703-
if (authentication) cloudsync_memory_free(authentication);
704-
if (check_endpoint) cloudsync_memory_free(check_endpoint);
705-
if (upload_endpoint) cloudsync_memory_free(upload_endpoint);
706-
if (apply_endpoint) cloudsync_memory_free(apply_endpoint);
707-
if (status_endpoint) cloudsync_memory_free(status_endpoint);
708-
}
709-
710680
if (result) {
711681
if (authentication) {
712682
if (data->authentication) cloudsync_memory_free(data->authentication);
713683
data->authentication = authentication;
714684
}
715-
685+
686+
if (data->org_id) cloudsync_memory_free(data->org_id);
687+
data->org_id = cloudsync_string_dup(org_id);
688+
716689
if (data->check_endpoint) cloudsync_memory_free(data->check_endpoint);
717690
data->check_endpoint = check_endpoint;
718-
691+
719692
if (data->upload_endpoint) cloudsync_memory_free(data->upload_endpoint);
720693
data->upload_endpoint = upload_endpoint;
721694

@@ -724,22 +697,24 @@ bool network_compute_endpoints (sqlite3_context *context, network_data *data, co
724697

725698
if (data->status_endpoint) cloudsync_memory_free(data->status_endpoint);
726699
data->status_endpoint = status_endpoint;
700+
} else {
701+
if (authentication) cloudsync_memory_free(authentication);
702+
if (check_endpoint) cloudsync_memory_free(check_endpoint);
703+
if (upload_endpoint) cloudsync_memory_free(upload_endpoint);
704+
if (apply_endpoint) cloudsync_memory_free(apply_endpoint);
705+
if (status_endpoint) cloudsync_memory_free(status_endpoint);
727706
}
728-
729-
// cleanup memory
730-
#ifndef SQLITE_WASM_EXTRA_INIT
731-
if (url) curl_url_cleanup(url);
732-
#endif
733-
if (scheme) curl_free(scheme);
734-
if (host) curl_free(host);
735-
if (port) curl_free(port);
736-
if (database) curl_free(database);
737-
if (query) curl_free(query);
738-
if (conn_string_https && conn_string_https != conn_string) cloudsync_memory_free(conn_string_https);
739-
707+
708+
// cleanup JSON-extracted strings
709+
if (address) cloudsync_memory_free(address);
710+
if (database) cloudsync_memory_free(database);
711+
if (project_id) cloudsync_memory_free(project_id);
712+
if (org_id) cloudsync_memory_free(org_id);
713+
if (apikey) cloudsync_memory_free(apikey);
714+
if (token) cloudsync_memory_free(token);
715+
740716
return result;
741717
}
742-
#endif
743718

744719
void network_result_to_sqlite_error (sqlite3_context *context, NETWORK_RESULT res, const char *default_error_message) {
745720
sqlite3_result_error(context, ((res.code == CLOUDSYNC_NETWORK_ERROR) && (res.buffer)) ? res.buffer : default_error_message, -1);
@@ -778,10 +753,9 @@ void cloudsync_network_init (sqlite3_context *context, int argc, sqlite3_value *
778753
// save site_id string representation: 01957493c6c07e14803727e969f1d2cc
779754
cloudsync_uuid_v7_stringify(site_id, netdata->site_id, false);
780755

781-
// connection string is something like:
782-
// https://UUID.g5.sqlite.cloud:443/chinook.sqlite?apikey=hWDanFolRT9WDK0p54lufNrIyfgLZgtMw6tb6fbPmpo
783-
// or https://UUID.g5.sqlite.cloud:443/chinook.sqlite
784-
// apikey part is optional and can be replaced by a session token once client is authenticated
756+
// connection string is a JSON object:
757+
// {"address":"https://UUID.sqlite.cloud:443","database":"chinook.sqlite","projectID":"abc123","organizationID":"org456","apikey":"KEY"}
758+
// apikey/token are optional and can be set later via cloudsync_network_set_token/cloudsync_network_set_apikey
785759

786760
const char *connection_param = (const char *)sqlite3_value_text(argv[0]);
787761

0 commit comments

Comments
 (0)