Files
Eric Ireland 2f244e31f2 Bound HTTP response writes with a send timeout
Assisted-by: OpenAI Codex (GPT-5)
Reviewed-by: Eric Ireland
2026-09-11 11:41:55 +10:00

180 lines
4.5 KiB
C

/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
#ifndef _XS_HTTPD_H
#define _XS_HTTPD_H
xs_dict *xs_httpd_request(FILE *f, xs_str **payload, int *p_size);
int xs_httpd_response(FILE *f, int status, const char *status_text,
const xs_dict *headers, const xs_val *body, int b_size);
#ifndef XS_HTTPD_WRITE_TIMEOUT
#define XS_HTTPD_WRITE_TIMEOUT 10.0
#endif
#ifdef XS_IMPLEMENTATION
xs_dict *xs_httpd_request(FILE *f, xs_str **payload, int *p_size)
/* processes an httpd connection */
{
xs *q_vars = NULL;
xs *p_vars = NULL;
xs *l1;
const char *v;
char *saveptr;
xs_socket_timeout(fileno(f), 2.0, 0.0);
/* read the first line and split it */
l1 = xs_strip_i(xs_readline(f));
char *raw_path;
const char *mtd;
const char *proto;
if (!(mtd = strtok_r(l1, " ", &saveptr)) ||
!(raw_path = strtok_r(NULL, " ", &saveptr)) ||
!(proto = strtok_r(NULL, " ", &saveptr)) ||
strtok_r(NULL, " ", &saveptr))
return NULL;
if (!xs_is_string(mtd) || !xs_is_string(raw_path) || !xs_is_string(proto))
return NULL;
xs_dict *req = xs_dict_new();
req = xs_dict_append(req, "method", mtd);
req = xs_dict_append(req, "raw_path", raw_path);
req = xs_dict_append(req, "proto", proto);
{
char *q = strchr(raw_path, '?');
/* get the variables */
if (q) {
*q++ = '\0';
q_vars = xs_url_vars(q);
}
/* store the path */
req = xs_dict_append(req, "path", raw_path);
}
/* read the headers */
for (;;) {
xs *l;
l = xs_strip_i(xs_readline(f));
/* done with the header? */
if (strcmp(l, "") == 0)
break;
/* split header and content */
char *cnt = strchr(l, ':');
if (!cnt)
continue;
*cnt++ = '\0';
cnt += strspn(cnt, " \r\n\t\v\f");
l = xs_rstrip_chars_i(l, " \r\n\t\v\f");
if (!xs_is_string(cnt))
continue;
req = xs_dict_append(req, xs_tolower_i(l), cnt);
}
xs_socket_timeout(fileno(f), 5.0, 0.0);
if ((v = xs_dict_get(req, "content-length")) != NULL) {
/* if it has a payload, load it */
*p_size = atoi(v);
*payload = xs_read(f, p_size);
}
else if ((v = xs_dict_get(req, "transfer-encoding")) != NULL &&
xs_startswith(v, "chunked")) {
/* handle chunked transfer encoding */
xs_str *body = xs_str_new(NULL);
for (;;) {
xs *line = xs_strip_i(xs_readline(f));
/* parse chunk size (in hex) */
int chunk_size = strtol(line, NULL, 16);
if (chunk_size <= 0)
break;
/* read chunk data */
xs *chunk = xs_read(f, &chunk_size);
if (chunk == NULL)
break;
body = xs_append_m(body, chunk, chunk_size);
/* read trailing \r\n after chunk data */
xs *dummy = xs_readline(f);
}
*p_size = xs_size(body) - 1; /* subtract trailing null */
*payload = body;
}
v = xs_dict_get(req, "content-type");
if (*payload && v && strcmp(v, "application/x-www-form-urlencoded") == 0) {
p_vars = xs_url_vars(*payload);
}
else
if (*payload && v && xs_startswith(v, "multipart/form-data")) {
p_vars = xs_multipart_form_data(*payload, *p_size, v);
}
else
p_vars = xs_dict_new();
req = xs_dict_append(req, "q_vars", q_vars);
req = xs_dict_append(req, "p_vars", p_vars);
if (errno)
req = xs_free(req);
return req;
}
int xs_httpd_response(FILE *f, int status, const char *status_text,
const xs_dict *headers, const xs_val *body, int b_size)
/* sends an httpd response */
{
if (xs_socket_timeout(fileno(f), 0.0, XS_HTTPD_WRITE_TIMEOUT) == -1)
return -1;
if (fprintf(f, "HTTP/1.1 %d %s\r\n", status,
status_text ? status_text : "") < 0)
return -1;
const xs_str *k;
const xs_val *v;
xs_dict_foreach(headers, k, v) {
if (fprintf(f, "%s: %s\r\n", k, v) < 0)
return -1;
}
if (b_size != 0 && fprintf(f, "content-length: %d\r\n", b_size) < 0)
return -1;
if (fprintf(f, "\r\n") < 0)
return -1;
if (body != NULL && b_size != 0 && fwrite(body, b_size, 1, f) != 1)
return -1;
return fflush(f) == 0 ? 0 : -1;
}
#endif /* XS_IMPLEMENTATION */
#endif /* XS_HTTPD_H */