cwist
Modern C web framework with libttak runtime
Loading...
Searching...
No Matches
http_client.c
Go to the documentation of this file.
1
6#define _POSIX_C_SOURCE 200809L
10#include "curl_global.h"
11
12#include <stdio.h>
13#include <stdlib.h>
14#include <string.h>
15#include <strings.h>
16
17#include <curl/curl.h>
18#include <ctype.h>
19
20/* ------------------------------------------------------------------ */
21/* Response accumulator */
22/* ------------------------------------------------------------------ */
23
24typedef struct {
25 char *data;
26 size_t size;
27 size_t cap;
28} response_body_t;
29
30typedef struct {
32 long status_code;
33 int header_count;
34} response_headers_t;
35
50static size_t write_callback(void *contents, size_t size, size_t nmemb, void *userp) {
51 size_t total = size * nmemb;
52 response_body_t *body = (response_body_t *)userp;
53 /* Refuse bodies larger than the server-side cap to prevent an OOM DoS
54 * from a malicious or misbehaving server. */
55 if (body->size + total > CWIST_HTTP_MAX_BODY_SIZE) return 0;
56 size_t need = body->size + total;
57 if (need > body->cap) {
58 size_t new_cap = body->cap ? body->cap * 2 : 4096;
59 while (new_cap < need) new_cap *= 2;
60 char *tmp = realloc(body->data, new_cap);
61 if (!tmp) return 0;
62 body->data = tmp;
63 body->cap = new_cap;
64 }
65 memcpy(body->data + body->size, contents, total);
66 body->size += total;
67 return total;
68}
69
86static size_t header_callback(char *ptr, size_t size, size_t nmemb, void *userp) {
87 size_t total = size * nmemb;
88 response_headers_t *rh = (response_headers_t *)userp;
89
90 /* libcurl sends headers as "Name: value\r\n" or "HTTP/1.1 200 OK\r\n" */
91 if (total > 5 && strncasecmp(ptr, "HTTP/", 5) == 0) {
92 /* Status line: parse status code */
93 char *p = ptr;
94 while (*p && !isspace((unsigned char)*p)) p++;
95 while (*p && isspace((unsigned char)*p)) p++;
96 rh->status_code = strtol(p, NULL, 10);
97 /* Each status line starts a new response: a followed redirect hop
98 * or a 1xx interim reply. Keep only the final response's headers,
99 * and apply the header cap per response. */
101 rh->headers = NULL;
102 rh->header_count = 0;
103 return total;
104 }
105
106 /* Guard against header-count exhaustion attacks. */
107 if (rh->header_count >= 200) return 0;
108
109 /* Find colon separator */
110 char *colon = memchr(ptr, ':', total);
111 if (!colon) return total;
112
113 size_t name_len = (size_t)(colon - ptr);
114 if (name_len == 0) return total;
115
116 char *value = colon + 1;
117 size_t value_len = total - name_len - 1;
118
119 /* Trim leading spaces in value */
120 while (value_len > 0 && isspace((unsigned char)*value)) {
121 value++;
122 value_len--;
123 }
124 /* Trim trailing whitespace (\r\n) */
125 while (value_len > 0 && isspace((unsigned char)value[value_len - 1])) {
126 value_len--;
127 }
128
129 if (name_len > 0 && value_len > 0) {
131 char *name = cwist_scratch_alloc(&name_s, name_len + 1);
132 if (!name) return total;
133 memcpy(name, ptr, name_len);
134 name[name_len] = '\0';
135
137 char *val = cwist_scratch_alloc(&val_s, value_len + 1);
138 if (!val) return total;
139 memcpy(val, value, value_len);
140 val[value_len] = '\0';
141
142 cwist_http_header_node *node = cwist_alloc(sizeof(*node));
143 if (!node) return total;
144 node->key = cwist_sstring_create();
145 node->value = cwist_sstring_create();
146 if (!node->key || !node->value) {
149 cwist_free(node);
150 return total;
151 }
152 cwist_sstring_assign(node->key, name);
154 node->next = rh->headers;
155 rh->headers = node;
156 rh->header_count++;
157 }
158
159 return total;
160}
161
162/* ------------------------------------------------------------------ */
163/* Client handle */
164/* ------------------------------------------------------------------ */
165
166struct cwist_http_client {
167 CURL *curl;
168 int follow_redirects;
169 int timeout_ms;
170 char *ca_bundle;
171 int altsvc_enabled;
172 char *altsvc_db;
173};
174
187
188 cwist_http_client *client = cwist_alloc(sizeof(*client));
189 if (!client) {
191 return NULL;
192 }
193
194 client->curl = curl_easy_init();
195 if (!client->curl) {
196 cwist_free(client);
198 return NULL;
199 }
200
201 client->follow_redirects = 1;
202 client->timeout_ms = 30000;
203 client->ca_bundle = NULL;
204 client->altsvc_enabled = 0;
205 client->altsvc_db = NULL;
206
207 curl_easy_setopt(client->curl, CURLOPT_FOLLOWLOCATION, 1L);
208 curl_easy_setopt(client->curl, CURLOPT_MAXREDIRS, 10L);
209 curl_easy_setopt(client->curl, CURLOPT_TIMEOUT_MS, 30000L);
210 curl_easy_setopt(client->curl, CURLOPT_TCP_KEEPALIVE, 1L);
211 curl_easy_setopt(client->curl, CURLOPT_ACCEPT_ENCODING, "");
212 curl_easy_setopt(client->curl, CURLOPT_USERAGENT, "CWIST-HTTP-Client/1.0");
213
214 return client;
215}
216
226void cwist_http_client_destroy(cwist_http_client *client) {
227 if (!client) return;
228 if (client->curl) {
229 curl_easy_cleanup(client->curl);
230 }
231 cwist_free(client->ca_bundle);
232 cwist_free(client->altsvc_db);
233 cwist_free(client);
235}
236
243void cwist_http_client_set_follow_redirects(cwist_http_client *client, int follow) {
244 if (!client) return;
245 client->follow_redirects = follow;
246 curl_easy_setopt(client->curl, CURLOPT_FOLLOWLOCATION, follow ? 1L : 0L);
247}
248
255void cwist_http_client_set_timeout_ms(cwist_http_client *client, int timeout_ms) {
256 if (!client) return;
257 client->timeout_ms = timeout_ms;
258 curl_easy_setopt(client->curl, CURLOPT_TIMEOUT_MS, (long)timeout_ms);
259}
260
270void cwist_http_client_set_ca_bundle(cwist_http_client *client, const char *path) {
271 if (!client) return;
272 cwist_free(client->ca_bundle);
273 client->ca_bundle = path ? strdup(path) : NULL;
274 curl_easy_setopt(client->curl, CURLOPT_CAINFO, client->ca_bundle);
275}
276
287void cwist_http_client_enable_altsvc(cwist_http_client *client, int enabled) {
288 if (!client) return;
289 client->altsvc_enabled = enabled;
291 enabled ? (long)(CURLALTSVC_H1 | CURLALTSVC_H2 | CURLALTSVC_H3) : 0L);
292}
293
302void cwist_http_client_set_altsvc_db(cwist_http_client *client, const char *path) {
303 if (!client) return;
304 cwist_free(client->altsvc_db);
305 client->altsvc_db = path ? strdup(path) : NULL;
306 curl_easy_setopt(client->curl, CURLOPT_ALTSVC, client->altsvc_db);
307}
308
309/* ------------------------------------------------------------------ */
310/* Request execution */
311/* ------------------------------------------------------------------ */
312
336cwist_error_t cwist_http_client_request(cwist_http_client *client, const char *url,
338 const char *body, size_t body_len,
341 if (!client || !client->curl || !url || !out_response) {
342 err.error.err_i16 = -1;
343 return err;
344 }
345
347
348 CURL *curl = client->curl;
349
350 /* Reset handle for reuse */
351 curl_easy_reset(curl);
352
353 /* Re-apply persistent settings after reset */
354 curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, client->follow_redirects ? 1L : 0L);
355 curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, (long)client->timeout_ms);
358 curl_easy_setopt(curl, CURLOPT_USERAGENT, "CWIST-HTTP-Client/1.0");
359 if (client->ca_bundle) {
360 curl_easy_setopt(curl, CURLOPT_CAINFO, client->ca_bundle);
361 }
362 if (client->altsvc_enabled) {
365 curl_easy_setopt(curl, CURLOPT_ALTSVC, client->altsvc_db);
366 }
367
368 /* URL */
369 curl_easy_setopt(curl, CURLOPT_URL, url);
370
371 /* Method */
372 const char *method_str = cwist_http_method_to_string(method);
373 if (strcmp(method_str, "GET") == 0) {
375 } else if (strcmp(method_str, "POST") == 0) {
377 } else if (strcmp(method_str, "PUT") == 0) {
380 } else if (strcmp(method_str, "DELETE") == 0) {
382 } else if (strcmp(method_str, "PATCH") == 0) {
384 } else if (strcmp(method_str, "HEAD") == 0) {
386 } else if (strcmp(method_str, "OPTIONS") == 0) {
387 curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "OPTIONS");
388 } else {
390 }
391
392 /* Request headers */
393 struct curl_slist *curl_headers = NULL;
394 cwist_http_header_node *node = headers;
395 while (node) {
396 if (node->key && node->key->data && node->value && node->value->data) {
397 char header_line[4096];
398 int n = snprintf(header_line, sizeof(header_line), "%s: %s", node->key->data,
399 node->value->data);
400 if (n > 0 && (size_t)n < sizeof(header_line)) {
402 }
403 }
404 node = node->next;
405 }
406 if (curl_headers) {
408 }
409
410 /* Request body */
411 if (body && body_len > 0) {
414 }
415
416 /* Response accumulators */
417 response_body_t resp_body = {0};
418 response_headers_t resp_hdrs = {0};
419
420 curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
421 curl_easy_setopt(curl, CURLOPT_WRITEDATA, &resp_body);
422 curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_callback);
424
425 /* Prefer HTTP/2, allow downgrade to HTTP/1.1 */
427
428 /* Perform */
429 CURLcode res = curl_easy_perform(curl);
430
431 /* Clean up request headers */
432 if (curl_headers) {
434 }
435
436 if (res != CURLE_OK) {
437 free(resp_body.data);
439 err.error.err_i16 = -1;
440 return err;
441 }
442
443 /* Build response object */
445 if (!response) {
446 free(resp_body.data);
448 err.error.err_i16 = -1;
449 return err;
450 }
451
452 response->status_code = (cwist_http_status_t)resp_hdrs.status_code;
453 if (resp_body.data && resp_body.size > 0) {
454 cwist_sstring_assign_len(response->body, resp_body.data, resp_body.size);
455 }
456
457 /* Transfer collected headers */
458 response->headers = resp_hdrs.headers;
459
460 /* Populate status text based on code */
461 switch (response->status_code) {
462 case CWIST_HTTP_OK: cwist_sstring_assign(response->status_text, "OK"); break;
463 case CWIST_HTTP_CREATED: cwist_sstring_assign(response->status_text, "Created"); break;
465 cwist_sstring_assign(response->status_text, "No Content");
466 break;
468 cwist_sstring_assign(response->status_text, "Bad Request");
469 break;
471 cwist_sstring_assign(response->status_text, "Unauthorized");
472 break;
473 case CWIST_HTTP_FORBIDDEN: cwist_sstring_assign(response->status_text, "Forbidden"); break;
474 case CWIST_HTTP_NOT_FOUND: cwist_sstring_assign(response->status_text, "Not Found"); break;
476 cwist_sstring_assign(response->status_text, "Internal Server Error");
477 break;
479 cwist_sstring_assign(response->status_text, "Not Implemented");
480 break;
482 cwist_sstring_assign(response->status_text, "Service Unavailable");
483 break;
484 default: cwist_sstring_assign(response->status_text, ""); break;
485 }
486
487 free(resp_body.data);
489 err.error.err_i16 = 0;
490 return err;
491}
Memory allocation wrappers backed by libttak.
void cwist_free(void *ptr)
Free memory obtained via cwist_alloc/cwist_strdup/etc.
Definition alloc.c:52
void * cwist_alloc(size_t size)
Allocate zeroed memory tracked by libttak.
Definition alloc.c:17
#define CWIST_SCRATCH_DEFER
Attach to a cwist_scratch_t variable's declaration so its allocation (arena- or heap-backed,...
Definition alloc.h:190
cwist_sstring * cwist_sstring_create(void)
Create a new sstring instance.
Definition sstring.c:802
cwist_error_t cwist_sstring_assign_len(cwist_sstring *str, const char *data, size_t len)
Assign raw bytes with length.
Definition sstring.c:88
cwist_error_t cwist_sstring_assign(cwist_sstring *str, const char *data)
Assign a C string.
Definition sstring.c:504
void cwist_sstring_destroy(cwist_sstring *str)
Destroy an sstring instance.
Definition sstring.c:823
void cwist_curl_global_acquire(void)
Acquire a reference on the global libcurl instance.
Definition curl_global.c:19
void cwist_curl_global_release(void)
Release a reference on the global libcurl instance.
Definition curl_global.c:36
@ CWIST_ERR_INT16
used when checking common errcodes in Unix/Linux
Definition cwist_err.h:16
cwist_error_t make_error(cwist_errtype_t type)
Creates an error object initialized with the specified type.
Definition error.c:14
#define CWIST_HTTP_MAX_BODY_SIZE
Definition http.h:41
cwist_http_status_t
Definition http.h:27
@ CWIST_HTTP_UNAUTHORIZED
Definition http.h:32
@ CWIST_HTTP_FORBIDDEN
Definition http.h:33
@ CWIST_HTTP_INTERNAL_ERROR
Definition http.h:35
@ CWIST_HTTP_CREATED
Definition http.h:29
@ CWIST_HTTP_NOT_IMPLEMENTED
Definition http.h:36
@ CWIST_HTTP_BAD_REQUEST
Definition http.h:31
@ CWIST_HTTP_NOT_FOUND
Definition http.h:34
@ CWIST_HTTP_OK
Definition http.h:28
@ CWIST_HTTP_NO_CONTENT
Definition http.h:30
cwist_http_method_t
Definition http.h:16
void cwist_http_header_free_all(cwist_http_header_node *head)
Free all headers.
Definition http.c:2006
cwist_http_response * cwist_http_response_create(void)
Create a new HTTP response object.
Definition http.c:2291
const char * cwist_http_method_to_string(cwist_http_method_t method)
Convert method enum to string.
Definition http.c:1554
cwist_http_client * cwist_http_client_create(void)
Create a new HTTP client handle with default settings.
void cwist_http_client_set_altsvc_db(cwist_http_client *client, const char *path)
Set the filesystem path of the alt-svc cache database.
cwist_error_t cwist_http_client_request(cwist_http_client *client, const char *url, cwist_http_method_t method, cwist_http_header_node *headers, const char *body, size_t body_len, cwist_http_response **out_response)
Execute a single HTTP request.
void cwist_http_client_set_ca_bundle(cwist_http_client *client, const char *path)
Set the path to a custom CA certificate bundle for TLS verification.
void cwist_http_client_set_follow_redirects(cwist_http_client *client, int follow)
Enable or disable automatic redirect following.
void cwist_http_client_destroy(cwist_http_client *client)
Destroy a client handle and release all associated resources.
void cwist_http_client_enable_altsvc(cwist_http_client *client, int enabled)
Enable or disable HTTP alternative services (H1/H2/H3 upgrade).
void cwist_http_client_set_timeout_ms(cwist_http_client *client, int timeout_ms)
Set the total request timeout in milliseconds.
Unified HTTP/1.1 and HTTP/2 client using libcurl.
struct cwist_http_client cwist_http_client
Opaque HTTP client handle.
Definition http_client.h:28
@ CWIST_HTTP_SERVICE_UNAVAILABLE
Definition http.h:111
size_t cap
Definition otel.c:56
Integrated error structure used throughout the system.
Definition cwist_err.h:72
Linked list for headers to handle multiple headers easily.
Definition http.h:59
cwist_sstring * value
Definition http.h:61
cwist_sstring * key
Definition http.h:60
struct cwist_http_header_node * next
Definition http.h:62
HTTP Response Object. Supports standard string body or Zero-Copy pointer body.
Definition http.h:82
Backing state for cwist_scratch_alloc()/CWIST_SCRATCH_DEFER: a single block-scoped allocation that pr...
Definition alloc.h:131
char * data
Access directly only when raw handling is necessary.
Definition sstring.h:15
Unified error code and type definitions for the CWIST system layer.