Skip to content

Commit 90b0cd1

Browse files
committed
feat(network): reuse CloudSync runtime tickets
- cache rotated CloudSync ticket headers and send them on later sync API requests - support ticket reuse in both libcurl and native Apple network transports - clear cached tickets when auth or endpoints change - add CLOUDSYNC_NETWORK_TICKET=0 opt-out and trace logging for benchmark comparisons
1 parent b947e46 commit 90b0cd1

3 files changed

Lines changed: 192 additions & 13 deletions

File tree

src/network/network.c

Lines changed: 148 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include <inttypes.h>
1212
#include <stdlib.h>
1313
#include <string.h>
14+
#include <ctype.h>
1415

1516
#ifdef CLOUDSYNC_NETWORK_TRACE
1617
#ifdef _WIN32
@@ -71,10 +72,13 @@ struct network_data {
7172
char site_id[UUID_STR_MAXLEN];
7273
char *authentication; // apikey or token
7374
char *org_id; // organization ID for X-CloudSync-Org header
75+
char *ticket; // optional short-lived sync runtime ticket
76+
char *ticket_expires_at;
7477
char *check_endpoint;
7578
char *upload_endpoint;
7679
char *apply_endpoint;
7780
char *status_endpoint;
81+
int ticket_enabled;
7882
#ifndef CLOUDSYNC_OMIT_CURL
7983
CURL *api_curl;
8084
CURL *artifact_curl;
@@ -163,6 +167,11 @@ typedef struct {
163167
size_t read_pos;
164168
} network_read_data;
165169

170+
typedef struct {
171+
char *ticket;
172+
char *expires_at;
173+
} network_ticket_headers;
174+
166175
static const char *cloudsync_default_headers[] = {
167176
CLOUDSYNC_HEADER_VERSION_LINE,
168177
};
@@ -192,12 +201,25 @@ char *network_data_get_orgid (network_data *data) {
192201
return data->org_id;
193202
}
194203

204+
char *network_data_get_ticket (network_data *data) {
205+
return data->ticket;
206+
}
207+
208+
static void network_data_clear_ticket (network_data *data) {
209+
if (!data) return;
210+
if (data->ticket) cloudsync_memory_free(data->ticket);
211+
if (data->ticket_expires_at) cloudsync_memory_free(data->ticket_expires_at);
212+
data->ticket = NULL;
213+
data->ticket_expires_at = NULL;
214+
}
215+
195216
bool network_data_set_endpoints (network_data *data, char *auth, char *check, char *upload, char *apply, char *status) {
196217
// sanity check
197218
if (!check || !upload) return false;
198219

199220
// always free previous owned pointers
200221
if (data->authentication) cloudsync_memory_free(data->authentication);
222+
network_data_clear_ticket(data);
201223
if (data->check_endpoint) cloudsync_memory_free(data->check_endpoint);
202224
if (data->upload_endpoint) cloudsync_memory_free(data->upload_endpoint);
203225
if (data->apply_endpoint) cloudsync_memory_free(data->apply_endpoint);
@@ -259,6 +281,7 @@ void network_data_free (network_data *data) {
259281
#endif
260282
if (data->authentication) cloudsync_memory_free(data->authentication);
261283
if (data->org_id) cloudsync_memory_free(data->org_id);
284+
network_data_clear_ticket(data);
262285
if (data->check_endpoint) cloudsync_memory_free(data->check_endpoint);
263286
if (data->upload_endpoint) cloudsync_memory_free(data->upload_endpoint);
264287
if (data->apply_endpoint) cloudsync_memory_free(data->apply_endpoint);
@@ -268,27 +291,68 @@ void network_data_free (network_data *data) {
268291

269292
// MARK: - Utils -
270293

294+
static bool network_endpoint_is_api(network_data *data, const char *endpoint) {
295+
if (!data || !endpoint) return false;
296+
return (data->check_endpoint && strcmp(endpoint, data->check_endpoint) == 0) ||
297+
(data->upload_endpoint && strcmp(endpoint, data->upload_endpoint) == 0) ||
298+
(data->apply_endpoint && strcmp(endpoint, data->apply_endpoint) == 0) ||
299+
(data->status_endpoint && strcmp(endpoint, data->status_endpoint) == 0);
300+
}
301+
302+
static bool network_env_disabled(const char *value) {
303+
return value && (strcmp(value, "0") == 0 || strcmp(value, "false") == 0 || strcmp(value, "off") == 0 || strcmp(value, "no") == 0);
304+
}
305+
306+
static bool network_ticket_enabled(network_data *data) {
307+
if (!data) return false;
308+
if (data->ticket_enabled == 0) {
309+
const char *value = getenv("CLOUDSYNC_NETWORK_TICKET");
310+
data->ticket_enabled = network_env_disabled(value) ? -1 : 1;
311+
}
312+
return data->ticket_enabled > 0;
313+
}
314+
315+
bool network_data_should_use_ticket (network_data *data, const char *endpoint, const char *authentication) {
316+
return data && authentication && authentication[0] != '\0' && data->ticket && data->ticket[0] != '\0' &&
317+
network_ticket_enabled(data) && network_endpoint_is_api(data, endpoint);
318+
}
319+
320+
void network_data_update_ticket (network_data *data, const char *ticket, const char *expires_at) {
321+
if (!data || !ticket || ticket[0] == '\0') return;
322+
323+
char *ticket_copy = cloudsync_string_dup(ticket);
324+
if (!ticket_copy) return;
325+
326+
char *expires_copy = NULL;
327+
if (expires_at && expires_at[0] != '\0') {
328+
expires_copy = cloudsync_string_dup(expires_at);
329+
if (!expires_copy) {
330+
cloudsync_memory_free(ticket_copy);
331+
return;
332+
}
333+
}
334+
335+
network_data_clear_ticket(data);
336+
data->ticket = ticket_copy;
337+
data->ticket_expires_at = expires_copy;
338+
339+
#ifdef CLOUDSYNC_NETWORK_TRACE
340+
fprintf(stderr,
341+
"[cloudsync-network] received_ticket=%s expires_at=%s\n",
342+
data->ticket, data->ticket_expires_at ? data->ticket_expires_at : "");
343+
#endif
344+
}
345+
271346
#ifndef CLOUDSYNC_OMIT_CURL
272347
static bool network_curl_pool_enabled(network_data *data) {
273348
if (!data) return false;
274349
if (data->curl_pool_enabled == 0) {
275350
const char *value = getenv("CLOUDSYNC_CURL_POOL");
276-
data->curl_pool_enabled = 1;
277-
if (value && (strcmp(value, "0") == 0 || strcmp(value, "false") == 0 || strcmp(value, "off") == 0 || strcmp(value, "no") == 0)) {
278-
data->curl_pool_enabled = -1;
279-
}
351+
data->curl_pool_enabled = network_env_disabled(value) ? -1 : 1;
280352
}
281353
return data->curl_pool_enabled > 0;
282354
}
283355

284-
static bool network_endpoint_is_api(network_data *data, const char *endpoint) {
285-
if (!data || !endpoint) return false;
286-
return (data->check_endpoint && strcmp(endpoint, data->check_endpoint) == 0) ||
287-
(data->upload_endpoint && strcmp(endpoint, data->upload_endpoint) == 0) ||
288-
(data->apply_endpoint && strcmp(endpoint, data->apply_endpoint) == 0) ||
289-
(data->status_endpoint && strcmp(endpoint, data->status_endpoint) == 0);
290-
}
291-
292356
static CURL *network_curl_for_endpoint(network_data *data, const char *endpoint, bool *pooled) {
293357
if (pooled) *pooled = false;
294358
if (!network_curl_pool_enabled(data)) {
@@ -340,14 +404,65 @@ static size_t network_receive_callback (void *ptr, size_t size, size_t nmemb, vo
340404
return (size * nmemb);
341405
}
342406

407+
static bool network_header_eq(const char *line, size_t len, const char *name) {
408+
size_t name_len = strlen(name);
409+
if (len <= name_len || line[name_len] != ':') return false;
410+
for (size_t i = 0; i < name_len; i++) {
411+
if (tolower((unsigned char)line[i]) != tolower((unsigned char)name[i])) return false;
412+
}
413+
return true;
414+
}
415+
416+
static char *network_header_value_dup(const char *line, size_t len, const char *name) {
417+
size_t name_len = strlen(name);
418+
const char *start = line + name_len + 1;
419+
const char *end = line + len;
420+
421+
while (start < end && (*start == ' ' || *start == '\t')) start++;
422+
while (end > start && (end[-1] == '\r' || end[-1] == '\n' || end[-1] == ' ' || end[-1] == '\t')) end--;
423+
424+
size_t value_len = (size_t)(end - start);
425+
char *value = cloudsync_memory_zeroalloc(value_len + 1);
426+
if (!value) return NULL;
427+
memcpy(value, start, value_len);
428+
value[value_len] = '\0';
429+
return value;
430+
}
431+
432+
static size_t network_header_callback(char *buffer, size_t size, size_t nitems, void *userdata) {
433+
network_ticket_headers *ticket_headers = (network_ticket_headers *)userdata;
434+
size_t len = size * nitems;
435+
436+
if (network_header_eq(buffer, len, CLOUDSYNC_HEADER_TICKET)) {
437+
char *ticket = network_header_value_dup(buffer, len, CLOUDSYNC_HEADER_TICKET);
438+
if (ticket) {
439+
if (ticket_headers->ticket) cloudsync_memory_free(ticket_headers->ticket);
440+
ticket_headers->ticket = ticket;
441+
}
442+
} else if (network_header_eq(buffer, len, CLOUDSYNC_HEADER_TICKET_EXPIRES_AT)) {
443+
char *expires_at = network_header_value_dup(buffer, len, CLOUDSYNC_HEADER_TICKET_EXPIRES_AT);
444+
if (expires_at) {
445+
if (ticket_headers->expires_at) cloudsync_memory_free(ticket_headers->expires_at);
446+
ticket_headers->expires_at = expires_at;
447+
}
448+
}
449+
450+
return len;
451+
}
452+
343453
NETWORK_RESULT network_receive_buffer (network_data *data, const char *endpoint, const char *authentication, bool zero_terminated, bool is_post_request, char *json_payload, const char **extra_headers, int nextra_headers) {
344454
char *buffer = NULL;
345455
size_t blen = 0;
346456
struct curl_slist* headers = NULL;
457+
network_ticket_headers ticket_headers = {NULL, NULL};
347458
char errbuf[CURL_ERROR_SIZE] = {0};
348459
long response_code = 0;
349460
bool pooled = false;
461+
bool using_ticket = network_data_should_use_ticket(data, endpoint, authentication);
350462
const char *method = (json_payload || is_post_request) ? "POST" : "GET";
463+
#ifndef CLOUDSYNC_NETWORK_TRACE
464+
(void)method;
465+
#endif
351466
#ifdef CLOUDSYNC_NETWORK_TRACE
352467
double trace_start_ms = network_trace_now_ms();
353468
#endif
@@ -397,12 +512,21 @@ NETWORK_RESULT network_receive_buffer (network_data *data, const char *endpoint,
397512
if (!tmp) {rc = CURLE_OUT_OF_MEMORY; goto cleanup;}
398513
headers = tmp;
399514
}
515+
if (using_ticket) {
516+
char ticket_header[CLOUDSYNC_SESSION_TOKEN_MAXSIZE];
517+
snprintf(ticket_header, sizeof(ticket_header), "%s: %s", CLOUDSYNC_HEADER_TICKET, data->ticket);
518+
struct curl_slist *tmp = curl_slist_append(headers, ticket_header);
519+
if (!tmp) {rc = CURLE_OUT_OF_MEMORY; goto cleanup;}
520+
headers = tmp;
521+
}
400522

401523
if (headers) curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
402524

403525
network_buffer netdata = {NULL, 0, 0, (zero_terminated) ? 1 : 0};
404526
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &netdata);
405527
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, network_receive_callback);
528+
curl_easy_setopt(curl, CURLOPT_HEADERDATA, &ticket_headers);
529+
curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, network_header_callback);
406530

407531
// add optional JSON payload (implies setting CURLOPT_POST to 1)
408532
// or set the CURLOPT_POST option
@@ -419,13 +543,18 @@ NETWORK_RESULT network_receive_buffer (network_data *data, const char *endpoint,
419543
if (rc == CURLE_OK) {
420544
buffer = netdata.buffer;
421545
blen = netdata.bused;
546+
if (response_code < 400 && ticket_headers.ticket) {
547+
network_data_update_ticket(data, ticket_headers.ticket, ticket_headers.expires_at);
548+
}
422549
} else if (netdata.buffer) {
423550
cloudsync_memory_free(netdata.buffer);
424551
netdata.buffer = NULL;
425552
}
426553

427554
cleanup:
428555
if (headers) curl_slist_free_all(headers);
556+
if (ticket_headers.ticket) cloudsync_memory_free(ticket_headers.ticket);
557+
if (ticket_headers.expires_at) cloudsync_memory_free(ticket_headers.expires_at);
429558

430559
// build result
431560
NETWORK_RESULT result = {0, NULL, 0, NULL, NULL};
@@ -440,6 +569,10 @@ NETWORK_RESULT network_receive_buffer (network_data *data, const char *endpoint,
440569
}
441570

442571
#ifdef CLOUDSYNC_NETWORK_TRACE
572+
fprintf(stderr,
573+
"[cloudsync-network] endpoint=%s using_ticket=%s\n",
574+
network_trace_endpoint_name(data, endpoint),
575+
using_ticket ? "true" : "false");
443576
network_trace_log_curl(data, method, endpoint, response_code, result.code, result.blen, curl, pooled, network_trace_now_ms() - trace_start_ms);
444577
#endif
445578
if (curl && !pooled) curl_easy_cleanup(curl);
@@ -886,6 +1019,8 @@ static bool network_compute_endpoints_with_address (sqlite3_context *context, ne
8861019
snprintf(status_endpoint, requested, "%s/%s/%s/%s/%s",
8871020
address, CLOUDSYNC_ENDPOINT_PREFIX, managedDatabaseId, data->site_id, CLOUDSYNC_ENDPOINT_STATUS);
8881021

1022+
network_data_clear_ticket(data);
1023+
8891024
if (data->check_endpoint) cloudsync_memory_free(data->check_endpoint);
8901025
data->check_endpoint = check_endpoint;
8911026

@@ -1001,6 +1136,7 @@ bool cloudsync_network_set_authentication_token (sqlite3_context *context, const
10011136
if (!new_auth_token) return false;
10021137

10031138
if (data->authentication) cloudsync_memory_free(data->authentication);
1139+
network_data_clear_ticket(data);
10041140
data->authentication = new_auth_token;
10051141

10061142
return true;

src/network/network.m

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ NETWORK_RESULT network_receive_buffer(network_data *data, const char *endpoint,
8282
double trace_start_ms = network_trace_now_ms();
8383
#endif
8484
const char *method = (json_payload || is_post_request) ? "POST" : "GET";
85+
bool using_ticket = network_data_should_use_ticket(data, endpoint, authentication);
86+
#ifndef CLOUDSYNC_NETWORK_TRACE
87+
(void)method;
88+
#endif
8589

8690
NSString *urlString = [NSString stringWithUTF8String:endpoint];
8791
NSURL *url = [NSURL URLWithString:urlString];
@@ -120,6 +124,10 @@ NETWORK_RESULT network_receive_buffer(network_data *data, const char *endpoint,
120124
NSString *authString = [NSString stringWithFormat:@"Bearer %s", authentication];
121125
[request setValue:authString forHTTPHeaderField:@"Authorization"];
122126
}
127+
if (using_ticket) {
128+
char *ticket = network_data_get_ticket(data);
129+
[request setValue:[NSString stringWithUTF8String:ticket] forHTTPHeaderField:@CLOUDSYNC_HEADER_TICKET];
130+
}
123131

124132
if (json_payload) {
125133
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
@@ -133,6 +141,8 @@ NETWORK_RESULT network_receive_buffer(network_data *data, const char *endpoint,
133141
__block NSString *responseError = nil;
134142
__block NSInteger statusCode = 0;
135143
__block NSInteger errorCode = 0;
144+
__block NSString *responseTicket = nil;
145+
__block NSString *responseTicketExpiresAt = nil;
136146

137147
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
138148

@@ -145,7 +155,18 @@ NETWORK_RESULT network_receive_buffer(network_data *data, const char *endpoint,
145155
errorCode = [error code];
146156
}
147157
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
148-
statusCode = [(NSHTTPURLResponse *)response statusCode];
158+
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
159+
statusCode = [httpResponse statusCode];
160+
NSDictionary *headers = [httpResponse allHeaderFields];
161+
for (id key in headers) {
162+
NSString *name = [key description];
163+
NSString *value = [[headers objectForKey:key] description];
164+
if ([name caseInsensitiveCompare:@CLOUDSYNC_HEADER_TICKET] == NSOrderedSame) {
165+
responseTicket = value;
166+
} else if ([name caseInsensitiveCompare:@CLOUDSYNC_HEADER_TICKET_EXPIRES_AT] == NSOrderedSame) {
167+
responseTicketExpiresAt = value;
168+
}
169+
}
149170
}
150171
dispatch_semaphore_signal(sema);
151172
}];
@@ -154,11 +175,20 @@ NETWORK_RESULT network_receive_buffer(network_data *data, const char *endpoint,
154175
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
155176
[session finishTasksAndInvalidate];
156177

178+
if (!responseError && (statusCode >= 200 && statusCode < 300) && responseTicket && [responseTicket length] > 0) {
179+
network_data_update_ticket(data, [responseTicket UTF8String],
180+
responseTicketExpiresAt ? [responseTicketExpiresAt UTF8String] : NULL);
181+
}
182+
157183
if (!responseError && (statusCode >= 200 && statusCode < 300)) {
158184
// check if OK should be returned
159185
if (responseData == nil || [responseData length] == 0) {
160186
NETWORK_RESULT result = {CLOUDSYNC_NETWORK_OK, NULL, 0, NULL, NULL};
161187
#ifdef CLOUDSYNC_NETWORK_TRACE
188+
fprintf(stderr,
189+
"[cloudsync-network] endpoint=%s using_ticket=%s\n",
190+
network_trace_endpoint_name(data, endpoint),
191+
using_ticket ? "true" : "false");
162192
network_trace_log(data, method, endpoint, (long)statusCode, result.code, 0, network_trace_now_ms() - trace_start_ms);
163193
#endif
164194
return result;
@@ -187,6 +217,10 @@ NETWORK_RESULT network_receive_buffer(network_data *data, const char *endpoint,
187217
result.xfree = network_buffer_cleanup;
188218

189219
#ifdef CLOUDSYNC_NETWORK_TRACE
220+
fprintf(stderr,
221+
"[cloudsync-network] endpoint=%s using_ticket=%s\n",
222+
network_trace_endpoint_name(data, endpoint),
223+
using_ticket ? "true" : "false");
190224
network_trace_log(data, method, endpoint, (long)statusCode, result.code, result.blen, network_trace_now_ms() - trace_start_ms);
191225
#endif
192226
return result;
@@ -213,6 +247,10 @@ NETWORK_RESULT network_receive_buffer(network_data *data, const char *endpoint,
213247
result.blen = responseError ? (size_t)errorCode : (size_t)statusCode;
214248

215249
#ifdef CLOUDSYNC_NETWORK_TRACE
250+
fprintf(stderr,
251+
"[cloudsync-network] endpoint=%s using_ticket=%s\n",
252+
network_trace_endpoint_name(data, endpoint),
253+
using_ticket ? "true" : "false");
216254
network_trace_log(data, method, endpoint, (long)statusCode, result.code, 0, network_trace_now_ms() - trace_start_ms);
217255
#endif
218256
return result;

src/network/network_private.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
#define CLOUDSYNC_ENDPOINT_STATUS "status"
1717
#define CLOUDSYNC_HEADER_ORG "X-CloudSync-Org"
1818
#define CLOUDSYNC_HEADER_VERSION "X-CloudSync-Version"
19+
#define CLOUDSYNC_HEADER_TICKET "X-CloudSync-Ticket"
20+
#define CLOUDSYNC_HEADER_TICKET_EXPIRES_AT "X-CloudSync-Ticket-Expires-At"
1921
// CLOUDSYNC_VERSION is defined in cloudsync.h — include it before this header at use sites.
2022
#define CLOUDSYNC_HEADER_VERSION_LINE CLOUDSYNC_HEADER_VERSION ": " CLOUDSYNC_VERSION
2123
#define CLOUDSYNC_HEADER_CHECK_CAPABILITIES "X-CloudSync-Capabilities: check-status-response"
@@ -36,6 +38,9 @@ typedef struct {
3638

3739
char *network_data_get_siteid (network_data *data);
3840
char *network_data_get_orgid (network_data *data);
41+
char *network_data_get_ticket (network_data *data);
42+
bool network_data_should_use_ticket (network_data *data, const char *endpoint, const char *authentication);
43+
void network_data_update_ticket (network_data *data, const char *ticket, const char *expires_at);
3944
bool network_data_set_endpoints (network_data *data, char *auth, char *check, char *upload, char *apply, char *status);
4045

4146
bool network_send_buffer(network_data *data, const char *endpoint, const char *authentication, const void *blob, int blob_size);

0 commit comments

Comments
 (0)