ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
lib/network/http/client/client.c
Go to the documentation of this file.
1
2/* Storage capacities in elements; text buffers include the terminating NUL. */
3#define NETWORK_HTTP_CLIENT_SIZE_TEXT_BUFFER_SIZE 32
4
5#include <defer.h>
12
13#include <ascii-chat/otel.h>
15#include <ascii-chat/common.h>
17#include <ascii-chat/version.h>
19#include <libplatform/socket.h>
20
21#include <ascii-chat-deps/bearssl/inc/bearssl.h>
22#ifndef _WIN32
23#include <netdb.h>
24#include <sys/socket.h>
25#include <arpa/inet.h>
26#else
27#include <winsock2.h>
28#include <ws2tcpip.h>
29#endif
30#include <stdio.h>
31#include <stdlib.h>
32#include <string.h>
33
34// ============================================================================
35// BearSSL Socket Callbacks
36// ============================================================================
37
41static int sock_read(void *ctx, unsigned char *buf, size_t len) {
42 socket_t *sock = (socket_t *)ctx;
43 ssize_t n = recv(*sock, (char *)buf, (int)len, 0);
44 if (n < 0) {
45 SET_ERRNO_SYS(ERROR_NETWORK, "Failed to receive data from socket");
46 return -1; // Error
47 }
48 if (n == 0) {
49 SET_ERRNO(ERROR_NETWORK, "Connection closed by remote");
50 return -1; // Connection closed
51 }
52 return (int)n;
53}
54
58static int sock_write(void *ctx, const unsigned char *buf, size_t len) {
59 socket_t *sock = (socket_t *)ctx;
60 ssize_t n = send(*sock, (const char *)buf, (int)len, 0);
61 if (n < 0) {
62 SET_ERRNO_SYS(ERROR_NETWORK, "Failed to send data to socket");
63 return -1; // Error
64 }
65 return (int)n;
66}
67
68// ============================================================================
69// HTTP Response Parsing
70// ============================================================================
71
76static char *extract_http_body(const char *response, size_t response_len) {
77 // Find "\r\n\r\n" (end of headers)
78 const char *body_start = strstr(response, "\r\n\r\n");
79 if (!body_start) {
80 log_error("No HTTP body found in response");
81 return NULL;
82 }
83 body_start += 4; // Skip "\r\n\r\n"
84
85 size_t body_len = response_len - (size_t)(body_start - response);
86 const char *transfer_encoding = strstr(response, "Transfer-Encoding:");
87 bool is_chunked = transfer_encoding && strstr(transfer_encoding, "chunked") != NULL;
88
89 if (!is_chunked) {
90 char *body = SAFE_MALLOC(body_len + 1, char *);
91 memcpy(body, body_start, body_len);
92 body[body_len] = '\0';
93 return body;
94 }
95
96 // GitHub commonly uses HTTP/1.1 chunked transfer encoding. The bytes after
97 // the headers are then a sequence of `<hex-size>\\r\\n<data>\\r\\n` chunks,
98 // not the JSON document itself. Decode into a bounded buffer before handing
99 // the body to JSON parsers.
100 SAFE_MALLOC_DEFER_FREE(body, body_len + 1, char *);
101 size_t decoded_len = 0;
102 const char *cursor = body_start;
103 const char *end = response + response_len;
104
105 while (cursor < end) {
106 const char *line_end = strstr(cursor, "\r\n");
107 if (!line_end || line_end >= end) {
108 log_error("Malformed chunked HTTP response: missing chunk size terminator");
109 return NULL;
110 }
111
113 size_t size_text_len = (size_t)(line_end - cursor);
114 const char *extension = memchr(cursor, ';', size_text_len);
115 if (extension) {
116 size_text_len = (size_t)(extension - cursor);
117 }
118 if (size_text_len == 0 || size_text_len >= sizeof(size_text)) {
119 log_error("Malformed chunked HTTP response: invalid chunk size");
120 return NULL;
121 }
122 memcpy(size_text, cursor, size_text_len);
123 size_text[size_text_len] = '\0';
124
125 char *size_end = NULL;
126 unsigned long long chunk_size = strtoull(size_text, &size_end, 16);
127 if (size_end == size_text || *size_end != '\0') {
128 log_error("Malformed chunked HTTP response: invalid chunk size '%s'", size_text);
129 return NULL;
130 }
131
132 cursor = line_end + 2;
133 if (chunk_size == 0) {
134 break;
135 }
136 if (chunk_size > (unsigned long long)(end - cursor) || (size_t)chunk_size > body_len - decoded_len) {
137 log_error("Malformed chunked HTTP response: chunk exceeds response body");
138 return NULL;
139 }
140
141 memcpy(body + decoded_len, cursor, (size_t)chunk_size);
142 decoded_len += (size_t)chunk_size;
143 cursor += (size_t)chunk_size;
144 if (cursor + 2 > end || cursor[0] != '\r' || cursor[1] != '\n') {
145 log_error("Malformed chunked HTTP response: missing chunk terminator");
146 return NULL;
147 }
148 cursor += 2;
149 }
150
151 body[decoded_len] = '\0';
152 char *result = body;
153 body = NULL;
154 return result;
155}
156
157// Return the status code, or -1 for a malformed status line.
158static int parse_http_status(const char *response, size_t length) {
159 if (length < 13 || strncmp(response, "HTTP/1.", sizeof("HTTP/1.") - 1) != 0 || response[7] == '\0' ||
160 response[8] != ' ' || response[9] < '0' || response[9] > '9' || response[10] < '0' || response[10] > '9' ||
161 response[11] < '0' || response[11] > '9' || (response[12] != ' ' && response[12] != '\r'))
162 return -1;
163 return (response[9] - '0') * 100 + (response[10] - '0') * 10 + response[11] - '0';
164}
165
166// ============================================================================
167// HTTPS GET Implementation
168// ============================================================================
169
170// Cap response size at 1 MB to prevent unbounded memory growth
171#define HTTPS_MAX_RESPONSE_SIZE (1024 * 1024)
172
173static void free_http_anchors(anchor_list *anchors) {
174 for (size_t i = 0; i < anchors->ptr; i++) {
175 free_ta_contents(&anchors->buf[i]);
176 }
177 SAFE_FREE(anchors->buf);
178}
179
180char *https_get(const char *hostname, const char *path) {
181 ASCII_OTEL_SCOPE(HTTP);
182 ascii_otel_http(&otel_scope, hostname, path);
183 char *body = NULL;
184 if (!hostname || !path) {
185 log_error("Invalid arguments");
186 return NULL;
187 }
188
189 char ascii_hostname[HOSTNAME_BUFFER_SIZE];
190 asciichat_error_t normalized = hostname_to_ascii(hostname, ascii_hostname, sizeof(ascii_hostname));
191 if (normalized != ASCIICHAT_OK)
192 return NULL;
193 hostname = ascii_hostname;
194
195 log_info("HTTPS GET https://%s%s", hostname, path);
196
197 // Load system CA certificates
198 char *pem_data = NULL;
199 size_t pem_size = 0;
200 if (platform_load_system_ca_certs(&pem_data, &pem_size) != 0) {
201 log_error("Failed to load system CA certificates");
202 return NULL;
203 }
204
205 // Parse PEM certificates into BearSSL trust anchors
207 defer(free_http_anchors(&anchors));
208 size_t num_anchors = read_trust_anchors_from_memory(&anchors, (unsigned char *)pem_data, pem_size);
209 SAFE_FREE(pem_data);
210
211 if (num_anchors == 0) {
212 log_error("No trust anchors loaded");
213 return NULL;
214 }
215 log_info("Loaded %zu trust anchors", num_anchors);
216
217 // Resolve hostname using getaddrinfo (modern, non-deprecated API)
218 struct addrinfo hints, *result;
219 memset(&hints, 0, sizeof(hints));
220 hints.ai_family = AF_INET; // IPv4
221 hints.ai_socktype = SOCK_STREAM;
222 hints.ai_protocol = IPPROTO_TCP;
223
224 int dns_result;
225 {
226 ASCII_OTEL_SCOPE(DNS_RESOLVE);
227 if (otel_scope.retained)
228 gotel_attr_string(&otel_scope.scope, "dns.question.name", hostname);
229 dns_result = getaddrinfo(hostname, "443", &hints, &result);
230 if (dns_result && otel_scope.retained)
231 gotel_error(&otel_scope.scope, "dns.error", "DNS lookup failed");
232 }
233 if (dns_result != 0) {
234 log_error("Failed to resolve hostname: %s", hostname);
235 return NULL;
236 }
237
238 defer(if (result) freeaddrinfo(result));
239
240 // Create TCP socket
241 socket_t sock = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
242 if (sock == INVALID_SOCKET_VALUE) {
243 log_error("Failed to create socket");
244 return NULL;
245 }
246
247 defer(if (sock != INVALID_SOCKET_VALUE) socket_close(sock));
248
249 // Set socket timeouts to avoid blocking indefinitely on stalled connections.
250 // Windows expects a DWORD timeout in milliseconds, while POSIX expects a
251 // struct timeval. Passing timeval to Winsock makes the timeout effectively
252 // a few milliseconds and commonly aborts the TLS handshake.
253#ifdef _WIN32
254 DWORD timeout_ms = 5000;
255 setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char *)&timeout_ms, sizeof(timeout_ms));
256 setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (const char *)&timeout_ms, sizeof(timeout_ms));
257#else
258 struct timeval timeout = {.tv_sec = 5, .tv_usec = 0};
259 setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char *)&timeout, sizeof(timeout));
260 setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (const char *)&timeout, sizeof(timeout));
261#endif
262
263 // Connect to server
264 if (connect(sock, result->ai_addr, (int)result->ai_addrlen) != 0) {
265 log_error("Failed to connect to %s:443", hostname);
266 return NULL;
267 }
268
269 freeaddrinfo(result);
270 result = NULL;
271
272 log_info("Connected to %s:443", hostname);
273
274 // Initialize BearSSL X.509 minimal validator
275 br_x509_minimal_context xc;
276 br_x509_minimal_init(&xc, &br_sha256_vtable, anchors.buf, anchors.ptr);
277
278 // Initialize BearSSL client context
279 br_ssl_client_context sc;
280 br_ssl_client_init_full(&sc, &xc, anchors.buf, anchors.ptr);
281
282 // Set I/O buffer
283 const size_t iobuf_size = BR_SSL_BUFSIZE_BIDI;
284 SAFE_MALLOC_DEFER_FREE(iobuf, iobuf_size, unsigned char *);
285 br_ssl_engine_set_buffer(&sc.eng, iobuf, BR_SSL_BUFSIZE_BIDI, 1);
286
287 // Initialize I/O context
288 br_sslio_context ioc;
289 br_sslio_init(&ioc, &sc.eng, sock_read, &sock, sock_write, &sock);
290
291 // Start TLS handshake
292 br_ssl_client_reset(&sc, hostname, 0);
293
294 log_info("Starting TLS handshake with %s", hostname);
295
296 // Build HTTP request
297 char request[BUFFER_SIZE_LARGE];
298 int request_len = safe_snprintf(request, sizeof(request),
299 "GET %s HTTP/1.1\r\n"
300 "Host: %s\r\n"
301 "Connection: close\r\n"
302 "User-Agent: ascii-chat/" ASCII_CHAT_VERSION_STRING "\r\n"
303 "\r\n",
304 path, hostname);
305
306 // Send HTTP request over TLS
307 if (br_sslio_write_all(&ioc, request, (size_t)request_len) != 0) {
308 log_error("Failed to send HTTP request");
309 return NULL;
310 }
311
312 br_sslio_flush(&ioc);
313 log_info("Sent HTTP request");
314
315 // Read HTTP response
316 size_t response_capacity = 8192;
317 size_t response_len = 0;
318 SAFE_MALLOC_DEFER_FREE(response_buf, response_capacity, char *);
319
320 while (1) {
321 // Ensure we have space to read
322 if (response_len + 1024 > response_capacity) {
323 response_capacity *= 2;
324 if (response_capacity > HTTPS_MAX_RESPONSE_SIZE) {
325 log_error("Response exceeds maximum size (%d bytes)", HTTPS_MAX_RESPONSE_SIZE);
326 return NULL;
327 }
328 response_buf = SAFE_REALLOC(response_buf, response_capacity, char *);
329 }
330
331 // Read data (reserve 1 byte for null terminator)
332 int n = br_sslio_read(&ioc, response_buf + response_len, response_capacity - response_len - 1);
333 if (n < 0) {
334 // Check for TLS errors
335 int err = br_ssl_engine_last_error(&sc.eng);
336 if (err != BR_ERR_OK) {
337 log_error("TLS error: %d", err);
338 return NULL;
339 }
340 // EOF or connection closed
341 break;
342 }
343 if (n == 0) {
344 break; // EOF
345 }
346
347 response_len += (size_t)n;
348 }
349
350 response_buf[response_len] = '\0';
351 log_info("Received %zu bytes", response_len);
352
353 // Close connection
354 br_sslio_close(&ioc);
355 socket_close(sock);
356 sock = INVALID_SOCKET_VALUE;
357
358 // Parse HTTP response
359 int code = parse_http_status(response_buf, response_len);
360 if (code >= 0 && otel_scope.retained)
361 gotel_http_attributes(&otel_scope.scope, "GET", code);
362 if (code != 200) {
363 if (code < 0)
364 SET_ERRNO(ERROR_NETWORK, "Invalid HTTP response: %s", response_buf);
365 else
366 SET_ERRNO(ERROR_NETWORK, "HTTP request failed: %.50s", response_buf);
367 return NULL;
368 }
369
370 body = extract_http_body(response_buf, response_len);
371 if (body)
372 ascii_otel_http_status(&otel_scope, 200);
373
374 return body;
375}
376
377// NOTE: fetch_github_ssh_keys, fetch_gitlab_ssh_keys, fetch_github_gpg_keys,
378// and fetch_gitlab_gpg_keys have been moved to crypto/keys/https_keys.c
379// They properly belong in the keys module, not the http_client module.
Synchronized per-thread pending error stacks and value snapshots. SET_ERRNO pushes,...
#define SET_ERRNO(code,...)
#define SET_ERRNO_SYS(code,...)
⚙️ Common definitions, error codes, macros, and types shared throughout the application
#define BUFFER_SIZE_LARGE
Large buffer size (1024 bytes).
#define SAFE_REALLOC(ptr, size, cast)
Definition common.h:285
#define SAFE_FREE(ptr)
Definition common.h:377
#define SAFE_MALLOC_DEFER_FREE(name, size, cast)
Definition common.h:386
#define SAFE_MALLOC(size, cast)
Definition common.h:265
br_x509_trust_anchor * buf
Definition pem.h:80
#define ANCHOR_LIST_INIT
Initializer for anchor_list.
Definition pem.h:94
size_t read_trust_anchors_from_memory(anchor_list *dst, const unsigned char *pem_data, size_t pem_len)
Read trust anchors from PEM-encoded data in memory.
Definition pem.c:457
void free_ta_contents(br_x509_trust_anchor *ta)
Free the contents of a trust anchor.
Definition pem.c:436
size_t ptr
Definition pem.h:81
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ ERROR_NETWORK
Definition error_codes.h:72
@ ASCIICHAT_OK
Definition error_codes.h:46
#define HOSTNAME_BUFFER_SIZE
#define log_error(...)
Log an ERROR message.
Definition log/log.h:634
#define log_info(...)
Log an INFO message.
Definition log/log.h:608
char * https_get(const char *hostname, const char *path)
Perform HTTPS GET request.
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Platform-safe snprintf wrapper.
asciichat_error_t platform_load_system_ca_certs(char **pem_data_out, size_t *pem_size_out)
Load system CA certificates for TLS/HTTPS.
asciichat_error_t hostname_to_ascii(const char *hostname, char *output, size_t capacity)
Definition url.c:103
int socket_t
Simple HTTPS client for fetching public keys from GitHub/GitLab.
#define NETWORK_HTTP_CLIENT_SIZE_TEXT_BUFFER_SIZE
#define HTTPS_MAX_RESPONSE_SIZE
void ascii_otel_http(ascii_otel_scope_t *s, const char *hostname, const char *path)
Definition otel.c:812
void ascii_otel_http_status(ascii_otel_scope_t *s, int status)
Definition otel.c:828
#define ASCII_OTEL_SCOPE(id)
Definition otel.h:72
BearSSL PEM and trust anchor utilities adapted for in-memory data.
🔤 String Manipulation and Shell Escaping Utilities
Vector type for trust anchors.
Definition pem.h:79
🌐 Production-Grade URL Validation and Parsing