53#if DETWS_ENABLE_WEBSOCKET
56#elif DETWS_ENABLE_AUTH
62#if DETWS_ENABLE_AUTH_LOCKOUT
66#include <esp_system.h>
72#include <esp_system.h>
75#if DETWS_ENABLE_WEBDAV
79#if DETWS_ENABLE_METRICS || DETWS_ENABLE_STATS
82#if DETWS_HTTP_EMIT_DATE
83#if DETWS_ENABLE_TIME_SOURCE
123 return "Partial Content";
124#if DETWS_ENABLE_WEBDAV
126 return "Multi-Status";
129 return "Moved Permanently";
135 return "Not Modified";
137 return "Temporary Redirect";
139 return "Permanent Redirect";
141 return "Bad Request";
143 return "Unauthorized";
149 return "Method Not Allowed";
151 return "Request Timeout";
154#if DETWS_ENABLE_WEBDAV
156 return "Precondition Failed";
160 return "Bad Gateway";
163 return "Payload Too Large";
165 return "URI Too Long";
167 return "Range Not Satisfiable";
169 return "Too Many Requests";
171 return "Internal Server Error";
173 return "Not Implemented";
175 return "Service Unavailable";
193 if (strcmp(m,
"GET") == 0)
195 if (strcmp(m,
"POST") == 0)
197 if (strcmp(m,
"PUT") == 0)
199 if (strcmp(m,
"DELETE") == 0)
201 if (strcmp(m,
"PATCH") == 0)
203 if (strcmp(m,
"HEAD") == 0)
205 if (strcmp(m,
"OPTIONS") == 0)
237 : _route_count(0), _not_found_handler(nullptr), _cors_enabled(false), _log_cb(nullptr), _listener_count(0),
238 _middleware_count(0), _rl_max(0), _rl_window_ms(0), _rl_window_start(0), _rl_count(0)
243 _middleware[i] =
nullptr;
244 _cors_header_buf[0] =
'\0';
245 _cache_control_buf[0] =
'\0';
247 _extra_hdr[i][0] =
'\0';
249 regen_digest_secret();
251#if DETWS_ENABLE_STATS
267void DetWebServer::note_response(uint8_t slot_id,
int code,
int body_len)
269#if DETWS_ENABLE_STATS
273 else if (code >= 400)
275 else if (code >= 200 && code < 300)
285#if DETWS_ENABLE_KEEPALIVE || DETWS_ENABLE_WEBSOCKET
289static bool conn_has_token(
const char *hdr,
const char *token)
293 size_t tlen = strnlen(token, 32);
297 while (*p ==
' ' || *p ==
',' || *p ==
'\t')
299 const char *start = p;
300 while (*p && *p !=
',')
302 size_t len = (size_t)(p - start);
303 while (len && (start[len - 1] ==
' ' || start[len - 1] ==
'\t'))
305 if (len == tlen && strncasecmp(start, token, tlen) == 0)
314#if DETWS_ENABLE_KEEPALIVE
315bool DetWebServer::keepalive_eval(uint8_t slot_id)
325 keep = !conn_has_token(c,
"close");
327 keep = conn_has_token(c,
"keep-alive");
348void DetWebServer::resp_end(uint8_t slot_id,
int code,
int body_len,
bool keep,
bool pre_flushed)
354 note_response(slot_id, code, body_len);
360const char *DetWebServer::resp_conn_hdr(uint8_t slot_id,
bool *keep_out)
363#if DETWS_ENABLE_KEEPALIVE
364 keep = keepalive_eval(slot_id);
370 return keep ?
"Connection: keep-alive\r\n" :
"Connection: close\r\n";
377int DetWebServer::append_resp_trailer(
char *buf,
size_t cap,
int hlen, uint8_t slot_id,
const char *cl)
381 if ((
size_t)hlen >= cap)
383#if DETWS_HTTP_EMIT_DATE
388 char date_hdr[48] =
"";
390#if DETWS_ENABLE_TIME_SOURCE
395 snprintf(date_hdr,
sizeof(date_hdr),
"Date: %s\r\n", imf);
397 const char *date_hdr =
"";
399 int n = snprintf(buf + hlen, cap - (
size_t)hlen,
"%s%s%s%s\r\n", date_hdr, _cors_enabled ? _cors_header_buf :
"",
400 _extra_hdr[slot_id], cl);
406 if ((
size_t)n >= cap - (
size_t)hlen)
415 _listen_ports[_listener_count] = port;
416 _listen_protos[_listener_count] = proto;
417 _listen_tls[_listener_count] =
false;
422 return (int32_t)(_listener_count - 1);
433#if DETWS_ENABLE_HTTP3
434 bool h3_running =
false;
442static void detws_pump_trampoline(
int worker_id)
449#if DETWS_ENABLE_HTTP3
452static void detws_h3_request_trampoline(
void *app, uint32_t conn_id, uint64_t stream_id,
const char *method,
453 const char *path,
const char *authority,
const uint8_t *body,
size_t body_len)
456 ((
DetWebServer *)app)->dispatch_h3_request(conn_id, stream_id, method, path, authority, body, body_len);
462static void detws_h3_rng(uint8_t *out,
size_t len)
468 uint32_t r = esp_random();
469 size_t n = (len - i) < 4 ? (len - i) : 4;
470 memcpy(out + i, &r, n);
474 static uint32_t s = 0x9e3779b9u;
475 for (
size_t i = 0; i < len; i++)
477 s = s * 1664525u + 1013904223u;
478 out[i] = (uint8_t)(s >> 24);
489static void detws_http_on_poll(uint8_t slot)
497 if (_listener_count == 0
505 regen_digest_secret();
513 for (
int i = 0; i < 8; i++)
515 uint32_t r = esp_random();
516 memcpy(sec + i * 4, &r, 4);
519 for (
int i = 0; i < 32; i++)
520 sec[i] = (uint8_t)(0xA5 ^ i);
522 csrf_set_secret(sec,
sizeof(sec));
527#if DETWS_ENABLE_WEBSOCKET
533 for (uint8_t i = 0; i < _listener_count; i++)
535 if (
listener_add(i, _listen_ports[i], _listen_protos[i], _listen_tls[i]) < 0)
538#if DETWS_ENABLE_HTTP3
543 QuicServerConfig h3cfg;
544 memset(&h3cfg, 0,
sizeof(h3cfg));
545 h3cfg.cert_der = _h3_cert;
546 h3cfg.cert_len = _h3_cert_len;
547 memcpy(h3cfg.ed25519_seed, _h3_seed,
sizeof(h3cfg.ed25519_seed));
548 h3cfg.rng = detws_h3_rng;
549 s_inst.h3_running = quic_server_begin(_h3_port, &h3cfg, detws_h3_request_trampoline,
this);
563 int32_t rc =
listen(port);
569#if DETWS_ENABLE_HTTP3
570bool DetWebServer::h3_cert(
const uint8_t *cert_der,
size_t cert_len,
const uint8_t ed25519_seed[32], uint16_t port)
572 if (!cert_der || cert_len == 0 || !ed25519_seed)
575 _h3_cert_len = cert_len;
576 memcpy(_h3_seed, ed25519_seed,
sizeof(_h3_seed));
585static bool h3_resp_sink(uint8_t slot,
int code,
const char *content_type,
const char *body,
size_t len)
588 return quic_server_respond(c->h3_conn_id, c->h3_stream, code, content_type, (
const uint8_t *)body, len);
591void DetWebServer::dispatch_h3_request(uint32_t conn_id, uint64_t stream_id,
const char *method,
const char *path,
592 const char *authority,
const uint8_t *body,
size_t body_len)
594 const uint8_t slot = DETWS_H3_DISPATCH_SLOT;
599 size_t mn = strnlen(method,
sizeof(r->
method));
600 if (mn >=
sizeof(r->
method))
601 mn =
sizeof(r->
method) - 1;
602 memcpy(r->
method, method, mn);
605 const char *q = strchr(path,
'?');
606 size_t plen = q ? (size_t)(q - path) : strnlen(path, sizeof(r->path));
607 if (plen >=
sizeof(r->
path))
608 plen =
sizeof(r->
path) - 1;
609 memcpy(r->
path, path, plen);
614 size_t ql = strnlen(q + 1,
sizeof(r->
query));
615 if (ql >=
sizeof(r->
query))
616 ql =
sizeof(r->
query) - 1;
617 memcpy(r->
query, q + 1, ql);
626 memcpy(h->
key,
"host", 5);
627 size_t vl = strnlen(authority,
sizeof(h->
val));
628 if (vl >=
sizeof(h->
val))
629 vl =
sizeof(h->
val) - 1;
630 memcpy(h->
val, authority, vl);
634 if (body && body_len)
637 memcpy(r->
body, body, n);
649 c->h3_conn_id = conn_id;
650 c->h3_stream = stream_id;
651 c->resp_sink = h3_resp_sink;
656 match_and_execute(slot);
660 c->resp_sink =
nullptr;
667bool DetWebServer::tls_cert(
const uint8_t *cert,
size_t cert_len,
const uint8_t *key,
size_t key_len)
669 return det_tls_global_init(cert, cert_len, key, key_len);
672int32_t DetWebServer::listen_tls(uint16_t port)
676 _listen_ports[_listener_count] = port;
678 _listen_tls[_listener_count] =
true;
683int32_t DetWebServer::begin_tls(uint16_t port,
const uint8_t *cert,
size_t cert_len,
const uint8_t *key,
size_t key_len,
686 if (!tls_cert(cert, cert_len, key, key_len))
688 int32_t rc = listen_tls(port);
695bool DetWebServer::tls_require_client_cert(
const uint8_t *ca,
size_t ca_len)
697 return det_tls_set_client_ca(ca, ca_len);
700int DetWebServer::tls_client_subject(uint8_t slot_id,
char *out,
size_t out_len)
702 return det_tls_peer_subject(slot_id, out, out_len);
709 if (_listener_count == 0)
725#if DETWS_ENABLE_WEBSOCKET
763 Route *r = &_routes[_route_count++];
774 Route *r = &_routes[_route_count++];
791 Route *r = &_routes[_route_count++];
801 const char *pass,
bool digest)
805 Route *r = &_routes[_route_count++];
810 r->auth_required =
true;
811 r->auth_digest = digest;
821#if DETWS_ENABLE_WEBSOCKET
822void DetWebServer::on_ws(
const char *path, WsConnectHandler on_connect, WsMessageHandler on_message,
823 WsCloseHandler on_close)
827 Route *r = &_routes[_route_count++];
829 r->
type = RouteType::ROUTE_WS;
830 r->ws_connect = on_connect;
831 r->ws_message = on_message;
832 r->ws_close = on_close;
837void DetWebServer::on_sse(
const char *path, SseConnectHandler on_connect)
841 Route *r = &_routes[_route_count++];
843 r->
type = RouteType::ROUTE_SSE;
844 r->sse_connect = on_connect;
850 _not_found_handler = callback;
868 if (!origin || origin[0] ==
'\0')
870 _cors_enabled =
false;
871 _cors_header_buf[0] =
'\0';
875 "Access-Control-Allow-Origin: %s\r\n"
876 "Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS\r\n"
877 "Access-Control-Allow-Headers: Content-Type\r\n",
879 _cors_enabled =
true;
884 if (!value || value[0] ==
'\0')
886 _cache_control_buf[0] =
'\0';
903bool DetWebServer::path_matches(
const char *route,
bool is_wildcard,
const char *req_path)
906 return strcmp(route, req_path) == 0;
910 return strncmp(route, req_path, prefix_len) == 0;
925static void capture_path_param(
HttpReq *req,
const char *key,
size_t klen,
const char *val,
size_t vlen)
932 memcpy(qp->
key, key, klen);
933 qp->
key[klen] =
'\0';
936 memcpy(qp->
val, val, vlen);
937 qp->
val[vlen] =
'\0';
940static bool match_path_params(
const char *route,
const char *path,
HttpReq *req)
943 const char *r = route;
944 const char *p = path;
946 while (*r ==
'/' && *p ==
'/')
950 const char *rseg = r;
951 while (*r && *r !=
'/')
953 size_t rlen = (size_t)(r - rseg);
954 const char *pseg = p;
955 while (*p && *p !=
'/')
957 size_t plen = (size_t)(p - pseg);
959 if (rlen > 0 && rseg[0] ==
':')
963 capture_path_param(req, rseg + 1, rlen - 1, pseg, plen);
965 else if (rlen != plen || strncmp(rseg, pseg, rlen) != 0)
972 return (*r ==
'\0' && *p ==
'\0');
987#if DETWS_ENABLE_WEBSOCKET
988void DetWebServer::ws_dispatch_message(
WsConn *ws)
990 for (uint8_t r = 0; r < _route_count; r++)
991 if (_routes[r].type == RouteType::ROUTE_WS && _routes[r].ws_message)
993 _routes[r].ws_message(ws->
ws_id);
998void DetWebServer::ws_dispatch_close(
WsConn *ws)
1000 for (uint8_t r = 0; r < _route_count; r++)
1001 if (_routes[r].type == RouteType::ROUTE_WS && _routes[r].
ws_close)
1003 _routes[r].ws_close(ws->
ws_id);
1030#if DETWS_ENABLE_HTTP3
1033 if (worker_id == 0 && s_inst.h3_running)
1065#if DETWS_ENABLE_EDGE_CACHE
1068static bool (*s_edge_poll)(uint8_t slot) =
nullptr;
1069void detws_http_set_edge_poll(
bool (*fn)(uint8_t slot))
1077#if DETWS_ENABLE_EDGE_CACHE
1080 if (s_edge_poll && s_edge_poll(i))
1083#if DETWS_ENABLE_FILE_SERVING
1086 if (
s_send.file[i].active)
1099#if DETWS_ENABLE_WEBSOCKET
1112 while ((n = det_tls_read(i, tbuf,
sizeof(tbuf))) > 0)
1114 for (
int k = 0; k < n; k++)
1119 ws_dispatch_message(ws);
1130 ws_dispatch_close(ws);
1143 ws_dispatch_message(ws);
1148 ws_dispatch_close(ws);
1167#if DETWS_ENABLE_KEEPALIVE
1183 match_and_execute(i);
1189 send(i, 400, DET_MIME_TEXT_PLAIN,
"Bad Request");
1193 send(i, 413, DET_MIME_TEXT_PLAIN,
"Payload Too Large");
1197 send(i, 414, DET_MIME_TEXT_PLAIN,
"URI Too Long");
1214#if DETWS_ENABLE_DIAG
1215void DetWebServer::diag(uint8_t slot_id)
1217 send(slot_id, 200, DET_MIME_JSON, DETWS_DIAG_JSON);
1230 return strcmp(
http_pool[slot_id].method,
"HEAD") == 0;
1234static void allow_append(
char *buf,
size_t cap,
const char *m)
1236 if (!m[0] || strstr(buf, m))
1238 size_t len = strnlen(buf, cap);
1240 snprintf(buf, cap,
"%s", m);
1242 snprintf(buf + len, cap - len,
", %s", m);
1250static void send_error_close(uint8_t slot_id,
const char *status,
const char *extra_hdr,
const char *body)
1259 int blen = (int)strnlen(body, 0xFFFF);
1261 int hlen = snprintf(header,
sizeof(header),
1264 "Content-Type: %s\r\n"
1265 "Content-Length: %d\r\n"
1266 "Connection: close\r\n\r\n",
1267 status, extra_hdr ? extra_hdr :
"", DET_MIME_TEXT_PLAIN, blen);
1286static void send_method_not_allowed(uint8_t slot_id,
const char *allow)
1289 snprintf(extra,
sizeof(extra),
"Allow: %s\r\n", allow);
1290 send_error_close(slot_id,
"405 Method Not Allowed", extra,
"Method Not Allowed");
1293#if DETWS_ENABLE_AUTH_LOCKOUT
1297static DetIp lockout_client_ip(uint8_t slot_id)
1307static void send_too_many_requests(uint8_t slot_id, uint32_t retry_after_s)
1310 snprintf(extra,
sizeof(extra),
"Retry-After: %lu\r\n", (
unsigned long)retry_after_s);
1311 send_error_close(slot_id,
"429 Too Many Requests", extra,
"Too Many Requests");
1315bool DetWebServer::route_admits(
const Route *r, uint8_t slot_id,
HttpReq *req)
const
1320 : r->is_param ? match_path_params(r->path, req->path, req)
1321 : path_matches(r->path, r->is_wildcard, req->path);
1331#if DETWS_ENABLE_CSRF
1338 char tok[CSRF_TOKEN_BUF];
1339 if (csrf_issue(tok,
sizeof(tok)) > 0)
1341 set_cookie(slot_id,
"csrf", tok,
"Path=/; SameSite=Strict");
1342 char body[CSRF_TOKEN_BUF + 16];
1343 snprintf(body,
sizeof(body),
"{\"token\":\"%s\"}", tok);
1344 send(slot_id, 200, DET_MIME_JSON, body);
1348 send(slot_id, 500, DET_MIME_TEXT_PLAIN,
"CSRF unavailable");
1359 if (!tok || !csrf_verify(tok))
1361 send(slot_id, 403, DET_MIME_TEXT_PLAIN,
"CSRF token missing or invalid");
1369#if DETWS_ENABLE_WEBSOCKET
1376 (strcasecmp(upgrade_hdr,
"websocket") == 0) &&
1380 send(slot_id, 400, DET_MIME_TEXT_PLAIN,
"WebSocket upgrade required");
1385 if (!ws_ver || strcmp(ws_ver,
"13") != 0)
1387 ws_send_version_required(slot_id);
1392 if (!ws_do_upgrade(slot_id, req, r->ws_connect))
1393 send(slot_id, 400, DET_MIME_TEXT_PLAIN,
"Bad WebSocket handshake");
1397#if DETWS_ENABLE_AUTH
1398bool DetWebServer::authorize_request(uint8_t slot_id,
HttpReq *req,
const Route *r)
1400#if DETWS_ENABLE_AUTH_LOCKOUT
1401 DetIp cip = lockout_client_ip(slot_id);
1402 uint32_t now = (uint32_t)millis();
1403 uint32_t remain = auth_lockout_remaining_ms(&cip, now);
1407 send_too_many_requests(slot_id, (remain + 999) / 1000);
1412 bool ok = r->auth_digest ? check_digest_auth(slot_id, req, r, &stale) : check_basic_auth(slot_id, req, r);
1413#if DETWS_ENABLE_AUTH_LOCKOUT
1417 auth_lockout_succeed(&cip);
1419 auth_lockout_fail(&cip, now);
1423 send_unauth(slot_id, r, stale);
1431 bool *path_matched,
char *allow_buf,
size_t allow_cap)
1433#if DETWS_ENABLE_WEBSOCKET
1434 if (r->
type == RouteType::ROUTE_WS)
1436 handle_ws_route(slot_id, req, method, r);
1442 if (r->
type == RouteType::ROUTE_SSE)
1444 if (!sse_do_upgrade(slot_id, req, r->sse_connect))
1445 send(slot_id, 503, DET_MIME_TEXT_PLAIN,
"Service Unavailable");
1450#if DETWS_ENABLE_FILE_SERVING
1451 if (r->
type == RouteType::ROUTE_STATIC)
1456 *path_matched =
true;
1457 allow_append(allow_buf, allow_cap,
"GET");
1458 allow_append(allow_buf, allow_cap,
"HEAD");
1461 serve_static_request(slot_id, req, r);
1472 *path_matched =
true;
1473 allow_append(allow_buf, allow_cap, method_name(r->
method));
1476 allow_append(allow_buf, allow_cap,
"HEAD");
1479#if DETWS_ENABLE_AUTH
1480 if (r->auth_required && !authorize_request(slot_id, req, r))
1487void DetWebServer::match_and_execute(uint8_t slot_id)
1494 _extra_hdr[slot_id][0] =
'\0';
1499 if (rate_limit_check(slot_id))
1501 if (run_middleware(slot_id, req))
1504#if DETWS_ENABLE_WEBDAV
1508 if (try_serve_dav(slot_id, req))
1519#if DETWS_ENABLE_CSRF
1520 if (csrf_gate(slot_id, req, method))
1527 send(slot_id, 501, DET_MIME_TEXT_PLAIN,
"Not Implemented");
1534 send(slot_id, 501, DET_MIME_TEXT_PLAIN,
"Not Implemented");
1540 bool path_matched =
false;
1542 allow_buf[0] =
'\0';
1544 for (uint8_t i = 0; i < _route_count; i++)
1546 Route *r = &_routes[i];
1547 if (!route_admits(r, slot_id, req))
1549 if (dispatch_matched_route(slot_id, req, method, r, &path_matched, allow_buf,
sizeof(allow_buf)))
1556 send_method_not_allowed(slot_id, allow_buf);
1560 if (_not_found_handler)
1561 _not_found_handler(slot_id, req);
1563 send(slot_id, 404, DET_MIME_TEXT_PLAIN,
"Not Found");
1585 send(slot_id, code, content_type, (
const uint8_t *)payload, payload ? strnlen(payload, 0xFFFF) : 0);
1588void DetWebServer::send(uint8_t slot_id,
int code,
const char *content_type,
const uint8_t *body,
size_t body_len)
1592 const char *payload = (
const char *)body;
1594#if DETWS_ENABLE_HTTP2 || DETWS_ENABLE_HTTP3
1599 if (conn->resp_sink)
1601 conn->resp_sink(slot_id, code, content_type, payload, body_len);
1611 int payload_len = (int)(body_len > 0xFFFF ? 0xFFFF : body_len);
1614 const char *cl = resp_conn_hdr(slot_id, &keep);
1617 int hlen = snprintf(header,
sizeof(header),
1618 "HTTP/1.1 %d %s\r\n"
1619 "Content-Type: %s\r\n"
1620 "Content-Length: %d\r\n",
1621 code,
status_text(code), content_type, payload_len);
1622 hlen = append_resp_trailer(header,
sizeof(header), hlen, slot_id, cl);
1634 if (!head && payload_len > 0 && (
size_t)hlen + (
size_t)payload_len <=
sizeof(header))
1636 memcpy(header + hlen, payload, (
size_t)payload_len);
1639 else if (!head && payload_len > 0)
1649 resp_end(slot_id, code, payload_len, keep,
true);
1667#if DETWS_ENABLE_HTTP2 || DETWS_ENABLE_HTTP3
1668 if (conn->resp_sink)
1670 conn->resp_sink(slot_id, code,
"text/plain",
"", 0);
1681 const char *cl = resp_conn_hdr(slot_id, &keep);
1684 int hlen = snprintf(header,
sizeof(header),
1685 "HTTP/1.1 %d %s\r\n"
1686 "Content-Length: 0\r\n",
1688 hlen = append_resp_trailer(header,
sizeof(header), hlen, slot_id, cl);
1692 resp_end(slot_id, code, 0, keep,
true);
1721 const char *cl = resp_conn_hdr(slot_id, &keep);
1724 int hlen = snprintf(header,
sizeof(header),
1725 "HTTP/1.1 %d %s\r\n"
1727 "Content-Length: 0\r\n",
1729 hlen = append_resp_trailer(header,
sizeof(header), hlen, slot_id, cl);
1733 resp_end(slot_id, code, 0, keep,
true);
#define DETWS_ENABLE_HTTP3
HTTP/3 (RFC 9114) over QUIC (RFC 9000) - implemented, host-tested end-to-end (HW verification pending...
DetIface
Network interface a connection arrived on (for per-route filtering).
@ DETIFACE_STA
Station interface (joined to an AP / your LAN).
@ DETIFACE_ANY
Unknown / no filter (matches any interface).
#define DETWS_ENABLE_TLS
TLS (HTTPS/WSS) via mbedTLS with a static memory pool (ESP32-only).
#define MAX_LISTENERS
Maximum number of simultaneously active listener ports.
ConnProto
Application protocol spoken on a listener port or connection slot.
@ PROTO_HTTP
HTTP/1.1 with optional WS and SSE upgrades.
#define CACHE_CONTROL_BUF_SIZE
Size of the optional Cache-Control header line stored in DetWebServer.
#define RESP_HDR_BUF_SIZE
Stack buffer for HTTP response header lines in send() / send_empty() / send_unauth() / serve_file().
#define QUERY_VAL_LEN
Maximum query-parameter value length.
#define DETWS_KEEPALIVE_MAX_REQUESTS
Maximum requests served on one keep-alive connection before it is closed.
#define BODY_BUF_SIZE
Maximum request body bytes stored in HttpReq::body.
#define MAX_MIDDLEWARE
Maximum globally-registered middleware functions.
#define MAX_HEADERS
Maximum HTTP headers stored per request.
#define MAX_PATH_LEN
Maximum URL path length (including leading /).
#define MAX_AUTH_LEN
Maximum username or password length for HTTP Basic Authentication.
#define CORS_HDR_BUF_SIZE
Size of the pre-built CORS header block stored in DetWebServer.
#define MAX_CONNS
Maximum simultaneous TCP connections (fixed static pool; ~3.95 KB of internal RAM per slot).
#define QUERY_KEY_LEN
Maximum query-parameter key length.
#define MAX_PATH_PARAMS
Maximum number of :name path parameters captured per route match.
#define MAX_ROUTES
Maximum simultaneously registered routes.
Per-peer brute-force lockout for HTTP auth (DETWS_ENABLE_AUTH_LOCKOUT).
Single-port HTTP server with deterministic, zero-allocation execution.
void set_cookie(uint8_t slot_id, const char *name, const char *value, const char *attrs=nullptr)
Queue a Set-Cookie response header for the next send on this slot.
void set_cache_control(const char *value)
Set the Cache-Control header emitted for static files.
void on_regex(const char *pattern, HttpMethod method, Handler callback)
Register a route whose path is a regular expression.
void on(const char *path, HttpMethod method, Handler callback)
Register a route handler.
void handle()
Drive the server - call every Arduino loop() iteration.
int32_t restart(const WebServerConfig *cfg=nullptr)
Hard-reset all connections and re-open all registered listeners.
int32_t listen(uint16_t port, ConnProto proto=ConnProto::PROTO_HTTP)
Register a port to listen on when begin() is called.
void send(uint8_t slot_id, int code, const char *content_type, const char *payload)
Send an HTTP response with a body and close the connection.
void on_not_found(Handler callback)
Register a fallback handler for unmatched requests.
void send_empty(uint8_t slot_id, int code)
Send a headers-only HTTP response and close the connection.
void http_poll_slot(uint8_t slot_id)
The instance-bound HTTP poll pump for one slot (the HTTP ProtoHandler's on_poll).
bool defer(uint8_t slot, detws_deferred_fn fn, void *arg)
Run fn(arg) on the worker that owns connection slot.
void on_request_log(RequestLogCb cb)
Install a per-request access-log callback (one hook, no buffering).
void service_once(int worker_id=0)
Run exactly one service iteration for worker worker_id (the body driven by that worker's task,...
void set_cors(const char *origin)
Enable CORS by pre-building the Access-Control headers.
void redirect(uint8_t slot_id, int code, const char *location)
Send an HTTP redirect (Location header, empty body) and close.
void set_ap_ip(uint32_t ap_ip)
Tell the server the softAP IPv4 address for STA/AP route filtering.
int32_t begin(const WebServerConfig *cfg=nullptr)
Initialize all connection slots and open all registered listeners.
DetWebServer()
Construct a DetWebServer with an empty routing table.
void stop()
Gracefully stop the server.
static void pool_init(const WebServerConfig *cfg=nullptr)
Initialize the connection pool and store the runtime config.
static void stop()
Abort all active connections and reset the pool to ConnState::CONN_FREE.
Pluggable monotonic clock for all library timing.
uint32_t detws_millis(void)
The library's monotonic time at 1000 Hz (milliseconds).
Stateless HMAC-signed CSRF token (DETWS_ENABLE_CSRF).
void fill_route_base(Route *r, const char *path)
Register a route in the route table.
const char * status_text(int code)
Convert an HTTP status code to its standard reason phrase.
bool req_is_head(uint8_t slot_id)
True if the request in slot slot_id used the HEAD method (send headers, no body).
Layer 7 (Application) - public HTTP routing API.
@ DETWS_ERR_LISTEN_FAILED
A listener failed to open (bind/listen/lwIP error).
@ DETWS_ERR_LISTENER_FULL
listen(): listener pool (MAX_LISTENERS) is full.
@ DETWS_ERR_NO_LISTENERS
begin() called before any listen() / begin(port).
void(* Handler)(uint8_t slot_id, HttpReq *request)
Callback signature for HTTP request handlers.
void(* RequestLogCb)(const char *method, const char *path, int status, int body_len)
Per-request access-log callback (see DetWebServer::on_request_log()).
@ ROUTE_HTTP
Standard HTTP request/response.
HttpMethod
HTTP request methods supported by the router.
@ HTTP_METHOD_UNKNOWN
Unrecognized method token → 501 Not Implemented.
@ HTTP_DELETE
Idempotent delete.
@ HTTP_POST
Non-idempotent create / action.
@ HTTP_OPTIONS
Capability query / CORS preflight.
@ HTTP_PATCH
Partial update.
@ HTTP_GET
Safe, idempotent read.
@ HTTP_PUT
Idempotent replace.
@ HTTP_HEAD
Same as GET but no response body.
Library-private declarations shared between dwserver.cpp and the src/server/*.cpp request-handler tra...
bool regex_match(const char *pattern, const char *path)
Whole-path regex match (anchored both ends; bounded by RE_MAX_STEPS, fails closed)....
Bridge between the HTTP/2 engine (h2_conn) and the server's request pipeline.
Tiny no-stdlib hex encode / decode / nibble helpers (one shared copy).
const char * http_get_header(const HttpReq *req, const char *key)
Look up a header value by name (case-insensitive).
HttpReq http_pool[CONN_POOL_SLOTS]
Pool of parser contexts, one per connection-pool slot (incl. reserved dispatch slots).
@ PARSE_ERROR
Unrecoverable parse failure → 400.
@ PARSE_ENTITY_TOO_LARGE
Content-Length > BODY_BUF_SIZE → 413.
@ PARSE_COMPLETE
Full request parsed; ready for dispatch.
@ PARSE_URI_TOO_LONG
Path exceeds MAX_PATH_LEN → 414.
@ HTTP_11
HTTP/1.1 - persistent connection by default.
@ DET_IP_NONE
empty / unparsed
int32_t listener_add(uint8_t idx, uint16_t port, ConnProto proto, bool tls)
Create a listening socket on port and register it at idx.
void listener_stop_all()
Stop all active listeners.
Layer 4 (Listener) - per-port TCP listener abstraction.
Shared HTTP Content-Type ("MIME") string constants (one source of truth).
size_t detws_ntp_http_date(char *out, size_t out_cap)
Format the current time as an RFC 7231 IMF-fixdate (HTTP Date).
Optional SNTP wall-clock time sync (DETWS_ENABLE_NTP).
void http_reset(uint8_t slot_id)
Reset the HTTP parser for a connection slot.
void http_proto_set_poll(void(*fn)(uint8_t slot))
Install the HTTP per-slot poll pump (the routing core's instance-bound on_poll).
void http_parse(uint8_t slot_id)
Drain the transport ring buffer and advance the HTTP parser.
Layer 6 (Presentation) - wires the transport ring buffer to the HTTP parser.
Layer 5 (Session) - per-protocol connection handler dispatch table.
const ProtoHandler * proto_get(ConnProto proto)
Look up the handler for proto.
HTTP/3 server glue - binds UDP to a pool of QUIC + HTTP/3 connections (RFC 9000/9114).
void server_tick(int worker_id)
Drive the session layer for one Arduino loop iteration.
Software SHA-1 implementation - no platform dependencies.
void sse_init()
Initialize all SSE pool slots to inactive.
SseConn * sse_find(uint8_t slot_id)
Find the SseConn for a given TCP slot, or nullptr.
SHA-256 (FIPS 180-4) - streaming context and one-shot API.
bool active
a chunked response is in progress on this slot.
A v4 or v6 address in network (big-endian) byte order.
DetIpFamily family
address family tag
Fully-parsed HTTP/1.1 request.
Header headers[MAX_HEADERS]
Captured header fields.
char query[MAX_QUERY_LEN]
Raw query string (after ?).
char method[DETWS_METHOD_BUF_SIZE]
HTTP method, null-terminated (OPTIONS, or WebDAV methods when enabled).
uint8_t body[BODY_BUF_SIZE+1]
Stored body bytes, always null-terminated.
QueryParam path_params[MAX_PATH_PARAMS]
:name captures from the matched route.
uint8_t header_count
Valid entries in headers[].
ParseState parse_state
Current parser state.
size_t path_idx
Write cursor into path[].
size_t content_length
Value of Content-Length header (0 if absent).
uint8_t path_param_count
Valid entries in path_params[].
size_t body_len
Bytes stored in body[] (≤ BODY_BUF_SIZE).
HttpVersion version
Protocol version parsed from the request line.
char path[MAX_PATH_LEN]
URL path, null-terminated; no query string.
size_t body_bytes_read
Body bytes received (may exceed BODY_BUF_SIZE).
size_t query_idx
Write cursor into query[].
DetWebServer * worker_server
DetWebServer * http_instance
Per-protocol connection event/poll callbacks (Layer 5 dispatch vtable).
void(* on_poll)(uint8_t slot)
Called for an active slot each handle() loop (nullable).
A single parsed query-string parameter.
char val[QUERY_VAL_LEN]
Parameter value (empty string if absent).
char key[QUERY_KEY_LEN]
Parameter name, null-terminated.
Internal route entry stored in the routing table.
bool is_param
true when the path contains a :name segment.
RouteType type
HTTP, WS, or SSE.
bool is_regex
true when the path is a regex (see on_regex()).
Handler callback
HTTP handler (RouteType::ROUTE_HTTP only).
DetIface iface_filter
Interface gate; DetIface::DETIFACE_ANY (0) = match any interface.
HttpMethod method
HTTP method (RouteType::ROUTE_HTTP only).
bool is_active
false for unused table slots.
bool is_wildcard
true when path ends with *.
char path[MAX_PATH_LEN]
Null-terminated path pattern.
ChunkSend chunk[MAX_CONNS]
A single TCP connection context.
DetIface iface
Interface this connection arrived on; set at accept time.
struct tcp_pcb * pcb
lwIP PCB; null when slot is free.
DetAtomic< ConnState > state
Lifecycle state; acquire/release for inter-task visibility.
Runtime-tunable server parameters.
WebSocket connection state stored in ws_pool[].
uint8_t ws_id
Index into ws_pool[] (set at init).
WsParseState parse_state
Current frame parser state.
bool det_conn_send(uint8_t slot, const void *data, u16_t len)
Send len bytes on connection slot (copies data; TLS-aware).
void det_conn_flush(uint8_t slot)
Flush queued bytes / finish the send on slot (TLS-aware).
void det_conn_begin_close(uint8_t slot_id)
Begin a graceful close that dwells in ConnState::CONN_CLOSING until the peer ACKs.
uint32_t detws_ap_ip
softAP IPv4 address (network byte order) for STA/AP interface tagging.
void det_conn_ack_consumed(uint8_t slot)
Reopen the TCP receive window by however much slot has drained.
TcpConn conn_pool[CONN_POOL_SLOTS]
Static pool of connection contexts. Defined in tcp.cpp. Sized CONN_POOL_SLOTS: MAX_CONNS TCP slots pl...
bool det_conn_send_flush(uint8_t slot, const void *data, u16_t len)
Send len bytes on slot and flush in a single tcpip_thread round-trip.
bool det_conn_remote_addr(uint8_t slot, DetIp *out)
The connected peer's address as a family-tagged DetIp (IPv4 or IPv6).
void det_conn_abort_slot(uint8_t slot)
Hard-abort connection slot (RST) for a fatal condition. The transport owns the teardown order: free t...
@ CONN_ACTIVE
Live connection; PCB is valid.
@ CONN_FREE
Slot is available; no PCB is attached.
size_t detws_time_http_date(char *out, size_t out_cap)
The current best time (detws_time_now, any registered NTP / GPS / RTC / ... source) formatted as an R...
Multi-source time fallback matrix (DETWS_ENABLE_TIME_SOURCE).
Deterministic TLS engine: mbedTLS over a static memory pool (DETWS_ENABLE_TLS).
Layer 7 (Application) - embedded web assets generated from src/web/input/.
WebDAV server core (RFC 4918): method classification, header parsing, and the 207 Multi-Status XML bu...
void ws_free(uint8_t slot_id)
Free the WsConn associated with a TCP slot.
void ws_feed_byte(WsConn *ws, uint8_t byte)
Feed one already-plaintext byte through the WS frame state machine.
void ws_close(WsConn *ws, WsCloseCode code)
Send a Close frame and mark the slot WsParseState::WS_CLOSED.
void ws_parse(WsConn *ws)
Drain the ring buffer for slot_id and feed bytes to the WS parser.
WsConn * ws_find(uint8_t slot_id)
Find the WsConn for a given TCP slot, or nullptr if none.
void ws_reset_frame(WsConn *ws)
Reset the frame parser back to WsParseState::WS_HEADER1, ready for the next frame.
void ws_init()
Initialize all WebSocket pool slots to inactive.
@ WS_FRAME_READY
Complete frame ready for dispatch.
@ WS_ERROR
Protocol error; close frame has been queued.
@ WS_CLOSED
Connection closed; slot may be recycled.
void detws_workers_stop(void)
Signal the worker task(s) to exit and wait briefly for them. No-op on host.
bool detws_workers_running(void)
True while worker task(s) are running (always false on host).
void detws_workers_start(detws_worker_pump_fn pump)
Spawn the worker task(s) and start them running pump. No-op on host.
bool detws_defer(int worker_id, detws_deferred_fn fn, void *arg)
Run fn(arg) on worker worker_id. Returns false if the queue is full.
void detws_worker_run_deferred(int worker_id)
Drain and run worker worker_id's deferred callbacks (called by the worker).
Layer 5 (Session) - server worker identity.
void(* detws_deferred_fn)(void *arg)
Deferred callback signature.