File syn_httpd.c¶
File List > net > syn_httpd.c
Go to the documentation of this file
#if __has_include("syn_config.h")
#include "syn_config.h"
#endif
#if !defined(SYN_USE_HTTPD) || SYN_USE_HTTPD
#include "../port/syn_port_system.h"
#include "../util/syn_assert.h"
#include "syn_httpd.h"
#include <string.h>
/* ── Constants ─────────────────────────────────────────────────────────── */
#define HTTPD_CLIENT_TIMEOUT_MS 5000
/* ── Internal helpers ──────────────────────────────────────────────────── */
#include "../util/syn_fmt.h"
static inline bool prefix_icase(const char *str, const char *prefix)
{
return syn_str_prefix_icase(str, prefix);
}
static bool sock_write(SYN_Socket sock, const char *str)
{
size_t len = strlen(str);
return syn_port_sock_send_all(sock, str, len) == (int)len;
}
static SYN_HttpMethod parse_method(const char *str, size_t len)
{
if (len == 3 && memcmp(str, "GET", 3) == 0)
return SYN_HTTP_GET;
if (len == 4 && memcmp(str, "POST", 4) == 0)
return SYN_HTTP_POST;
if (len == 3 && memcmp(str, "PUT", 3) == 0)
return SYN_HTTP_PUT;
if (len == 6 && memcmp(str, "DELETE", 6) == 0)
return SYN_HTTP_DELETE;
return SYN_HTTP_GET; /* default fallback */
}
static inline uint32_t parse_uint(const char *s)
{
return syn_fmt_parse_uint(s);
}
static int parse_headers_from_buf(SYN_Socket sock, SYN_HttpdRequest *req, uint8_t *buf,
size_t total)
{
memset(req, 0, sizeof(*req));
req->client_sock = sock;
buf[total] = '\0';
char *end_of_headers = strstr((const char *)buf, "\r\n\r\n");
if (end_of_headers == NULL)
return -1;
/* Parse request line: "GET /path?query HTTP/1.1\r\n" */
char *line = (char *)buf;
char *sp1 = strchr(line, ' ');
if (sp1 == NULL)
return -1;
req->method = parse_method(line, (size_t)(sp1 - line));
char *path_start = sp1 + 1;
char *sp2 = strchr(path_start, ' ');
if (sp2 == NULL)
return -1;
*sp2 = '\0'; /* null-terminate the path+query */
/* Split path and query */
char *qmark = strchr(path_start, '?');
if (qmark != NULL) {
*qmark = '\0';
req->query = qmark + 1;
}
req->path = path_start;
/* Parse headers */
char *hdr_start = strstr(sp2 + 1, "\r\n");
if (hdr_start) {
hdr_start += 2;
req->headers = hdr_start;
}
while (hdr_start && *hdr_start != '\r' && *hdr_start != '\n') {
char *next_line = strstr(hdr_start, "\r\n");
if (prefix_icase(hdr_start, "content-length:")) {
const char *val = hdr_start + 15;
while (*val == ' ')
val++;
req->content_length = parse_uint(val);
} else if (prefix_icase(hdr_start, "content-type:")) {
/* cppcheck-suppress constVariablePointer */
char *val = hdr_start + 13;
while (*val == ' ')
val++;
req->content_type = val;
/* Null-terminate Content-Type value at end of line */
if (next_line)
*next_line = '\0';
}
if (!next_line)
break;
hdr_start = next_line + 2;
}
/* Calculate buffered body */
if (end_of_headers) {
size_t header_len = (size_t)(end_of_headers + 4 - (char *)buf);
if (total > header_len) {
req->body_buffered_offset = header_len;
req->body_buffered_len = total - header_len;
}
}
return 0;
}
static const SYN_HttpdRoute *match_route(const SYN_Httpd *srv, const SYN_HttpdRequest *req)
{
for (size_t i = 0; i < srv->route_count; i++) {
const SYN_HttpdRoute *r = &srv->routes[i];
if (r->method != req->method)
continue;
size_t plen = strlen(r->path);
if (plen > 0 && r->path[plen - 1] == '*') {
/* Prefix match: "/api/" with wildcard */
if (strncmp(req->path, r->path, plen - 1) == 0) {
return r;
}
} else {
/* Exact match */
if (strcmp(req->path, r->path) == 0) {
return r;
}
}
}
return NULL;
}
static void send_error(SYN_Socket sock, int code, const char *reason)
{
sock_write(sock, "HTTP/1.1 ");
/* Simple int-to-string for status code */
char code_str[4];
code_str[0] = (char)('0' + (code / 100));
code_str[1] = (char)('0' + ((code / 10) % 10));
code_str[2] = (char)('0' + (code % 10));
code_str[3] = '\0';
sock_write(sock, code_str);
sock_write(sock, " ");
sock_write(sock, reason);
sock_write(sock, "\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
}
static void drop_client(SYN_Httpd *srv)
{
if (srv == NULL)
return;
if (srv->client != SYN_SOCKET_INVALID) {
syn_port_sock_close(srv->client);
srv->client = SYN_SOCKET_INVALID;
}
srv->state = SYN_HTTPD_IDLE;
srv->rx_total = 0;
}
/* ── Server API ────────────────────────────────────────────────────────── */
SYN_Status syn_httpd_init(SYN_Httpd *srv, uint16_t port, const SYN_HttpdRoute *routes,
size_t route_count, uint8_t *work_buf, size_t work_buf_size)
{
SYN_ASSERT(srv != NULL);
SYN_ASSERT(routes != NULL || route_count == 0);
SYN_ASSERT(work_buf != NULL);
SYN_ASSERT(work_buf_size >= 256);
memset(srv, 0, sizeof(*srv));
srv->routes = routes;
srv->route_count = route_count;
srv->work_buf = work_buf;
srv->work_buf_size = work_buf_size;
srv->port = port;
srv->client = SYN_SOCKET_INVALID;
srv->state = SYN_HTTPD_IDLE;
srv->listener = syn_port_sock_listen(port, 2);
if (srv->listener == SYN_SOCKET_INVALID) {
return SYN_ERROR;
}
srv->running = true;
return SYN_OK;
}
static void dispatch_request(SYN_Httpd *srv)
{
/* Parse request from buffered data */
SYN_HttpdRequest req;
if (parse_headers_from_buf(srv->client, &req, srv->work_buf, srv->rx_total) != 0) {
send_error(srv->client, 400, "Bad Request");
drop_client(srv);
return;
}
/* Match route */
const SYN_HttpdRoute *route = match_route(srv, &req);
if (route == NULL) {
send_error(srv->client, 404, "Not Found");
drop_client(srv);
return;
}
/* Dispatch to handler */
SYN_HttpdResponse resp;
resp.sock = srv->client;
resp.buf = srv->work_buf;
resp.buf_size = srv->work_buf_size;
resp.headers_sent = false;
resp.upgraded = false;
route->handler(&req, &resp, route->ctx);
/* If handler didn't send anything, send 204 */
if (!resp.headers_sent && !resp.upgraded) {
send_error(srv->client, 204, "No Content");
}
if (!resp.upgraded) {
drop_client(srv);
} else {
/* Upgraded connection (e.g. WebSocket) — don't close, just reset state */
srv->client = SYN_SOCKET_INVALID;
srv->state = SYN_HTTPD_IDLE;
srv->rx_total = 0;
}
}
SYN_Status syn_httpd_step(SYN_Httpd *srv)
{
SYN_ASSERT(srv != NULL);
if (!srv->running)
return SYN_ERROR;
switch (srv->state) {
/* ── IDLE: poll for new client (non-blocking) ──────────────────── */
case SYN_HTTPD_IDLE: {
SYN_Socket client = syn_port_sock_accept(srv->listener, 0);
if (client == SYN_SOCKET_INVALID) {
return SYN_TIMEOUT; /* No client — caller should yield */
}
/* Client connected — start reading headers */
srv->client = client;
srv->rx_total = 0;
srv->recv_deadline = syn_port_get_tick_ms() + HTTPD_CLIENT_TIMEOUT_MS;
srv->state = SYN_HTTPD_READING_HEADERS;
/* Fall through — attempt first recv in the same tick */
}
/* fallthrough */
/* ── READING_HEADERS: non-blocking recv, accumulate in work_buf ─ */
case SYN_HTTPD_READING_HEADERS: {
/* Check deadline — drop stale connections without blocking */
if ((int32_t)(syn_port_get_tick_ms() - srv->recv_deadline) >= 0) {
send_error(srv->client, 408, "Request Timeout");
drop_client(srv);
return SYN_TIMEOUT;
}
/* Non-blocking recv (timeout=0) */
size_t space = srv->work_buf_size - 1 - srv->rx_total;
if (space == 0) {
/* Buffer full without finding \r\n\r\n — reject */
send_error(srv->client, 413, "Request Too Large");
drop_client(srv);
return SYN_ERROR;
}
int n = syn_port_sock_recv(srv->client, srv->work_buf + srv->rx_total, space, 0);
if (n < 0) {
/* No data available this tick — yield and retry next tick */
return SYN_TIMEOUT;
}
if (n == 0) {
/* Connection closed before headers were complete */
send_error(srv->client, 400, "Bad Request");
drop_client(srv);
return SYN_ERROR;
}
srv->rx_total += (size_t)n;
srv->work_buf[srv->rx_total] = '\0';
/* Check for complete headers */
if (strstr((const char *)srv->work_buf, "\r\n\r\n") != NULL) {
srv->state = SYN_HTTPD_DISPATCHING;
/* Fall through to dispatch immediately */
dispatch_request(srv);
return SYN_OK;
}
/* Headers incomplete — yield, try again next tick */
return SYN_TIMEOUT;
}
/* ── DISPATCHING: parse + route + respond ──────────────────────── */
case SYN_HTTPD_DISPATCHING:
dispatch_request(srv);
return SYN_OK;
default:
srv->state = SYN_HTTPD_IDLE;
return SYN_ERROR;
}
}
void syn_httpd_stop(SYN_Httpd *srv)
{
SYN_ASSERT(srv != NULL);
/* Drop any in-progress client */
drop_client(srv);
if (srv->listener != SYN_SOCKET_INVALID) {
syn_port_sock_close(srv->listener);
srv->listener = SYN_SOCKET_INVALID;
}
srv->running = false;
}
/* ── Response helpers ──────────────────────────────────────────────────── */
void syn_httpd_status(const SYN_HttpdResponse *resp, int code, const char *reason)
{
if (resp == NULL || reason == NULL) {
return;
}
SYN_ASSERT(resp != NULL);
SYN_ASSERT(!resp->headers_sent);
char code_str[4];
code_str[0] = (char)('0' + (code / 100));
code_str[1] = (char)('0' + ((code / 10) % 10));
code_str[2] = (char)('0' + (code % 10));
code_str[3] = '\0';
sock_write(resp->sock, "HTTP/1.1 ");
sock_write(resp->sock, code_str);
sock_write(resp->sock, " ");
sock_write(resp->sock, reason);
sock_write(resp->sock, "\r\n");
sock_write(resp->sock, "Connection: close\r\n");
}
void syn_httpd_header(const SYN_HttpdResponse *resp, const char *name, const char *value)
{
if (resp == NULL || name == NULL || value == NULL) {
return;
}
SYN_ASSERT(resp != NULL);
SYN_ASSERT(!resp->headers_sent);
sock_write(resp->sock, name);
sock_write(resp->sock, ": ");
sock_write(resp->sock, value);
sock_write(resp->sock, "\r\n");
}
static void finalize_headers(SYN_HttpdResponse *resp)
{
if (!resp)
return;
if (!resp->headers_sent) {
sock_write(resp->sock, "\r\n");
resp->headers_sent = true;
}
}
void syn_httpd_body(SYN_HttpdResponse *resp, const void *data, size_t len)
{
if (resp == NULL) {
return;
}
SYN_ASSERT(resp != NULL);
finalize_headers(resp);
if (data != NULL && len > 0) {
syn_port_sock_send_all(resp->sock, data, len);
}
}
void syn_httpd_body_str(SYN_HttpdResponse *resp, const char *str)
{
SYN_ASSERT(resp != NULL);
SYN_ASSERT(str != NULL);
syn_httpd_body(resp, str, strlen(str));
}
int syn_httpd_read_body(const SYN_HttpdRequest *req, const SYN_HttpdResponse *resp, void *buf,
size_t max_len)
{
SYN_ASSERT(req != NULL);
SYN_ASSERT(resp != NULL);
if (req->content_length == 0)
return 0;
SYN_HttpdRequest *rw = (SYN_HttpdRequest *)(uintptr_t)req;
size_t remaining = req->content_length - rw->body_consumed;
if (remaining == 0)
return 0;
size_t to_read = max_len;
if (to_read > remaining)
to_read = remaining;
if (rw->body_buffered_len > 0) {
size_t consume = rw->body_buffered_len;
if (consume > to_read)
consume = to_read;
memcpy(buf, resp->buf + rw->body_buffered_offset, consume);
rw->body_buffered_offset += consume;
rw->body_buffered_len -= consume;
rw->body_consumed += consume;
return (int)consume;
}
/* Non-blocking read — timeout=0, never stalls the scheduler */
int n = syn_port_sock_recv(resp->sock, buf, to_read, 0);
if (n > 0) {
rw->body_consumed += (size_t)n;
}
return n;
}
/* ── Protothread task ──────────────────────────────────────────────────── */
SYN_PT_Status syn_httpd_task(SYN_PT *pt, SYN_Task *task)
{
SYN_Httpd *srv = (SYN_Httpd *)task->user_data;
PT_BEGIN(pt);
for (;;) {
/* Non-blocking step — always returns immediately */
if (srv->running) {
syn_httpd_step(srv);
}
PT_YIELD(pt);
}
PT_END(pt);
}
#endif /* SYN_USE_HTTPD */