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