comparison seobeo/s_http_client.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 ce7f4400c2de
children
comparison
equal deleted inserted replaced
278:8d560f50ed4c 279:b3b547563ec7
38 { 38 {
39 if (headers[i].key && strcasecmp(headers[i].key, name) == 0) 39 if (headers[i].key && strcasecmp(headers[i].key, name) == 0)
40 return headers[i].value; 40 return headers[i].value;
41 } 41 }
42 return NULL; 42 return NULL;
43 }
44
45 static boolean Seobeo_Client_Header_Has_Token(
46 const char *value, const char *token)
47 {
48 if (!value || !token)
49 return FALSE;
50 size_t token_length = strlen(token);
51 const char *cursor = value;
52 while (*cursor)
53 {
54 while (*cursor == ' ' || *cursor == '\t' || *cursor == ',')
55 cursor++;
56 const char *end = cursor;
57 while (*end && *end != ',')
58 end++;
59 const char *trimmed_end = end;
60 while (trimmed_end > cursor &&
61 (trimmed_end[-1] == ' ' || trimmed_end[-1] == '\t'))
62 trimmed_end--;
63 if ((size_t)(trimmed_end - cursor) == token_length &&
64 strncasecmp(cursor, token, token_length) == 0)
65 return TRUE;
66 cursor = end;
67 }
68 return FALSE;
69 }
70
71 static boolean Seobeo_Client_Decode_Chunked(
72 const char *encoded, size_t encoded_length,
73 char *decoded, size_t decoded_capacity, size_t *decoded_length)
74 {
75 if (!encoded || !decoded || !decoded_length)
76 return FALSE;
77 size_t input_offset = 0;
78 size_t output_offset = 0;
79 while (input_offset < encoded_length)
80 {
81 size_t line_end = input_offset;
82 while (line_end + 1 < encoded_length &&
83 !(encoded[line_end] == '\r' && encoded[line_end + 1] == '\n'))
84 line_end++;
85 if (line_end + 1 >= encoded_length)
86 return FALSE;
87
88 size_t chunk_size = 0;
89 boolean have_digit = FALSE;
90 for (size_t i = input_offset; i < line_end && encoded[i] != ';'; i++)
91 {
92 uint8 c = (uint8)encoded[i];
93 int32 digit;
94 if (c >= '0' && c <= '9')
95 digit = c - '0';
96 else if (c >= 'a' && c <= 'f')
97 digit = c - 'a' + 10;
98 else if (c >= 'A' && c <= 'F')
99 digit = c - 'A' + 10;
100 else
101 return FALSE;
102 if (chunk_size > (SIZE_MAX - (size_t)digit) / 16)
103 return FALSE;
104 chunk_size = chunk_size * 16 + (size_t)digit;
105 have_digit = TRUE;
106 }
107 if (!have_digit)
108 return FALSE;
109 input_offset = line_end + 2;
110 if (chunk_size == 0)
111 {
112 *decoded_length = output_offset;
113 return TRUE;
114 }
115 if (chunk_size > encoded_length - input_offset ||
116 chunk_size > decoded_capacity - output_offset)
117 return FALSE;
118 memcpy(decoded + output_offset, encoded + input_offset, chunk_size);
119 output_offset += chunk_size;
120 input_offset += chunk_size;
121 if (input_offset + 1 >= encoded_length ||
122 encoded[input_offset] != '\r' || encoded[input_offset + 1] != '\n')
123 return FALSE;
124 input_offset += 2;
125 }
126 return FALSE;
43 } 127 }
44 128
45 static void Seobeo_Client_Parse_Url(const char *url, char **p_host, 129 static void Seobeo_Client_Parse_Url(const char *url, char **p_host,
46 char **p_port, char **p_path, boolean *p_use_tls, Dowa_Arena *p_arena) 130 char **p_port, char **p_path, boolean *p_use_tls, Dowa_Arena *p_arena)
47 { 131 {
403 Seobeo_Handle_Consume(p_handle, (uint32)hdr_len); 487 Seobeo_Handle_Consume(p_handle, (uint32)hdr_len);
404 488
405 size_t body_len = 0; 489 size_t body_len = 0;
406 const char *content_length = Seobeo_Client_Header_Value( 490 const char *content_length = Seobeo_Client_Header_Value(
407 p_resp->headers, "Content-Length"); 491 p_resp->headers, "Content-Length");
408 if (content_length) 492 const char *transfer_encoding = Seobeo_Client_Header_Value(
493 p_resp->headers, "Transfer-Encoding");
494 boolean is_chunked = Seobeo_Client_Header_Has_Token(
495 transfer_encoding, "chunked");
496 if (content_length && !is_chunked)
409 { 497 {
410 body_len = (size_t)strtoull(content_length, NULL, 10); 498 body_len = (size_t)strtoull(content_length, NULL, 10);
411 } 499 }
412 500
413 FILE *p_file = NULL; 501 FILE *p_file = NULL;
482 } 570 }
483 else 571 else
484 { 572 {
485 size_t cap = 1024 * 1024 * 5; 573 size_t cap = 1024 * 1024 * 5;
486 size_t used = 0; 574 size_t used = 0;
487 char *body = download_path ? NULL : Dowa_Arena_Allocate(p_resp->p_arena, cap); 575 char *body = (!download_path || is_chunked)
576 ? Dowa_Arena_Allocate(p_resp->p_arena, cap) : NULL;
488 577
489 while (1) 578 while (1)
490 { 579 {
491 int n = p_handle->read_buffer_len > 0 580 int n = p_handle->read_buffer_len > 0
492 ? (int)p_handle->read_buffer_len 581 ? (int)p_handle->read_buffer_len
493 : Seobeo_Handle_Read(p_handle); 582 : Seobeo_Handle_Read(p_handle);
494 if (n > 0) 583 if (n > 0)
495 { 584 {
496 last_progress_ms = Seobeo_Client_Monotonic_Milliseconds(); 585 last_progress_ms = Seobeo_Client_Monotonic_Milliseconds();
497 if (download_path) 586 if (download_path && !is_chunked)
498 { 587 {
499 fwrite(p_handle->read_buffer, 1, p_handle->read_buffer_len, p_file); 588 fwrite(p_handle->read_buffer, 1, p_handle->read_buffer_len, p_file);
500 used += p_handle->read_buffer_len; 589 used += p_handle->read_buffer_len;
501 } 590 }
502 else 591 else
531 Seobeo_Client_Response_Destroy(p_resp); 620 Seobeo_Client_Response_Destroy(p_resp);
532 return NULL; 621 return NULL;
533 } 622 }
534 } 623 }
535 624
536 if (!download_path) 625 if (is_chunked)
626 {
627 char *decoded = Dowa_Arena_Allocate(p_resp->p_arena, used + 1);
628 size_t decoded_length = 0;
629 if (!decoded ||
630 !Seobeo_Client_Decode_Chunked(
631 body, used, decoded, used, &decoded_length))
632 {
633 if (p_file) fclose(p_file);
634 Seobeo_Client_Response_Destroy(p_resp);
635 return NULL;
636 }
637 decoded[decoded_length] = '\0';
638 if (download_path)
639 {
640 fwrite(decoded, 1, decoded_length, p_file);
641 p_resp->body_length = decoded_length;
642 }
643 else
644 {
645 p_resp->body = decoded;
646 p_resp->body_length = decoded_length;
647 }
648 }
649 else if (!download_path)
537 { 650 {
538 p_resp->body = body; 651 p_resp->body = body;
539 p_resp->body_length = used; 652 p_resp->body_length = used;
540 if (used < cap) 653 if (used < cap)
541 body[used] = '\0'; 654 body[used] = '\0';