ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
http.c
Go to the documentation of this file.
1
2/* Storage capacities in elements; text buffers include the terminating NUL. */
3#define MCP_SERVER_HTTP_ORIGIN_BUFFER_SIZE 2048
4#define MCP_SERVER_HTTP_TYPE_BUFFER_SIZE 128
5#define MCP_SERVER_HTTP_ACCEPT_BUFFER_SIZE 512
6#define MCP_SERVER_HTTP_VERSION_BUFFER_SIZE 64
7
8#include "mcp.h"
16#include <sodium.h>
17#include <string.h>
18#include <stdlib.h>
19#include <time.h>
20#include <uv.h>
21#include <sys/stat.h>
22
23static const char *shared_header(const http_request_info_t *info, const char *name) {
24 for (size_t i = 0; i < info->header_count; ++i)
25 if (!platform_strcasecmp(info->headers[i].name, name))
26 return info->headers[i].value;
27 return NULL;
28}
29
31 if (s && s->shared_http)
33}
34
35static void shared_http_finish(http_response_t *response, unsigned status) {
36 (void)http_response_set_status(response, status);
37 ASCII_OTEL_SCOPE(MCP_HTTP_RESPONSE);
38 ASCII_OTEL_ATTR_INT("http.response.status_code", status);
39 ASCII_OTEL_ATTR_INT("http.response.body.size", (int64_t)http_response_body_length(response));
41 (void)http_response_finish(response);
42}
43
44static void shared_http_wakeup(void *user_data) {
45 mcp_server_t *s = user_data;
46 for (unsigned i = 0; i < s->policy.max_jobs; ++i) {
47 mcp_job_t *j = &s->jobs[i];
49 continue;
50 http_response_t *response = j->connection;
51 if (http_response_is_cancelled(response)) {
52 j->connection = NULL;
53 http_response_release(&response);
54 continue;
55 }
56 if (s->policy.restricted && mcp_download_finalize(&s->policy, j->response, (int64_t)time(NULL)) != ASCIICHAT_OK) {
57 yyjson_mut_val *root = yyjson_mut_doc_get_root(j->response);
58 yyjson_mut_obj_remove_str(root, "result");
59 yyjson_mut_val *error = yyjson_mut_obj(j->response);
60 yyjson_mut_obj_add_int(j->response, error, "code", -32603);
61 yyjson_mut_obj_add_str(j->response, error, "message", "Cannot publish download");
62 yyjson_mut_obj_add_val(j->response, root, "error", error);
63 }
64 size_t length = 0;
65 char *body = yyjson_mut_write(j->response, 0, &length);
66 if (body) {
67 (void)http_response_set_status(response, 200);
68 (void)http_response_write(response, body, length);
69 shared_http_finish(response, 200);
70 free(body);
71 } else {
72 /* Always terminate the retained transport before releasing its slot. */
73 shared_http_finish(response, 500);
74 }
75 j->connection = NULL;
76 http_response_release(&response);
77 }
78 mcp_jobs_reap(s, false);
79}
80
81static void shared_http_request(http_request_t *request, http_response_t *response, void *user_data) {
82 mcp_server_t *s = user_data;
83 const http_request_info_t *info = http_request_info(request);
84 const char *origin = shared_header(info, "origin");
85 if (origin && (!s->policy.origin || strcmp(origin, s->policy.origin))) {
86 shared_http_finish(response, 403);
87 return;
88 }
89 if (s->policy.restricted && !strncmp(info->path, "/downloads/", sizeof("/downloads/") - 1)) {
90 if (strcmp(info->method, "GET")) {
91 shared_http_finish(response, 405);
92 return;
93 }
94 const char *name = info->path + sizeof("/downloads/") - 1;
95 int64_t expires = 0;
96 char path[PLATFORM_MAX_PATH_LENGTH];
97 int n = safe_snprintf(path, sizeof(path), "%s/%s", s->policy.downloads_dir, name);
98 if (n < 0 || (size_t)n >= sizeof(path) || !mcp_download_name(name, &expires) || expires <= (int64_t)time(NULL)) {
99 shared_http_finish(response, 404);
100 return;
101 }
102 uv_fs_t fs = {0};
103 int rc = uv_fs_open(NULL, &fs, path, UV_FS_O_RDONLY | UV_FS_O_NOFOLLOW, 0, NULL);
104 uv_fs_req_cleanup(&fs);
105 if (rc < 0) {
106 shared_http_finish(response, 404);
107 return;
108 }
109 uv_file file = rc;
110 rc = uv_fs_fstat(NULL, &fs, file, NULL);
111 size_t size = rc == 0 && fs.statbuf.st_size >= 0 ? (size_t)fs.statbuf.st_size : 0;
112 uv_fs_req_cleanup(&fs);
113 if (!size || size > MCP_MAX_DOWNLOAD) {
114 uv_fs_close(NULL, &fs, file, NULL);
115 uv_fs_req_cleanup(&fs);
116 shared_http_finish(response, size > MCP_MAX_DOWNLOAD ? 413 : 404);
117 return;
118 }
119 char *contents = SAFE_MALLOC(size, char *);
120 uv_buf_t buffer = uv_buf_init(contents, (unsigned)size);
121 rc = contents ? uv_fs_read(NULL, &fs, file, &buffer, 1, 0, NULL) : -1;
122 uv_fs_req_cleanup(&fs);
123 uv_fs_close(NULL, &fs, file, NULL);
124 uv_fs_req_cleanup(&fs);
125 if (rc != (int)size || http_response_write(response, contents, size) != ASCIICHAT_OK) {
126 SAFE_FREE(contents);
127 shared_http_finish(response, 500);
128 return;
129 }
130 SAFE_FREE(contents);
131 const char *extension = strrchr(name, '.');
132 if (http_response_set_header(response, "Content-Type", mcp_output_mime(extension ? extension + 1 : "")) !=
133 ASCIICHAT_OK ||
134 http_response_set_header(response, "Cache-Control", "no-store") != ASCIICHAT_OK) {
135 shared_http_finish(response, 500);
136 return;
137 }
138 shared_http_finish(response, 200);
139 return;
140 }
141 if (strcmp(info->path, "/mcp")) {
142 shared_http_finish(response, 404);
143 return;
144 }
145 if (strcmp(info->method, "POST")) {
146 shared_http_finish(response, 405);
147 return;
148 }
149 const char *type = shared_header(info, "content-type");
150 if (!type || strncmp(type, "application/json", sizeof("application/json") - 1)) {
151 shared_http_finish(response, 415);
152 return;
153 }
154 const char *accept = shared_header(info, "accept");
155 if (!accept || !strstr(accept, "application/json") || !strstr(accept, "text/event-stream")) {
156 shared_http_finish(response, 406);
157 return;
158 }
159 const char *version = shared_header(info, "mcp-protocol-version");
160 if (version && strcmp(version, "2025-11-25") && strcmp(version, "2025-06-18") && strcmp(version, "2025-03-26")) {
161 shared_http_finish(response, 400);
162 return;
163 }
164 size_t length = 0;
165 const char *body = http_request_body(request, &length);
166 yyjson_doc *doc = body ? yyjson_read(body, length, 0) : NULL;
167 if (!doc) {
168 yyjson_mut_doc *error = mcp_error(NULL, -32700, "Parse error");
169 char *encoded = yyjson_mut_write(error, 0, &length);
170 yyjson_mut_doc_free(error);
171 if (encoded) {
172 (void)http_response_write(response, encoded, length);
173 free(encoded);
174 }
175 shared_http_finish(response, 400);
176 return;
177 }
178 char session_id[MCP_SESSION_ID_LENGTH + 1] = {0};
179 const char *session = shared_header(info, "mcp-session-id");
180 if (session) {
181 if (strlen(session) != MCP_SESSION_ID_LENGTH) {
182 yyjson_doc_free(doc);
183 shared_http_finish(response, 400);
184 return;
185 }
186 for (size_t i = 0; i < MCP_SESSION_ID_LENGTH; ++i) {
187 if (!((session[i] >= '0' && session[i] <= '9') || (session[i] >= 'a' && session[i] <= 'f'))) {
188 yyjson_doc_free(doc);
189 shared_http_finish(response, 400);
190 return;
191 }
192 }
193 SAFE_STRNCPY(session_id, session, sizeof(session_id));
194 }
195 bool issue_session = false;
196 yyjson_val *root = yyjson_doc_get_root(doc), *method = mcp_get(root, "method");
197 if (yyjson_is_str(method) && yyjson_equals_str(method, "initialize") && mcp_get(root, "id")) {
198 unsigned char random[16];
199 randombytes_buf(random, sizeof(random));
200 sodium_bin2hex(session_id, sizeof(session_id), random, sizeof(random));
201 issue_session = true;
202 }
203 bool pending = false;
204 yyjson_mut_doc *reply = mcp_dispatch(s, doc, response, session_id, &pending);
205 yyjson_doc_free(doc);
206 if (pending) {
207 http_response_t *retained = NULL;
208 if (http_response_retain(response, &retained) != ASCIICHAT_OK) {
209 shared_http_finish(response, 503);
210 return;
211 }
212 for (unsigned i = 0; i < s->policy.max_jobs; ++i)
213 if (s->jobs[i].renderer_job.started && s->jobs[i].connection == response)
214 s->jobs[i].connection = retained;
215 return;
216 }
217 size_t encoded_length = 0;
218 bool has_reply = reply != NULL;
219 char *encoded = reply ? yyjson_mut_write(reply, 0, &encoded_length) : NULL;
220 if (reply)
221 yyjson_mut_doc_free(reply);
222 if (encoded) {
223 (void)http_response_write(response, encoded, encoded_length);
224 free(encoded);
225 }
226 if (issue_session && http_response_set_header(response, "MCP-Session-Id", session_id) != ASCIICHAT_OK) {
227 shared_http_finish(response, 500);
228 return;
229 }
230 shared_http_finish(response, has_reply ? (encoded ? 200 : 500) : 202);
231}
232
234 http_server_config_t config = {.bind_address = s->policy.address,
235 .bind_address6 = s->policy.address6,
236 .port = (uint16_t)s->policy.port,
237 .enable_http1 = true,
238 .enable_http2 = true,
239 .tls_cert_path = s->policy.tls_cert,
240 .tls_key_path = s->policy.tls_key,
241 .tls_key_password = s->policy.tls_password,
242 .max_connections = s->policy.max_jobs * 4U,
243 .max_request_body_bytes = MCP_MAX_INPUT,
244 .max_response_body_bytes = MCP_MAX_DOWNLOAD,
245 .on_request = shared_http_request,
246 .on_wakeup = shared_http_wakeup,
247 .user_data = s};
248 if (!config.max_connections)
249 config.max_connections = 1;
250 asciichat_error_t result = http_server_init(&s->shared_http, &config);
251 if (result != ASCIICHAT_OK)
252 return result;
253 result = http_server_start(s->shared_http);
254 if (result != ASCIICHAT_OK) {
256 return result;
257 }
258 nat_upnp_context_t *mapping = NULL;
259 if (GET_OPTION(enable_upnp)) {
260 asciichat_error_t err = nat_upnp_open((uint16_t)s->policy.port, "ascii-chat MCP", &mapping);
261 if (err != ASCIICHAT_OK)
262 LOG_ERRNO_IF_SET("MCP port forwarding unavailable");
263 }
264 while (!atomic_load_bool(&s->stopping) && !APP_CALLBACK_BOOL(should_exit)) {
265 uv_sleep(16);
266 mcp_jobs_reap(s, false);
267 if (mapping && time_get_ns() >= mapping->refresh_at_ns) {
268 asciichat_error_t err = nat_upnp_refresh(mapping);
269 if (err != ASCIICHAT_OK)
270 LOG_ERRNO_IF_SET("MCP mapping renewal failed");
271 }
272 }
273 for (unsigned i = 0; i < s->policy.max_jobs; ++i)
274 if (s->jobs[i].renderer_job.started)
278 nat_upnp_close(&mapping);
279 mcp_jobs_reap(s, true);
280 return ASCIICHAT_OK;
281}
Application-level callbacks for library code.
#define APP_CALLBACK_BOOL(callback_name)
#define LOG_ERRNO_IF_SET(message)
asciichat_error_t mcp_download_finalize(const mcp_policy_t *policy, yyjson_mut_doc *doc, int64_t now)
Definition downloads.c:74
bool mcp_download_name(const char *name, int64_t *expires)
Definition downloads.c:22
const char * mcp_output_mime(const char *extension)
Definition downloads.c:10
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 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
@ ASCIICHAT_OK
Definition error_codes.h:46
#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.
void mcp_http_wake(mcp_server_t *s)
Definition http.c:30
asciichat_error_t mcp_http_run(mcp_server_t *s)
Definition http.c:233
bool http_renderer_job_is_done(const http_renderer_job_t *job)
void http_renderer_job_cancel(http_renderer_job_t *job)
struct http_request http_request_t
struct http_request_info http_request_info_t
struct http_response http_response_t
@ STATS_COUNTER_MCP_HTTP_RESPONSES
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)
size_t http_response_body_length(const http_response_t *r)
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)
void stats_counter_add(stats_scope_t *scope, stats_counter_id_t id, uint64_t amount)
bool should_exit(void)
Definition main.c:249
#define MCP_MAX_INPUT
Definition mcp.h:29
yyjson_mut_doc * mcp_error(yyjson_val *id, int code, const char *message)
yyjson_mut_doc * mcp_dispatch(mcp_server_t *server, yyjson_doc *message, void *connection, const char *session_id, bool *pending)
struct mcp_server mcp_server_t
The useful part of mcp job is in server, message, response, request: together they describe what is p...
struct mcp_job mcp_job_t
When code needs to work with mcp policy, it keeps hosted, restricted, address, address6 here instead ...
#define MCP_MAX_DOWNLOAD
Definition mcp.h:31
#define MCP_SESSION_ID_LENGTH
Definition mcp.h:32
void mcp_jobs_reap(mcp_server_t *server, bool all)
⚙️ 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
stats_scope_t * stats_runtime_scope(void)
Definition runtime.c:80
uint8_t session_id[SESSION_ID_SIZE]
📊 Status screen display and management for server/discovery service modes
🔤 String Manipulation and Shell Escaping Utilities
void * connection
Definition mcp.h:68
yyjson_mut_doc * response
Definition mcp.h:63
http_renderer_job_t renderer_job
Definition mcp.h:67
const char * tls_key
Definition mcp.h:40
const char * tls_password
Definition mcp.h:40
const char * address
Definition mcp.h:38
unsigned max_jobs
Definition mcp.h:41
const char * tls_cert
Definition mcp.h:40
char downloads_dir[PLATFORM_MAX_PATH_LENGTH]
Definition mcp.h:44
const char * address6
Definition mcp.h:39
const char * origin
Definition mcp.h:42
int port
Definition mcp.h:43
bool restricted
Definition mcp.h:37
http_server_t * shared_http
Definition mcp.h:84
atomic_t stopping
Definition mcp.h:85
mcp_job_t * jobs
Definition mcp.h:82
mcp_policy_t policy
Definition mcp.h:81
uint64_t refresh_at_ns
Next renewal or retry deadline.
Definition upnp.h:44
⏱️ High-precision timing utilities using sokol_time.h and uthash
asciichat_error_t nat_upnp_refresh(nat_upnp_context_t *ctx)
Refresh port mapping (e.g., for long-running servers).
Definition upnp.c:316
void nat_upnp_close(nat_upnp_context_t **ctx)
Close port mapping and clean up.
Definition upnp.c:251
asciichat_error_t nat_upnp_open(uint16_t internal_port, const char *description, nat_upnp_context_t **ctx)
Discover and open port via UPnP.
Definition upnp.c:203
UPnP/NAT-PMP port mapping for TCP and UDP connectivity.
struct nat_upnp_context nat_upnp_context_t
Handle to UPnP context.