ascii-chat 0.6.0
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
http_client.c
Go to the documentation of this file.
1
7#include "http_client.h"
8#include "../common.h"
9#include "../crypto/pem_utils.h"
10#include "version.h"
11#include "asciichat_errno.h"
12#include "platform/socket.h"
13
14#include <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 char *body;
80 body = SAFE_MALLOC(body_len + 1, char *);
81 memcpy(body, body_start, body_len);
82 body[body_len] = '\0';
83
84 return body;
85}
86
90static asciichat_error_t check_http_status(const char *response) {
91 // Look for "HTTP/1.x 200 OK"
92 if (strncmp(response, "HTTP/1.", 7) != 0) {
93 return SET_ERRNO(ERROR_NETWORK, "Invalid HTTP response: %s", response);
94 }
95
96 // Skip to status code
97 const char *status = response + 9;
98 if (strncmp(status, "200", 3) != 0) {
99 return SET_ERRNO(ERROR_NETWORK, "HTTP request failed: %.50s", response);
100 }
101
102 return ASCIICHAT_OK;
103}
104
105// ============================================================================
106// HTTPS GET Implementation
107// ============================================================================
108
109char *https_get(const char *hostname, const char *path) {
110 if (!hostname || !path) {
111 log_error("Invalid arguments to https_get");
112 return NULL;
113 }
114
115 log_info("HTTPS GET https://%s%s", hostname, path);
116
117 // Load system CA certificates
118 char *pem_data = NULL;
119 size_t pem_size = 0;
120 if (platform_load_system_ca_certs(&pem_data, &pem_size) != 0) {
121 log_error("Failed to load system CA certificates");
122 return NULL;
123 }
124
125 // Parse PEM certificates into BearSSL trust anchors
127 size_t num_anchors = read_trust_anchors_from_memory(&anchors, (unsigned char *)pem_data, pem_size);
128 SAFE_FREE(pem_data);
129
130 if (num_anchors == 0) {
131 log_error("No trust anchors loaded");
132 // Free anchors before returning to prevent leak
133 goto cleanup_anchors;
134 }
135 log_info("Loaded %zu trust anchors", num_anchors);
136
137 // Resolve hostname using getaddrinfo (modern, non-deprecated API)
138 struct addrinfo hints, *result;
139 memset(&hints, 0, sizeof(hints));
140 hints.ai_family = AF_INET; // IPv4
141 hints.ai_socktype = SOCK_STREAM;
142 hints.ai_protocol = IPPROTO_TCP;
143
144 if (getaddrinfo(hostname, "443", &hints, &result) != 0) {
145 log_error("Failed to resolve hostname: %s", hostname);
146 goto cleanup_anchors;
147 }
148
149 // Create TCP socket
150 socket_t sock = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
151 if (sock == INVALID_SOCKET_VALUE) {
152 log_error("Failed to create socket");
153 freeaddrinfo(result);
154 goto cleanup_anchors;
155 }
156
157 // Connect to server
158 if (connect(sock, result->ai_addr, (int)result->ai_addrlen) != 0) {
159 log_error("Failed to connect to %s:443", hostname);
160 socket_close(sock);
161 freeaddrinfo(result);
162 goto cleanup_anchors;
163 }
164
165 freeaddrinfo(result);
166
167 log_info("Connected to %s:443", hostname);
168
169 // Initialize BearSSL X.509 minimal validator
170 br_x509_minimal_context xc;
171 br_x509_minimal_init(&xc, &br_sha256_vtable, anchors.buf, anchors.ptr);
172
173 // Initialize BearSSL client context
174 br_ssl_client_context sc;
175 br_ssl_client_init_full(&sc, &xc, anchors.buf, anchors.ptr);
176
177 // Set I/O buffer
178 unsigned char *iobuf;
179 iobuf = SAFE_MALLOC(BR_SSL_BUFSIZE_BIDI, unsigned char *);
180 br_ssl_engine_set_buffer(&sc.eng, iobuf, BR_SSL_BUFSIZE_BIDI, 1);
181
182 // Initialize I/O context
183 br_sslio_context ioc;
184 br_sslio_init(&ioc, &sc.eng, sock_read, &sock, sock_write, &sock);
185
186 // Start TLS handshake
187 br_ssl_client_reset(&sc, hostname, 0);
188
189 log_info("Starting TLS handshake with %s", hostname);
190
191 // Build HTTP request
192 char request[BUFFER_SIZE_LARGE];
193 int request_len = safe_snprintf(request, sizeof(request),
194 "GET %s HTTP/1.1\r\n"
195 "Host: %s\r\n"
196 "Connection: close\r\n"
197 "User-Agent: ascii-chat/" ASCII_CHAT_VERSION_STRING "\r\n"
198 "\r\n",
199 path, hostname);
200
201 // Send HTTP request over TLS
202 if (br_sslio_write_all(&ioc, request, (size_t)request_len) != 0) {
203 log_error("Failed to send HTTP request");
204 SAFE_FREE(iobuf);
205 socket_close(sock);
206 goto cleanup_anchors;
207 }
208
209 br_sslio_flush(&ioc);
210 log_info("Sent HTTP request");
211
212 // Read HTTP response
213 char *response_buf = NULL;
214 size_t response_capacity = 8192;
215 size_t response_len = 0;
216 response_buf = SAFE_MALLOC(response_capacity, char *);
217
218 while (1) {
219 // Ensure we have space to read
220 if (response_len + 1024 > response_capacity) {
221 response_capacity *= 2;
222 response_buf = SAFE_REALLOC(response_buf, response_capacity, char *);
223 }
224
225 // Read data
226 int n = br_sslio_read(&ioc, response_buf + response_len, response_capacity - response_len);
227 if (n < 0) {
228 // Check for TLS errors
229 int err = br_ssl_engine_last_error(&sc.eng);
230 if (err != BR_ERR_OK) {
231 log_error("TLS error: %d", err);
232 SAFE_FREE(response_buf);
233 SAFE_FREE(iobuf);
234 socket_close(sock);
235 goto cleanup_anchors;
236 }
237 // EOF or connection closed
238 break;
239 }
240 if (n == 0) {
241 break; // EOF
242 }
243
244 response_len += (size_t)n;
245 }
246
247 response_buf[response_len] = '\0';
248 log_info("Received %zu bytes", response_len);
249
250 // Close connection
251 br_sslio_close(&ioc);
252 socket_close(sock);
253
254 // Parse HTTP response
255 asciichat_error_t status = check_http_status(response_buf);
256 if (status != ASCIICHAT_OK) {
257 SAFE_FREE(response_buf);
258 SAFE_FREE(iobuf);
259 goto cleanup_anchors;
260 }
261
262 char *body = extract_http_body(response_buf, response_len);
263 SAFE_FREE(response_buf);
264 SAFE_FREE(iobuf);
265
266 // Cleanup trust anchors
267 for (size_t i = 0; i < anchors.ptr; i++) {
268 free_ta_contents(&anchors.buf[i]);
269 }
270 SAFE_FREE(anchors.buf);
271
272 return body;
273
274cleanup_anchors:
275 for (size_t i = 0; i < anchors.ptr; i++) {
276 free_ta_contents(&anchors.buf[i]);
277 }
278 SAFE_FREE(anchors.buf);
279 return NULL;
280}
281
282// NOTE: fetch_github_ssh_keys, fetch_gitlab_ssh_keys, fetch_github_gpg_keys,
283// and fetch_gitlab_gpg_keys have been moved to crypto/keys/https_keys.c
284// They properly belong in the keys module, not the http_client module.
⚠️‼️ Error and/or exit() when things go bad.
#define BUFFER_SIZE_LARGE
Large buffer size (1024 bytes)
#define SAFE_REALLOC(ptr, size, cast)
Definition common.h:228
#define SAFE_FREE(ptr)
Definition common.h:320
#define SAFE_MALLOC(size, cast)
Definition common.h:208
br_x509_trust_anchor * buf
Definition pem_utils.h:79
#define ANCHOR_LIST_INIT
Initializer for anchor_list.
Definition pem_utils.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_utils.c:440
void free_ta_contents(br_x509_trust_anchor *ta)
Free the contents of a trust anchor.
Definition pem_utils.c:419
size_t ptr
Definition pem_utils.h:80
#define SET_ERRNO_SYS(code, context_msg,...)
Set error code with custom message and system error context.
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:46
@ ERROR_NETWORK
Definition error_codes.h:69
@ ASCIICHAT_OK
Definition error_codes.h:48
#define log_error(...)
Log an ERROR message.
#define log_info(...)
Log an INFO message.
char * https_get(const char *hostname, const char *path)
Perform HTTPS GET request.
int socket_t
Socket handle type (POSIX: int)
Definition socket.h:50
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe version of snprintf that ensures null termination.
#define INVALID_SOCKET_VALUE
Invalid socket value (POSIX: -1)
Definition socket.h:52
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.
Simple HTTPS client for fetching public keys from GitHub/GitLab.
Cross-platform socket interface for ascii-chat.
Vector type for trust anchors.
Definition pem_utils.h:78