54#if PC_ENABLE_WEBSOCKET
63#if PC_ENABLE_HTTP_DELIVERY
66#if PC_ENABLE_AUTH_LOCKOUT
69#if PC_ENABLE_FORWARDED_TRUST
73#include <esp_system.h>
79#include <esp_system.h>
86#if PC_ENABLE_METRICS || PC_ENABLE_STATS
90#if PC_ENABLE_TIME_SOURCE
130 return "Partial Content";
133 return "Multi-Status";
136 return "Moved Permanently";
142 return "Not Modified";
144 return "Temporary Redirect";
146 return "Permanent Redirect";
148 return "Bad Request";
150 return "Unauthorized";
156 return "Method Not Allowed";
158 return "Request Timeout";
163 return "Precondition Failed";
167 return "Bad Gateway";
170 return "Payload Too Large";
172 return "URI Too Long";
174 return "Range Not Satisfiable";
176 return "Too Many Requests";
178 return "Internal Server Error";
180 return "Not Implemented";
182 return "Service Unavailable";
200 if (strcmp(m,
"GET") == 0)
204 if (strcmp(m,
"POST") == 0)
208 if (strcmp(m,
"PUT") == 0)
212 if (strcmp(m,
"DELETE") == 0)
216 if (strcmp(m,
"PATCH") == 0)
220 if (strcmp(m,
"HEAD") == 0)
224 if (strcmp(m,
"OPTIONS") == 0)
258 : _route_count(0), _not_found_handler(nullptr), _cors_enabled(false), _log_cb(nullptr), _listener_count(0),
259 _middleware_count(0), _rl_max(0), _rl_window_ms(0), _rl_window_start(0), _rl_count(0)
267 _middleware[i] =
nullptr;
269 _cors_header_buf[0] =
'\0';
270 _cache_control_buf[0] =
'\0';
273 _extra_hdr[i][0] =
'\0';
276 regen_digest_secret();
294void PC::note_response(uint8_t slot_id,
int code,
int body_len)
302 else if (code >= 400)
306 else if (code >= 200 && code < 300)
318#if PC_ENABLE_KEEPALIVE || PC_ENABLE_WEBSOCKET
322static bool conn_has_token(
const char *hdr,
const char *token)
328 size_t tlen = strnlen(token, 32);
332 while (*p ==
' ' || *p ==
',' || *p ==
'\t')
336 const char *start = p;
337 while (*p && *p !=
',')
341 size_t len = (size_t)(p - start);
342 while (len && (start[len - 1] ==
' ' || start[len - 1] ==
'\t'))
346 if (len == tlen && strncasecmp(start, token, tlen) == 0)
359#if PC_ENABLE_KEEPALIVE
360bool PC::keepalive_eval(uint8_t slot_id)
373 keep = !conn_has_token(c,
"close");
377 keep = conn_has_token(c,
"keep-alive");
403void PC::pc_resp_end(uint8_t slot_id,
int code,
int body_len,
bool keep,
bool pre_flushed)
413 note_response(slot_id, code, body_len);
419const char *PC::pc_resp_conn_hdr(uint8_t slot_id,
bool *keep_out)
422#if PC_ENABLE_KEEPALIVE
423 keep = keepalive_eval(slot_id);
433 return keep ?
"Connection: keep-alive\r\n" :
"Connection: close\r\n";
441 "Content-Length: 0\r\n"
442 "Connection: close\r\n\r\n";
448int PC::append_resp_trailer(
char *buf,
size_t cap,
int hlen, uint8_t slot_id,
const char *cl)
461 if ((
size_t)hlen >= cap)
470 char date_hdr[48] =
"";
472#if PC_ENABLE_TIME_SOURCE
478 pc_sb sb_date_hdr = {date_hdr,
sizeof(date_hdr), 0,
true};
488 const char *date_hdr =
"";
490 pc_sb sb411 = {buf + hlen, cap - (size_t)hlen, 0,
true};
492 pc_sb_put(&sb411, _cors_enabled ? _cors_header_buf :
"");
510 _listen_ports[_listener_count] = port;
511 _listen_protos[_listener_count] = proto;
512 _listen_tls[_listener_count] =
false;
517 return (int32_t)(_listener_count - 1);
529 bool pc_h3_running =
false;
537static void pc_pump_trampoline(
int worker_id)
549static void pc_h3_request_trampoline(
void *app, uint32_t conn_id, uint64_t stream_id,
const char *method,
550 const char *path,
const char *authority,
const uint8_t *body,
size_t body_len)
555 ((
PC *)app)->dispatch_h3_request(conn_id, stream_id, method, path, authority, body, body_len);
562static void pc_h3_rng(uint8_t *out,
size_t len)
568 uint32_t r = esp_random();
569 size_t n = (len - i) < 4 ? (len - i) : 4;
570 memcpy(out + i, &r, n);
574 static uint32_t s = 0x9e3779b9u;
575 for (
size_t i = 0; i < len; i++)
577 s = s * 1664525u + 1013904223u;
578 out[i] = (uint8_t)(s >> 24);
589static void pc_http_on_poll(uint8_t slot)
603 if (_listener_count == 0
611 regen_digest_secret();
619 for (
int i = 0; i < 8; i++)
621 uint32_t r = esp_random();
622 memcpy(sec + i * 4, &r, 4);
625 for (
int i = 0; i < 32; i++)
627 sec[i] = (uint8_t)(0xA5 ^ i);
630 pc_csrf_set_secret(sec,
sizeof(sec));
637#if PC_ENABLE_WEBSOCKET
643 for (uint8_t i = 0; i < _listener_count; i++)
649 if (
listener_add(i, _listen_ports[i], _listen_protos[i], _listen_tls[i]) < 0)
660 QuicServerConfig h3cfg;
661 memset(&h3cfg, 0,
sizeof(h3cfg));
662 h3cfg.cert_der = _h3_cert;
663 h3cfg.cert_len = _h3_cert_len;
664 memcpy(h3cfg.ed25519_seed, _h3_seed,
sizeof(h3cfg.ed25519_seed));
665 h3cfg.rng = pc_h3_rng;
666 s_inst.pc_h3_running = pc_quic_server_begin(_h3_port, &h3cfg, pc_h3_request_trampoline,
this);
680 int32_t rc =
listen(port);
689bool PC::pc_h3_cert(
const uint8_t *cert_der,
size_t cert_len,
const uint8_t ed25519_seed[32], uint16_t port)
691 if (!cert_der || cert_len == 0 || !ed25519_seed)
696 _h3_cert_len = cert_len;
697 memcpy(_h3_seed, ed25519_seed,
sizeof(_h3_seed));
706static bool pc_h3_resp_sink(uint8_t slot,
int code,
const char *content_type,
const char *body,
size_t len)
709 return pc_quic_server_respond(c->pc_h3_conn_id, c->pc_h3_stream, code, content_type, (
const uint8_t *)body, len);
712void PC::dispatch_h3_request(uint32_t conn_id, uint64_t stream_id,
const char *method,
const char *path,
713 const char *authority,
const uint8_t *body,
size_t body_len)
715 const uint8_t slot = PC_H3_DISPATCH_SLOT;
720 size_t mn = strnlen(method,
sizeof(r->
method));
721 if (mn >=
sizeof(r->
method))
723 mn =
sizeof(r->
method) - 1;
725 memcpy(r->
method, method, mn);
728 const char *q = strchr(path,
'?');
729 size_t plen = q ? (size_t)(q - path) : strnlen(path, sizeof(r->path));
730 if (plen >=
sizeof(r->
path))
732 plen =
sizeof(r->
path) - 1;
734 memcpy(r->
path, path, plen);
739 size_t ql = strnlen(q + 1,
sizeof(r->
query));
740 if (ql >=
sizeof(r->
query))
742 ql =
sizeof(r->
query) - 1;
744 memcpy(r->
query, q + 1, ql);
753 memcpy(h->
key,
"host", 5);
754 size_t vl = strnlen(authority,
sizeof(h->
val));
755 if (vl >=
sizeof(h->
val))
757 vl =
sizeof(h->
val) - 1;
759 memcpy(h->
val, authority, vl);
763 if (body && body_len)
766 memcpy(r->
body, body, n);
778 c->pc_h3_conn_id = conn_id;
779 c->pc_h3_stream = stream_id;
780 c->pc_resp_sink = pc_h3_resp_sink;
785 match_and_execute(slot);
789 c->pc_resp_sink =
nullptr;
796bool PC::tls_cert(
const uint8_t *cert,
size_t cert_len,
const uint8_t *key,
size_t key_len)
798 return pc_tls_global_init(cert, cert_len, key, key_len);
801int32_t PC::listen_tls(uint16_t port)
807 _listen_ports[_listener_count] = port;
809 _listen_tls[_listener_count] =
true;
814int32_t PC::begin_tls(uint16_t port,
const uint8_t *cert,
size_t cert_len,
const uint8_t *key,
size_t key_len,
817 if (!tls_cert(cert, cert_len, key, key_len))
821 int32_t rc = listen_tls(port);
830bool PC::tls_require_client_cert(
const uint8_t *ca,
size_t ca_len)
832 return pc_tls_set_client_ca(ca, ca_len);
835int PC::tls_client_subject(uint8_t slot_id,
char *out,
size_t out_len)
837 return pc_tls_peer_subject(slot_id, out, out_len);
844 if (_listener_count == 0)
864#if PC_ENABLE_WEBSOCKET
904 Route *r = &_routes[_route_count++];
917 Route *r = &_routes[_route_count++];
936 Route *r = &_routes[_route_count++];
946 const char *pass,
bool digest)
952 Route *r = &_routes[_route_count++];
957 r->auth_required =
true;
958 r->auth_digest = digest;
968#if PC_ENABLE_WEBSOCKET
969void PC::on_ws(
const char *path, WsConnectHandler on_connect, WsMessageHandler on_message, WsCloseHandler on_close)
975 Route *r = &_routes[_route_count++];
977 r->
type = RouteType::ROUTE_WS;
978 r->ws_connect = on_connect;
979 r->ws_message = on_message;
980 r->ws_close = on_close;
985void PC::on_sse(
const char *path, SseConnectHandler on_connect)
991 Route *r = &_routes[_route_count++];
993 r->
type = RouteType::ROUTE_SSE;
994 r->pc_sse_connect = on_connect;
1000 _not_found_handler = callback;
1018 if (!origin || origin[0] ==
'\0')
1020 _cors_enabled =
false;
1021 _cors_header_buf[0] =
'\0';
1025 pc_sb_put(&sb__cors_header_buf,
"Access-Control-Allow-Origin: ");
1026 pc_sb_put(&sb__cors_header_buf, origin);
1027 pc_sb_put(&sb__cors_header_buf,
"\r\nAccess-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH, HEAD, "
1028 "OPTIONS\r\nAccess-Control-Allow-Headers: Content-Type\r\n");
1031 _cors_header_buf[0] =
'\0';
1033 _cors_enabled =
true;
1038 if (!value || value[0] ==
'\0')
1040 _cache_control_buf[0] =
'\0';
1044 pc_sb_put(&sb__cache_control_buf,
"Cache-Control: ");
1045 pc_sb_put(&sb__cache_control_buf, value);
1046 pc_sb_put(&sb__cache_control_buf,
"\r\n");
1049 _cache_control_buf[0] =
'\0';
1053#if PC_ENABLE_HTTP_DELIVERY
1054bool PC::set_cache_control_swr(uint32_t max_age_s, uint32_t swr_s)
1059 if (pc_delivery_cache_control(max_age_s, swr_s, directive,
sizeof(directive)) == 0)
1079bool PC::path_matches(
const char *route,
bool is_wildcard,
const char *req_path)
1083 return strcmp(route, req_path) == 0;
1088 return strncmp(route, req_path, prefix_len) == 0;
1103static void capture_path_param(
HttpReq *req,
const char *key,
size_t klen,
const char *val,
size_t vlen)
1114 memcpy(qp->
key, key, klen);
1115 qp->
key[klen] =
'\0';
1120 memcpy(qp->
val, val, vlen);
1121 qp->
val[vlen] =
'\0';
1124static bool match_path_params(
const char *route,
const char *path,
HttpReq *req)
1127 const char *r = route;
1128 const char *p = path;
1130 while (*r ==
'/' && *p ==
'/')
1134 const char *rseg = r;
1135 while (*r && *r !=
'/')
1139 size_t rlen = (size_t)(r - rseg);
1140 const char *pseg = p;
1141 while (*p && *p !=
'/')
1145 size_t plen = (size_t)(p - pseg);
1147 if (rlen > 0 && rseg[0] ==
':')
1153 capture_path_param(req, rseg + 1, rlen - 1, pseg, plen);
1155 else if (rlen != plen || strncmp(rseg, pseg, rlen) != 0)
1162 return (*r ==
'\0' && *p ==
'\0');
1177#if PC_ENABLE_WEBSOCKET
1178void PC::ws_dispatch_message(
const WsConn *ws)
const
1180 for (uint8_t r = 0; r < _route_count; r++)
1182 if (_routes[r].type == RouteType::ROUTE_WS && _routes[r].ws_message)
1184 _routes[r].ws_message(ws->
ws_id);
1190void PC::ws_dispatch_close(
const WsConn *ws)
const
1192 for (uint8_t r = 0; r < _route_count; r++)
1194 if (_routes[r].type == RouteType::ROUTE_WS && _routes[r].
ws_close)
1196 _routes[r].ws_close(ws->
ws_id);
1229 if (worker_id == 0 && s_inst.pc_h3_running)
1267#if PC_ENABLE_EDGE_CACHE
1270static bool (*s_edge_poll)(uint8_t slot) =
nullptr;
1271void pc_http_set_edge_poll(
bool (*fn)(uint8_t slot))
1279#if PC_ENABLE_EDGE_CACHE
1282 if (s_edge_poll && s_edge_poll(i))
1287#if PC_ENABLE_FILE_SERVING
1290 if (
s_send.file[i].active)
1303#if PC_ENABLE_WEBSOCKET
1316 while ((n = pc_tls_read(i, tbuf,
sizeof(tbuf))) > 0)
1318 for (
int k = 0; k < n; k++)
1323 ws_dispatch_message(ws);
1338 ws_dispatch_close(ws);
1351 ws_dispatch_message(ws);
1356 ws_dispatch_close(ws);
1377#if PC_ENABLE_KEEPALIVE
1390#if PC_REQUEST_TIMEOUT_MS > 0
1405 send(i, 408, PC_MIME_TEXT_PLAIN,
"Request Timeout");
1414 match_and_execute(i);
1422 send(i, 400, PC_MIME_TEXT_PLAIN,
"Bad Request");
1426 send(i, 413, PC_MIME_TEXT_PLAIN,
"Payload Too Large");
1430 send(i, 414, PC_MIME_TEXT_PLAIN,
"URI Too Long");
1450void PC::diag(uint8_t slot_id)
1452 send(slot_id, 200, PC_MIME_JSON, PC_DIAG_JSON);
1465 return strcmp(
http_pool[slot_id].method,
"HEAD") == 0;
1469static void allow_append(
char *buf,
size_t cap,
const char *m)
1471 if (!m[0] || strstr(buf, m))
1475 size_t len = strnlen(buf, cap);
1478 pc_sb sb_buf = {buf, cap, 0,
true};
1487 pc_sb sb1300 = {buf + len, cap - len, 0,
true};
1502static void send_error_close(uint8_t slot_id,
const char *status,
const char *extra_hdr,
const char *body)
1511 int blen = (int)strnlen(body, 0xFFFF);
1516 pc_sb sb_header = {header,
sizeof(header), 0,
true};
1520 pc_sb_put(&sb_header, extra_hdr ? extra_hdr :
"");
1521 pc_sb_put(&sb_header,
"Content-Type: ");
1522 pc_sb_put(&sb_header, PC_MIME_TEXT_PLAIN);
1523 pc_sb_put(&sb_header,
"\r\nContent-Length: ");
1525 pc_sb_put(&sb_header,
"\r\nConnection: close\r\n\r\n");
1549static void send_method_not_allowed(uint8_t slot_id,
const char *allow)
1552 pc_sb sb_extra = {extra,
sizeof(extra), 0,
true};
1560 send_error_close(slot_id,
"405 Method Not Allowed", extra,
"Method Not Allowed");
1563#if PC_ENABLE_AUTH_LOCKOUT
1567static pc_ip lockout_client_ip(uint8_t slot_id)
1577static void send_too_many_requests(uint8_t slot_id, uint32_t retry_after_s)
1580 pc_sb sb_extra2 = {extra,
sizeof(extra), 0,
true};
1582 pc_sb_u32(&sb_extra2, (uint32_t)((
unsigned long)retry_after_s));
1588 send_error_close(slot_id,
"429 Too Many Requests", extra,
"Too Many Requests");
1592bool PC::route_admits(
const Route *r, uint8_t slot_id,
HttpReq *req)
const
1603 : r->is_param ? match_path_params(r->path, req->path, req)
1604 : path_matches(r->path, r->is_wildcard, req->path);
1625 char tok[CSRF_TOKEN_BUF];
1626 if (pc_csrf_issue(tok,
sizeof(tok)) > 0)
1628 set_cookie(slot_id,
"csrf", tok,
"Path=/; SameSite=Strict");
1629 char body[CSRF_TOKEN_BUF + 16];
1630 pc_sb sb_body = {body,
sizeof(body), 0,
true};
1638 send(slot_id, 200, PC_MIME_JSON, body);
1642 send(slot_id, 500, PC_MIME_TEXT_PLAIN,
"CSRF unavailable");
1653 if (!tok || !pc_csrf_verify(tok))
1655 send(slot_id, 403, PC_MIME_TEXT_PLAIN,
"CSRF token missing or invalid");
1663#if PC_ENABLE_WEBSOCKET
1670 (strcasecmp(upgrade_hdr,
"websocket") == 0) &&
1674 send(slot_id, 400, PC_MIME_TEXT_PLAIN,
"WebSocket upgrade required");
1679 if (!ws_ver || strcmp(ws_ver,
"13") != 0)
1681 ws_send_version_required(slot_id);
1686 if (!ws_do_upgrade(slot_id, req, r->ws_connect))
1688 send(slot_id, 400, PC_MIME_TEXT_PLAIN,
"Bad WebSocket handshake");
1694bool PC::authorize_request(uint8_t slot_id,
HttpReq *req,
const Route *r)
1696#if PC_ENABLE_AUTH_LOCKOUT
1697 pc_ip cip = lockout_client_ip(slot_id);
1698#if PC_ENABLE_FORWARDED_TRUST
1706 pc_forwarded_effective_ip(&cip, fwd, &eff);
1710 uint32_t now = (uint32_t)millis();
1711 uint32_t remain = auth_lockout_remaining_ms(&cip, now);
1715 send_too_many_requests(slot_id, (remain + 999) / 1000);
1720 bool ok = r->auth_digest ? check_digest_auth(slot_id, req, r, &stale) : check_basic_auth(slot_id, req, r);
1721#if PC_ENABLE_AUTH_LOCKOUT
1726 auth_lockout_succeed(&cip);
1730 auth_lockout_fail(&cip, now);
1735 send_unauth(slot_id, r, stale);
1742bool PC::dispatch_matched_route(uint8_t slot_id,
HttpReq *req,
HttpMethod method,
Route *r,
bool *path_matched,
1743 char *allow_buf,
size_t allow_cap)
1745#if PC_ENABLE_WEBSOCKET
1746 if (r->
type == RouteType::ROUTE_WS)
1748 handle_ws_route(slot_id, req, method, r);
1754 if (r->
type == RouteType::ROUTE_SSE)
1756 if (!pc_sse_do_upgrade(slot_id, req, r->pc_sse_connect))
1758 send(slot_id, 503, PC_MIME_TEXT_PLAIN,
"Service Unavailable");
1764#if PC_ENABLE_FILE_SERVING
1765 if (r->
type == RouteType::ROUTE_STATIC)
1770 *path_matched =
true;
1771 allow_append(allow_buf, allow_cap,
"GET");
1772 allow_append(allow_buf, allow_cap,
"HEAD");
1775 serve_static_request(slot_id, req, r);
1786 *path_matched =
true;
1787 allow_append(allow_buf, allow_cap, method_name(r->
method));
1791 allow_append(allow_buf, allow_cap,
"HEAD");
1796 if (r->auth_required && !authorize_request(slot_id, req, r))
1805void PC::match_and_execute(uint8_t slot_id)
1812 _extra_hdr[slot_id][0] =
'\0';
1817 if (rate_limit_check(slot_id))
1821 if (run_middleware(slot_id, req))
1830 if (try_serve_dav(slot_id, req))
1844 if (pc_csrf_gate(slot_id, req, method))
1853 send(slot_id, 501, PC_MIME_TEXT_PLAIN,
"Not Implemented");
1860 send(slot_id, 501, PC_MIME_TEXT_PLAIN,
"Not Implemented");
1866 bool path_matched =
false;
1868 allow_buf[0] =
'\0';
1870 for (uint8_t i = 0; i < _route_count; i++)
1872 Route *r = &_routes[i];
1873 if (!route_admits(r, slot_id, req))
1877 if (dispatch_matched_route(slot_id, req, method, r, &path_matched, allow_buf,
sizeof(allow_buf)))
1886 send_method_not_allowed(slot_id, allow_buf);
1890 if (_not_found_handler)
1892 _not_found_handler(slot_id, req);
1896 send(slot_id, 404, PC_MIME_TEXT_PLAIN,
"Not Found");
1916void PC::send(uint8_t slot_id,
int code,
const char *content_type,
const char *payload)
1919 send(slot_id, code, content_type, (
const uint8_t *)payload, payload ? strnlen(payload, 0xFFFF) : 0);
1922void PC::send(uint8_t slot_id,
int code,
const char *content_type,
const uint8_t *body,
size_t body_len)
1928 const char *payload = (
const char *)body;
1930#if PC_ENABLE_HTTP2 || PC_ENABLE_HTTP3
1935 if (conn->pc_resp_sink)
1937 conn->pc_resp_sink(slot_id, code, content_type, payload, body_len);
1947 int payload_len = (int)(body_len > 0xFFFF ? 0xFFFF : body_len);
1950 const char *cl = pc_resp_conn_hdr(slot_id, &keep);
1953 pc_sb sb_header2 = {header,
sizeof(header), 0,
true};
1955 pc_sb_i64(&sb_header2, (int64_t)(code));
1958 pc_sb_put(&sb_header2,
"\r\nContent-Type: ");
1960 pc_sb_put(&sb_header2,
"\r\nContent-Length: ");
1961 pc_sb_i64(&sb_header2, (int64_t)(payload_len));
1964 hlen = append_resp_trailer(header,
sizeof(header), hlen, slot_id, cl);
1972 pc_resp_end(slot_id, 500, 0,
false);
1986 if (!head && payload_len > 0 && (
size_t)hlen + (
size_t)payload_len <=
sizeof(header))
1988 memcpy(header + hlen, payload, (
size_t)payload_len);
1991 else if (!head && payload_len > 0)
2001 pc_resp_end(slot_id, code, payload_len, keep,
true);
2021#if PC_ENABLE_HTTP2 || PC_ENABLE_HTTP3
2022 if (conn->pc_resp_sink)
2024 conn->pc_resp_sink(slot_id, code,
"text/plain",
"", 0);
2035 const char *cl = pc_resp_conn_hdr(slot_id, &keep);
2038 pc_sb sb_header3 = {header,
sizeof(header), 0,
true};
2040 pc_sb_i64(&sb_header3, (int64_t)(code));
2043 pc_sb_put(&sb_header3,
"\r\nContent-Length: 0\r\n");
2045 hlen = append_resp_trailer(header,
sizeof(header), hlen, slot_id, cl);
2049 pc_resp_end(slot_id, code, 0, keep,
true);
2080 const char *cl = pc_resp_conn_hdr(slot_id, &keep);
2083 pc_sb sb_header4 = {header,
sizeof(header), 0,
true};
2085 pc_sb_i64(&sb_header4, (int64_t)(code));
2088 pc_sb_put(&sb_header4,
"\r\nLocation: ");
2090 pc_sb_put(&sb_header4,
"\r\nContent-Length: 0\r\n");
2092 hlen = append_resp_trailer(header,
sizeof(header), hlen, slot_id, cl);
2096 pc_resp_end(slot_id, code, 0, keep,
true);
Per-peer brute-force lockout for HTTP auth (PC_ENABLE_AUTH_LOCKOUT).
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.
Single-port HTTP server with deterministic, zero-allocation execution.
PC()
Construct a PC with an empty routing table.
void on_request_log(RequestLogCb cb)
Install a per-request access-log callback (one hook, no buffering).
void send_empty(uint8_t slot_id, int code)
Send a headers-only HTTP response and close the connection.
void on_regex(const char *pattern, HttpMethod method, Handler callback)
Register a route whose path is a regular expression.
int32_t restart(const WebServerConfig *cfg=nullptr)
Hard-reset all connections and re-open all registered listeners.
void redirect(uint8_t slot_id, int code, const char *location)
Send an HTTP redirect (Location header, empty body) and close.
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 set_ap_ip(uint32_t ap_ip)
Tell the server the softAP IPv4 address for STA/AP route filtering.
void service_once(int worker_id=0)
Run exactly one service iteration for worker worker_id (the body driven by that worker's task,...
int32_t begin(const WebServerConfig *cfg=nullptr)
Initialize all connection slots and open all registered listeners.
void on(const char *path, HttpMethod method, Handler callback)
Register a route handler.
void stop()
Gracefully stop the server.
bool defer(uint8_t slot, pc_deferred_fn fn, void *arg) const
Run fn(arg) on the worker that owns connection slot.
void handle()
Drive the server - call every Arduino loop() iteration.
void set_cors(const char *origin)
Enable CORS by pre-building the Access-Control headers.
void set_cache_control(const char *value)
Set the Cache-Control header emitted for static files.
void on_not_found(Handler callback)
Register a fallback handler for unmatched requests.
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 http_poll_slot(uint8_t slot_id)
The instance-bound HTTP poll pump for one slot (the HTTP ProtoHandler's on_poll).
Pluggable monotonic clock for all library timing.
uint32_t pc_millis(void)
The library's monotonic time at 1000 Hz (milliseconds).
Stateless HMAC-signed CSRF token (PC_ENABLE_CSRF).
Trusted-reverse-proxy resolution of a forwarded client address (PC_ENABLE_FORWARDED_TRUST).
Base-16 conversion between raw bytes and their ASCII digits.
HTTP delivery optimizations: stale-while-revalidate, Range/206 delta fetch, SW precache (PC_ENABLE_HT...
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).
bool http_forwarded_client(const HttpReq *req, char *ip_out, size_t ip_cap, bool *is_https)
Recover the original client from a reverse-proxy Forwarded (RFC 7239) or de-facto X-Forwarded-For / X...
@ PARSE_ERROR
Unrecoverable parse failure → 400.
@ PARSE_ENTITY_TOO_LARGE
Content-Length > BODY_BUF_SIZE → 413.
@ PARSE_BODY
Reading the request body.
@ 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.
@ PC_IP_NONE
empty / unparsed
#define PC_IP_STR_MAX
Longest text an pc_ip_format can produce, including the NUL (RFC 5952 v4-mapped).
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 pc_ntp_http_date(char *out, size_t out_cap)
Format the current time as an RFC 7231 IMF-fixdate (HTTP Date).
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.
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.
const size_t PC_RESP_HDR_OVERFLOW_LEN
Length of PC_RESP_HDR_OVERFLOW, taken with sizeof where the array bound is still visible.
bool req_is_head(uint8_t slot_id)
True if the request in slot slot_id used the HEAD method (send headers, no body).
const char PC_RESP_HDR_OVERFLOW[]
The fixed reply sent when a response's own headers will not fit RESP_HDR_BUF_SIZE.
Layer 7 (Application) - public HTTP routing API.
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 PC::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.
@ PC_ERR_LISTENER_FULL
listen(): listener pool (MAX_LISTENERS) is full.
@ PC_ERR_NO_LISTENERS
begin() called before any listen() / begin(port).
@ PC_ERR_LISTEN_FAILED
A listener failed to open (bind/listen/lwIP error).
#define PC_ENABLE_HTTP3
HTTP/3 (RFC 9114) over QUIC (RFC 9000) - implemented, host-tested end-to-end (HW verification pending...
#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 PC_REQUEST_TIMEOUT_MS
Request-header read deadline in milliseconds (slow-loris defense). Default 10 s; 0 disables.
#define CACHE_CONTROL_BUF_SIZE
Size of the optional Cache-Control header line stored in PC.
#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 MAX_MIDDLEWARE
Maximum globally-registered middleware functions.
#define MAX_PATH_LEN
Maximum URL path length (including leading /).
#define PC_ENABLE_TLS
TLS (HTTPS/WSS) via mbedTLS with a static memory pool (ESP32-only).
#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 PC.
#define PC_KEEPALIVE_MAX_REQUESTS
Maximum requests served on one keep-alive connection before it is closed.
#define QUERY_KEY_LEN
Maximum query-parameter key length.
#define MAX_PATH_PARAMS
Maximum number of :name path parameters captured per route match.
pc_iface
Network interface a connection arrived on (for per-route filtering).
@ PC_IFACE_STA
Station interface (joined to an AP / your LAN).
@ PC_IFACE_ANY
Unknown / no filter (matches any interface).
Library-private declarations shared between protocore.cpp and the src/server/*.cpp request-handler tr...
bool regex_match(const char *pattern, const char *path)
Whole-path regex match (anchored both ends; bounded by RE_MAX_STEPS, fails closed)....
void server_tick(int worker_id)
Drive the session layer for one Arduino loop iteration.
SHA-1 (FIPS 180-4) - one-shot digest.
SHA-256 (FIPS 180-4) - streaming context and one-shot API.
void pc_sse_init()
Initialize all SSE pool slots to inactive.
SseConn * pc_sse_find(uint8_t slot_id)
Find the SseConn for a given TCP slot, or nullptr.
Bounded no-heap string builder that fails closed on overflow (one shared copy).
size_t pc_sb_finish(pc_sb *b)
NUL-terminate and return the built length, or 0 if the build overflowed.
void pc_sb_i64(pc_sb *b, int64_t v)
Append v as signed decimal (64-bit), with a leading '-' when negative.
void pc_sb_put(pc_sb *b, const char *s)
Append NUL-terminated s; leaves the buffer untouched and clears ok if it would not fit.
void pc_sb_u32(pc_sb *b, uint32_t v)
Append v as decimal (no leading zeros; "0" for zero).
bool active
a chunked response is in progress on this slot.
Fully-parsed HTTP/1.1 request.
Header headers[MAX_HEADERS]
Captured header fields.
char query[MAX_QUERY_LEN]
Raw query string (after ?).
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.
char method[PC_METHOD_BUF_SIZE]
HTTP method, null-terminated (OPTIONS, or WebDAV methods when enabled).
size_t body_bytes_read
Body bytes received (may exceed BODY_BUF_SIZE).
size_t query_idx
Write cursor into query[].
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).
pc_iface iface_filter
Interface gate; pc_iface::PC_IFACE_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.
struct tcp_pcb * pcb
lwIP PCB; null when slot is free.
pc_iface iface
Interface this connection arrived on; set at accept time.
pc_atomic< 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.
A v4 or v6 address in network (big-endian) byte order.
pc_ip_family family
address family tag
Bump-append target; ok latches false once an append would overflow cap.
void pc_conn_set_state(uint8_t slot, ConnState st)
The single conn_pool[slot].state write path. Writes the state (release) and keeps the free-slot bitma...
void pc_conn_abort_slot(uint8_t slot)
Hard-abort connection slot (RST) for a fatal condition. The transport owns the teardown order: free t...
TcpConn conn_pool[CONN_POOL_SLOTS]
Static pool of connection contexts. Defined in tcp.cpp. Sized CONN_POOL_SLOTS: MAX_CONNS TCP slots pl...
void pc_conn_flush(uint8_t slot)
Flush queued bytes / finish the send on slot (TLS-aware).
uint32_t pc_ap_ip
softAP IPv4 address (network byte order) for STA/AP interface tagging.
void pc_conn_ack_consumed(uint8_t slot)
Reopen the TCP receive window by however much slot has drained.
bool pc_conn_send(uint8_t slot, const void *data, u16_t len)
Send len bytes on connection slot (copies data; TLS-aware).
void pc_conn_begin_close(uint8_t slot_id)
Begin a graceful close that dwells in ConnState::CONN_CLOSING until the peer ACKs.
bool pc_conn_remote_addr(uint8_t slot, pc_ip *out)
The connected peer's address as a family-tagged pc_ip (IPv4 or IPv6).
bool pc_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.
@ CONN_ACTIVE
Live connection; PCB is valid.
@ CONN_FREE
Slot is available; no PCB is attached.
size_t pc_time_http_date(char *out, size_t out_cap)
The current best time (pc_time_now, any registered NTP / GPS / RTC / ... source) formatted as an RFC ...
Multi-source time fallback matrix (PC_ENABLE_TIME_SOURCE).
Deterministic TLS engine: mbedTLS over a static memory pool (PC_ENABLE_TLS).
Layer 7 (Application) - embedded web assets generated from web_assets/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 pc_worker_run_deferred(int worker_id)
Drain and run worker worker_id's deferred callbacks (called by the worker).
void pc_workers_start(pc_worker_pump_fn pump)
Spawn the worker task(s) and start them running pump. No-op on host.
bool pc_workers_running(void)
True while worker task(s) are running (always false on host).
bool pc_defer(int worker_id, pc_deferred_fn fn, void *arg)
Run fn(arg) on worker worker_id. Returns false if the queue is full.
void pc_workers_stop(void)
Signal the worker task(s) to exit and wait briefly for them. No-op on host.
Layer 5 (Session) - server worker identity.
void(* pc_deferred_fn)(void *arg)
Deferred callback signature.