ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
api-server/main.c
Go to the documentation of this file.
1#include "main.h"
8#include <libplatform/filesystem.h>
11#include <libplatform/systemd.h>
13#include <ascii-chat/version.h>
14#include <ascii-chat/otel.h>
15#include <string.h>
16#include <stdlib.h>
17#include <stdio.h>
18#include <uv.h>
19
26
36
37static void api_finish(http_response_t *response, unsigned status) {
38 (void)http_response_set_status(response, status);
39 (void)http_response_finish(response);
40}
41
42static unsigned api_render_status(asciichat_error_t error) {
43 if (error == ERROR_BUFFER_FULL)
44 return 503;
45 if (error == ERROR_BUFFER_OVERFLOW)
46 return 413;
47 if (error == ERROR_INVALID_PARAM || error == ERROR_MEDIA_INIT || error == ERROR_MEDIA_DECODE)
48 return 422;
49 return 500;
50}
51
52static const unsigned char *api_find_bytes(const unsigned char *data, size_t length, const char *needle) {
53 size_t needle_length = strlen(needle);
54 if (!needle_length || needle_length > length)
55 return NULL;
56 for (size_t i = 0; i + needle_length <= length; ++i)
57 if (!memcmp(data + i, needle, needle_length))
58 return data + i;
59 return NULL;
60}
61
62static bool api_decode_query_component(const char *src, size_t length, char *dst, size_t capacity) {
63 size_t out = 0;
64 for (size_t i = 0; i < length; ++i) {
65 unsigned char c = (unsigned char)src[i];
66 if (c == '%') {
67 if (i + 2 >= length)
68 return false;
69 int hi = (src[i + 1] >= '0' && src[i + 1] <= '9') ? src[i + 1] - '0'
70 : (src[i + 1] >= 'a' && src[i + 1] <= 'f') ? src[i + 1] - 'a' + 10
71 : (src[i + 1] >= 'A' && src[i + 1] <= 'F') ? src[i + 1] - 'A' + 10
72 : -1;
73 int lo = (src[i + 2] >= '0' && src[i + 2] <= '9') ? src[i + 2] - '0'
74 : (src[i + 2] >= 'a' && src[i + 2] <= 'f') ? src[i + 2] - 'a' + 10
75 : (src[i + 2] >= 'A' && src[i + 2] <= 'F') ? src[i + 2] - 'A' + 10
76 : -1;
77 if (hi < 0 || lo < 0)
78 return false;
79 c = (unsigned char)((hi << 4) | lo);
80 i += 2;
81 } else if (c == '+')
82 c = ' ';
83 if (!c || out + 1 >= capacity)
84 return false;
85 dst[out++] = (char)c;
86 }
87 dst[out] = 0;
88 return true;
89}
90
91static void api_write_result(http_response_t *response, const char *format, const http_render_result_t *result) {
92 if (!strcmp(format, "files")) {
93 static const char boundary[] = "ascii-chat-frame";
94 (void)http_response_set_header(response, "Content-Type", "multipart/mixed; boundary=ascii-chat-frame");
95 for (size_t i = 0; i < result->frame_count; ++i) {
96 const http_render_frame_t *frame = &result->frames[i];
97 if (!frame->text)
98 continue;
99 char header[256];
100 int length = safe_snprintf(header, sizeof(header),
101 "--%s\r\nContent-Type: text/plain; charset=utf-8\r\n"
102 "Content-Disposition: attachment; filename=frame-%zu.txt\r\n\r\n",
103 boundary, i);
104 (void)http_response_write(response, header, (size_t)length);
105 (void)http_response_write(response, frame->text, frame->text_length);
106 (void)http_response_write(response, "\r\n", 2);
107 }
108 (void)http_response_write(response, "--ascii-chat-frame--\r\n", 22);
109 } else if (!strcmp(format, "json")) {
110 size_t cap = 64;
111 for (size_t frame_index = 0; frame_index < result->frame_count; ++frame_index) {
112 const http_render_frame_t *frame = &result->frames[frame_index];
113 // Every byte may expand to a six-byte JSON escape (for example, "\u001f").
114 if (cap > SIZE_MAX - 96 || frame->text_length > (SIZE_MAX - cap - 96) / 6) {
115 (void)http_response_set_status(response, 500);
116 return;
117 }
118 cap += frame->text_length * 6 + 96;
119 }
120 char *json = SAFE_MALLOC(cap, char *);
121 int written = safe_snprintf(json, cap, "{\"frames\":[");
122 if (written < 0) {
123 SAFE_FREE(json);
124 (void)http_response_set_status(response, 500);
125 return;
126 }
127 size_t out = (size_t)written;
128 for (size_t frame_index = 0; frame_index < result->frame_count; ++frame_index) {
129 const http_render_frame_t *frame = &result->frames[frame_index];
130 if (frame_index) {
131 written = safe_snprintf(json + out, cap - out, ",");
132 if (written < 0) {
133 SAFE_FREE(json);
134 (void)http_response_set_status(response, 500);
135 return;
136 }
137 out += (size_t)written;
138 }
139 written = safe_snprintf(json + out, cap - out, "{\"timestamp_ms\":%llu,\"text\":\"",
140 (unsigned long long)frame->timestamp_ms);
141 if (written < 0) {
142 SAFE_FREE(json);
143 (void)http_response_set_status(response, 500);
144 return;
145 }
146 out += (size_t)written;
147 for (size_t i = 0; i < frame->text_length && out + 8 < cap; ++i) {
148 unsigned char c = (unsigned char)frame->text[i];
149 if (c == '"' || c == '\\') {
150 json[out++] = '\\';
151 json[out++] = (char)c;
152 } else if (c == '\n') {
153 json[out++] = '\\';
154 json[out++] = 'n';
155 } else if (c < 0x20) {
156 written = safe_snprintf(json + out, cap - out, "\\u%04x", c);
157 if (written < 0) {
158 SAFE_FREE(json);
159 (void)http_response_set_status(response, 500);
160 return;
161 }
162 out += (size_t)written;
163 } else
164 json[out++] = (char)c;
165 }
166 written = safe_snprintf(json + out, cap - out, "\"}");
167 if (written < 0) {
168 SAFE_FREE(json);
169 (void)http_response_set_status(response, 500);
170 return;
171 }
172 out += (size_t)written;
173 }
174 written = safe_snprintf(json + out, cap - out, "]}");
175 if (written < 0) {
176 SAFE_FREE(json);
177 (void)http_response_set_status(response, 500);
178 return;
179 }
180 out += (size_t)written;
181 (void)http_response_set_header(response, "Content-Type", "application/json");
182 (void)http_response_write(response, json, out);
183 SAFE_FREE(json);
184 } else if (!strcmp(format, "html")) {
185 (void)http_response_set_header(response, "Content-Type", "text/html; charset=utf-8");
186 (void)http_response_write(response, "<pre>", 5);
187 for (size_t i = 0; i < result->ascii_length; ++i) {
188 const char *escaped = result->ascii[i] == '&' ? "&amp;"
189 : result->ascii[i] == '<' ? "&lt;"
190 : result->ascii[i] == '>' ? "&gt;"
191 : NULL;
192 (void)http_response_write(response, escaped ? escaped : &result->ascii[i], escaped ? strlen(escaped) : 1);
193 }
194 (void)http_response_write(response, "</pre>", 6);
195 } else {
196 (void)http_response_set_header(response, "Content-Type", "text/plain; charset=utf-8");
197 (void)http_response_write(response, result->ascii, result->ascii_length);
198 }
199}
200
201static void api_render_complete(http_renderer_job_t *job, const http_render_result_t *result, void *user_data) {
202 (void)result;
203 api_server_t *server = user_data;
204 api_render_job_t *pending = job->user_data;
205 if (pending) {
206 atomic_store_bool(&pending->completed, true);
207 }
208 if (server)
209 (void)http_server_wakeup(server->http);
210}
211
212static void api_render_error(http_renderer_job_t *job, asciichat_error_t error, void *user_data) {
213 (void)error;
214 api_render_complete(job, &job->result, user_data);
215}
216
217static void api_wakeup(void *user_data) {
218 api_server_t *server = user_data;
219 api_render_job_t **link = &server->jobs;
220 while (*link) {
221 api_render_job_t *pending = *link;
222 if (pending->renderer_job && pending->response && http_response_is_cancelled(pending->response)) {
223 (void)http_renderer_cancel(pending->renderer_job);
225 }
226 if (!pending->renderer_job || !atomic_load_bool(&pending->completed) ||
228 link = &pending->next;
229 continue;
230 }
231 *link = pending->next;
232 if (pending->response) {
233 if (pending->renderer_job->result.error == ASCIICHAT_OK) {
234 api_write_result(pending->response, pending->format, &pending->renderer_job->result);
235 api_finish(pending->response, 200);
236 } else {
237 log_warn("API render failed: %s", asciichat_error_string(pending->renderer_job->result.error));
238 api_finish(pending->response, api_render_status(pending->renderer_job->result.error));
239 }
240 }
241 pending->renderer_job->user_data = NULL;
243 platform_delete_temp_file(pending->path);
244 SAFE_FREE(pending);
245 }
246 /* Every renderer job is owned by a pending entry. Reap only after the
247 * completed entries have been unlinked and no callback can use them. */
248 http_renderer_reap(server->renderer, false);
249}
250
251static void api_discard_jobs(api_server_t *server) {
252 while (server->jobs) {
253 api_render_job_t *pending = server->jobs;
254 server->jobs = pending->next;
256 platform_delete_temp_file(pending->path);
257 SAFE_FREE(pending);
258 }
259}
260
261static void api_request(http_request_t *request, http_response_t *response, void *user_data) {
262 api_server_t *server = user_data;
263 const http_request_info_t *info = http_request_info(request);
264 ASCII_OTEL_SCOPE(HTTP);
265 ASCII_OTEL_ATTR_STRING("service.name", "ascii-chat-api");
266 ASCII_OTEL_ATTR_STRING("http.route", info->path);
267 ASCII_OTEL_ATTR_STRING("http.request.method", info->method);
268 ASCII_OTEL_ATTR_INT("http.protocol", info->protocol);
269 if (!strcmp(info->method, "GET") && !strcmp(info->path, "/health")) {
270 static const char body[] =
271 "{\"status\":\"ok\",\"service\":\"ascii-chat-api\",\"version\":\"" ASCII_CHAT_VERSION_FULL "\"}";
272 (void)http_response_set_header(response, "Content-Type", "application/json");
273 (void)http_response_write(response, body, sizeof(body) - 1);
274 api_finish(response, 200);
275 return;
276 }
277 if (strcmp(info->path, "/health") && strcmp(info->path, "/convert")) {
278 api_finish(response, 404);
279 return;
280 }
281 if (!strcmp(info->path, "/health")) {
282 api_finish(response, 405);
283 return;
284 }
285 if (strcmp(info->method, "POST")) {
286 api_finish(response, 405);
287 return;
288 }
289 const char *content_type = NULL;
290 for (size_t i = 0; i < info->header_count; ++i)
291 if (!platform_strcasecmp(info->headers[i].name, "content-type"))
292 content_type = info->headers[i].value;
293 bool multipart = content_type && !strncmp(content_type, "multipart/form-data;", 20);
294 if (!content_type ||
295 (!multipart && strncmp(content_type, "image/", 6) && strncmp(content_type, "application/octet-stream", 24))) {
296 api_finish(response, 415);
297 return;
298 }
299 size_t length = 0;
300 const unsigned char *body = http_request_body(request, &length);
301 if (!body || !length) {
302 api_finish(response, 400);
303 return;
304 }
305 if (multipart) {
306 const char *boundary = strstr(content_type, "boundary=");
307 if (!boundary || !boundary[9]) {
308 api_finish(response, 400);
309 return;
310 }
311 boundary += 9;
312 char boundary_value[256];
313 size_t boundary_len = strcspn(boundary, ";\r\n");
314 if (boundary_len >= sizeof(boundary_value)) {
315 api_finish(response, 400);
316 return;
317 }
318 memcpy(boundary_value, boundary, boundary_len);
319 boundary_value[boundary_len] = 0;
320 if (boundary_value[0] == '"') {
321 size_t n = strlen(boundary_value);
322 if (n < 2 || boundary_value[n - 1] != '"') {
323 api_finish(response, 400);
324 return;
325 }
326 memmove(boundary_value, boundary_value + 1, n - 2);
327 boundary_value[n - 2] = 0;
328 }
329 boundary = boundary_value;
330 char marker[256];
331 int marker_len = safe_snprintf(marker, sizeof(marker), "--%s", boundary);
332 if (marker_len <= 0 || (size_t)marker_len >= sizeof(marker)) {
333 api_finish(response, 400);
334 return;
335 }
336 const unsigned char *start = api_find_bytes(body, length, "\r\n\r\n");
337 if (!start || (size_t)(start - body) >= length) {
338 api_finish(response, 400);
339 return;
340 }
341 if (!api_find_bytes(body, (size_t)(start - body), "name=\"image\"")) {
342 api_finish(response, 400);
343 return;
344 }
345 start += 4;
346 const unsigned char *end = api_find_bytes(start, length - (size_t)(start - body), marker);
347 if (!end || end <= start || (size_t)(end - body) > length) {
348 api_finish(response, 400);
349 return;
350 }
351 body = start;
352 length = (size_t)(end - start);
353 while (length && (body[length - 1] == '\r' || body[length - 1] == '\n'))
354 --length;
355 if (!length) {
356 api_finish(response, 400);
357 return;
358 }
359 }
360 http_render_request_t render_request;
361 http_render_request_init(&render_request);
362 render_request.max_output_bytes = 8u * 1024u * 1024u;
363 const char *query = info->query ? info->query : "";
364 char format[16] = "text";
365 char output[16] = "files";
366 for (const char *p = query; *p;) {
367 const char *eq = strchr(p, '=');
368 const char *amp = strchr(p, '&');
369 if (!eq || (amp && eq > amp)) {
370 api_finish(response, 400);
371 return;
372 }
373 size_t key_len = (size_t)(eq - p), value_len = (size_t)((amp ? amp : p + strlen(p)) - eq - 1);
374 char key[32], value[256];
375 if (!key_len || key_len >= sizeof(key) || value_len >= sizeof(value)) {
376 api_finish(response, 400);
377 return;
378 }
379 if (!api_decode_query_component(p, key_len, key, sizeof(key)) ||
380 !api_decode_query_component(eq + 1, value_len, value, sizeof(value))) {
381 api_finish(response, 400);
382 return;
383 }
384 char *endptr = NULL;
385 if (!strcmp(key, "width") || !strcmp(key, "height")) {
386 long n = strtol(value, &endptr, 10);
387 if (!endptr || *endptr || n < 1 || n > 3840) {
388 api_finish(response, 422);
389 return;
390 }
391 if (!strcmp(key, "width"))
392 render_request.options.width = (int)n;
393 else
394 render_request.options.height = (int)n;
395 } else if (!strcmp(key, "palette")) {
396 SAFE_STRNCPY(render_request.options.palette, value, sizeof(render_request.options.palette));
397 } else if (!strcmp(key, "format")) {
398 SAFE_STRNCPY(format, value, sizeof(format));
399 if (strcmp(format, "text") && strcmp(format, "json") && strcmp(format, "html")) {
400 api_finish(response, 422);
401 return;
402 }
403 } else if (!strcmp(key, "output")) {
404 SAFE_STRNCPY(output, value, sizeof(output));
405 if (strcmp(output, "text") && strcmp(output, "files")) {
406 api_finish(response, 422);
407 return;
408 }
409 } else if (!strcmp(key, "palette-chars")) {
410 SAFE_STRNCPY(render_request.options.palette_chars, value, sizeof(render_request.options.palette_chars));
411 } else if (!strcmp(key, "color")) {
412 if (!strcmp(value, "none"))
413 render_request.options.color_mode = TERM_COLOR_NONE;
414 else if (!strcmp(value, "16"))
415 render_request.options.color_mode = TERM_COLOR_16;
416 else if (!strcmp(value, "256"))
417 render_request.options.color_mode = TERM_COLOR_256;
418 else if (!strcmp(value, "truecolor") || !strcmp(value, "24bit"))
419 render_request.options.color_mode = TERM_COLOR_TRUECOLOR;
420 else if (!strcmp(value, "auto"))
421 render_request.options.color_mode = TERM_COLOR_AUTO;
422 else {
423 api_finish(response, 422);
424 return;
425 }
426 } else if (!strcmp(key, "renderer")) {
427 if (strcmp(value, "auto") && strcmp(value, "block") && strcmp(value, "braille") && strcmp(value, "half-block")) {
428 api_finish(response, 422);
429 return;
430 }
431 SAFE_STRNCPY(render_request.options.render_theme, value, sizeof(render_request.options.render_theme));
432 } else {
433 api_finish(response, 400);
434 return;
435 }
436 p = amp ? amp + 1 : p + strlen(p);
437 }
438 ASCII_OTEL_ATTR_STRING("http.render.format", format);
439 ASCII_OTEL_ATTR_INT("http.render.width", render_request.options.width);
440 ASCII_OTEL_ATTR_INT("http.render.height", render_request.options.height);
441 char path[PLATFORM_MAX_PATH_LENGTH];
442 int fd = -1;
443 if (platform_create_temp_file(path, sizeof(path), "ascii-api", &fd) != 0 ||
444 platform_write(fd, body, length) != (ssize_t)length) {
445 if (fd >= 0)
446 platform_close(fd);
447 api_finish(response, 500);
448 return;
449 }
450 platform_close(fd);
451 render_request.source.type = HTTP_RENDER_SOURCE_FILE;
452 SAFE_STRNCPY(render_request.source.path, path, sizeof(render_request.source.path));
453 render_request.source.body_length = length;
454 render_request.output_type = !strcmp(output, "files") ? HTTP_RENDER_OUTPUT_FILES : HTTP_RENDER_OUTPUT_TEXT;
455 render_request.max_output_bytes = 8u * 1024u * 1024u;
456 SAFE_STRNCPY(render_request.source.mime_type, content_type, sizeof(render_request.source.mime_type));
457 api_render_job_t *pending = SAFE_CALLOC(1, sizeof(*pending), api_render_job_t *);
458 if (!pending) {
459 platform_delete_temp_file(path);
460 api_finish(response, 500);
461 return;
462 }
463 pending->server = server;
464 SAFE_STRNCPY(pending->format, !strcmp(output, "files") ? "files" : format, sizeof(pending->format));
465 SAFE_STRNCPY(pending->path, path, sizeof(pending->path));
466 atomic_store_bool(&pending->completed, false);
467 http_response_t *retained = NULL;
468 if (http_response_retain(response, &retained) != ASCIICHAT_OK) {
469 SAFE_FREE(pending);
470 platform_delete_temp_file(path);
471 api_finish(response, 500);
472 return;
473 }
474 pending->response = retained;
475 render_request.user_data = pending;
476 http_renderer_job_t *render_job = NULL;
477 asciichat_error_t err = http_renderer_submit(server->renderer, &render_request, pending, &render_job);
478 if (err != ASCIICHAT_OK) {
480 SAFE_FREE(pending);
481 platform_delete_temp_file(path);
482 api_finish(response, api_render_status(err));
483 return;
484 }
485 pending->renderer_job = render_job;
486 pending->next = server->jobs;
487 server->jobs = pending;
488 if (atomic_load_bool(&pending->completed))
489 (void)http_server_wakeup(server->http);
490}
491
493 http_server_t *server = NULL;
494 api_server_t api = {0};
495 quic_endpoint_t *http3_endpoint = NULL;
496 quic_endpoint_t *http3_endpoint6 = NULL;
497 const char *tls_cert = GET_OPTION(tls_cert);
498 const char *tls_key = GET_OPTION(tls_key);
499 const char *tls_password = GET_OPTION(tls_key_password);
500 if (!tls_cert || !*tls_cert)
501 tls_cert = NULL;
502 if (!tls_key || !*tls_key)
503 tls_key = NULL;
504 if (!tls_password || !*tls_password)
505 tls_password = NULL;
506 http_server_config_t config = {.bind_address = GET_OPTION(address),
507 .bind_address6 = GET_OPTION(address6),
508 .port = (uint16_t)GET_OPTION(port),
509 .enable_http1 = true,
510 .enable_http2 = true,
511 .enable_http3 = false,
512 .tls_cert_path = tls_cert,
513 .tls_key_path = tls_key,
514 .tls_key_password = tls_password,
515 .max_connections = (size_t)GET_OPTION(max_clients),
516 .max_request_body_bytes = 10u * 1024u * 1024u,
517 .max_response_body_bytes = 8u * 1024u * 1024u,
518 .on_request = api_request,
519 .on_wakeup = api_wakeup};
520 if (!config.max_connections)
521 config.max_connections = 1;
523 http_renderer_config_t renderer_config = {.max_jobs = (unsigned)config.max_connections,
524 .max_input_bytes = config.max_request_body_bytes,
525 .max_output_bytes = config.max_response_body_bytes,
526 .timeout_ms = 10000,
527 .restricted = true,
528 .on_complete = api_render_complete,
529 .on_error = api_render_error,
530 .user_data = &api};
531 err = http_renderer_init(&api.renderer, &renderer_config);
532 if (err != ASCIICHAT_OK)
533 return err;
534 config.on_request = api_request;
535 config.user_data = &api;
536 err = http_server_init(&server, &config);
537 if (err != ASCIICHAT_OK) {
538 LOG_ERRNO_IF_SET("API HTTP server initialization failed");
540 return err;
541 }
542 api.http = server;
543 if (tls_cert && tls_key) {
544 struct sockaddr_storage bind_address;
545 socklen_t bind_length = 0;
546 err = parse_numeric_socket_address(GET_OPTION(address), (uint16_t)GET_OPTION(port), &bind_address, &bind_length);
547 if (err == ASCIICHAT_OK) {
548 quic_config_t quic_config = {.alpn = "h3",
549 .certificate_file = tls_cert,
550 .private_key_file = tls_key,
551 .enable_http3 = true,
552 .http_server = server};
553 err =
554 quic_endpoint_open((const struct sockaddr *)&bind_address, bind_length, true, &quic_config, &http3_endpoint);
555 if (err == ASCIICHAT_OK && GET_OPTION(address6) && *GET_OPTION(address6)) {
556 struct sockaddr_storage bind_address6;
557 socklen_t bind_length6 = 0;
558 err = parse_numeric_socket_address(GET_OPTION(address6), (uint16_t)GET_OPTION(port), &bind_address6,
559 &bind_length6);
560 if (err == ASCIICHAT_OK)
561 err = quic_endpoint_open((const struct sockaddr *)&bind_address6, bind_length6, true, &quic_config,
562 &http3_endpoint6);
563 }
564 }
565 if (err != ASCIICHAT_OK) {
566 LOG_ERRNO_IF_SET("API HTTP/3 listener initialization failed");
567 http_server_destroy(&server);
569 return err;
570 }
571 }
572 err = http_server_start(server);
573 if (err != ASCIICHAT_OK) {
574 LOG_ERRNO_IF_SET("API HTTP server start failed");
575 if (http3_endpoint) {
576 (void)quic_endpoint_stop(http3_endpoint);
577 (void)quic_endpoint_destroy(&http3_endpoint);
578 }
579 if (http3_endpoint6) {
580 (void)quic_endpoint_stop(http3_endpoint6);
581 (void)quic_endpoint_destroy(&http3_endpoint6);
582 }
583 http_server_destroy(&server);
585 return err;
586 }
587 platform_systemd_notify_status("API HTTP listener started");
588 platform_systemd_notify_ready();
590 uv_sleep(16);
591 http_server_stop(server);
592 if (http3_endpoint) {
593 (void)quic_endpoint_stop(http3_endpoint);
594 (void)quic_endpoint_destroy(&http3_endpoint);
595 }
596 if (http3_endpoint6) {
597 (void)quic_endpoint_stop(http3_endpoint6);
598 (void)quic_endpoint_destroy(&http3_endpoint6);
599 }
601 api_discard_jobs(&api);
602 http_server_destroy(&server);
603 return ASCIICHAT_OK;
604}
struct api_render_job api_render_job_t
int api_server_main(void)
Application-level callbacks for library code.
#define APP_CALLBACK_BOOL(callback_name)
#define LOG_ERRNO_IF_SET(message)
asciichat_error_t quic_endpoint_open(const struct sockaddr *address, socklen_t length, bool listener, const quic_config_t *config, quic_endpoint_t **out)
Definition endpoint.c:131
asciichat_error_t quic_endpoint_stop(quic_endpoint_t *endpoint)
Definition endpoint.c:308
asciichat_error_t quic_endpoint_destroy(quic_endpoint_t **endpoint)
Definition endpoint.c:353
unsigned short uint16_t
Definition common.h:61
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:423
#define SAFE_FREE(ptr)
Definition common.h:377
#define SAFE_MALLOC(size, cast)
Definition common.h:265
#define SAFE_CALLOC(count, size, cast)
Definition common.h:275
#define PLATFORM_MAX_PATH_LENGTH
Definition common.h:95
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ ERROR_BUFFER_FULL
@ ASCIICHAT_OK
Definition error_codes.h:46
@ ERROR_MEDIA_DECODE
Definition error_codes.h:67
@ ERROR_MEDIA_INIT
Definition error_codes.h:65
@ ERROR_INVALID_PARAM
@ ERROR_BUFFER_OVERFLOW
#define log_warn(...)
Log a WARN message.
Definition log/log.h:621
#define GET_OPTION(field)
Safely get a specific option field (lock-free read).
Definition options.h:1336
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Platform-safe snprintf wrapper.
ssize_t platform_write(int fd, const void *buf, size_t count)
Platform-safe write function.
asciichat_error_t http_renderer_submit(http_renderer_t *renderer, const http_render_request_t *request, void *user_data, http_renderer_job_t **out_job)
void http_render_request_init(http_render_request_t *request)
void http_renderer_reap(http_renderer_t *renderer, bool all)
asciichat_error_t http_renderer_cancel(http_renderer_job_t *job)
asciichat_error_t http_renderer_init(http_renderer_t **out, const http_renderer_config_t *config)
bool http_renderer_job_is_done(const http_renderer_job_t *job)
void http_renderer_destroy(http_renderer_t **renderer)
struct http_renderer_job http_renderer_job_t
@ HTTP_RENDER_SOURCE_FILE
@ HTTP_RENDER_OUTPUT_FILES
@ HTTP_RENDER_OUTPUT_TEXT
struct http_renderer http_renderer_t
struct quic_endpoint quic_endpoint_t
Backend-neutral HTTP request/response contract.
struct http_request http_request_t
struct http_request_info http_request_info_t
struct http_response http_response_t
asciichat_error_t parse_numeric_socket_address(const char *host, uint16_t port, struct sockaddr_storage *address, socklen_t *length)
Definition ip.c:154
🌍 IP Address Parsing and Formatting Utilities
const void * http_request_body(const http_request_t *r, size_t *n)
asciichat_error_t http_server_init(http_server_t **out, const http_server_config_t *cfg)
asciichat_error_t http_response_set_status(http_response_t *r, unsigned s)
asciichat_error_t http_server_wakeup(http_server_t *s)
void http_server_stop(http_server_t *s)
void http_server_destroy(http_server_t **p)
asciichat_error_t http_response_set_header(http_response_t *r, const char *n, const char *v)
asciichat_error_t http_response_retain(http_response_t *r, http_response_t **retained)
bool http_response_is_cancelled(const http_response_t *r)
asciichat_error_t http_server_start(http_server_t *s)
asciichat_error_t http_response_write(http_response_t *r, const void *d, size_t n)
const http_request_info_t * http_request_info(const http_request_t *r)
void http_response_release(http_response_t **response)
asciichat_error_t http_response_finish(http_response_t *r)
bool should_exit(void)
Definition main.c:249
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
#define ASCII_OTEL_SCOPE(id)
Definition otel.h:72
#define ASCII_OTEL_ATTR_INT(key, value)
Definition otel.h:67
#define ASCII_OTEL_ATTR_STRING(key, value)
Definition otel.h:66
static bool size_t gotel_query_t * out
Definition queries.h:8
🔤 String Manipulation and Shell Escaping Utilities
http_renderer_job_t * renderer_job
http_response_t * response
char path[PLATFORM_MAX_PATH_LENGTH]
api_render_job_t * next
api_server_t * server
http_server_t * http
http_renderer_t * renderer
api_render_job_t * jobs
char render_theme[OPTIONS_BUFF_SIZE]
char palette_chars[OPTIONS_BUFF_SIZE]
char palette[OPTIONS_BUFF_SIZE]
http_render_source_t source
http_render_output_type_t output_type
http_render_options_t options
http_render_frame_t * frames
asciichat_error_t error
char path[PLATFORM_MAX_PATH_LENGTH]
http_render_source_type_t type
http_render_result_t result
⏱️ High-precision timing utilities using sokol_time.h and uthash
struct http_server http_server_t
Definition tls.h:8