DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
dwserver.h
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file dwserver.h
6 * @brief Layer 7 (Application) - public HTTP routing API.
7 *
8 * This is the only header most application code needs to include.
9 * The full OSI include chain is pulled in automatically:
10 * @code
11 * dwserver.h (L7 Application)
12 * ├── network_drivers/presentation/presentation.h (L6 Presentation)
13 * │ ├── network_drivers/presentation/http_parser/http_parser.h (parser types)
14 * │ └── network_drivers/transport/tcp.h (L4 Transport)
15 * │ └── ServerConfig.h (compile-time config)
16 * └── network_drivers/session/session.h (L5 Session - event drain)
17 * @endcode
18 *
19 * **Feature flags** - define any of these to 0 before including to strip
20 * the feature from the build entirely:
21 * @code
22 * #define DETWS_ENABLE_WEBSOCKET 0
23 * #define DETWS_ENABLE_SSE 0
24 * #define DETWS_ENABLE_MULTIPART 0
25 * #define DETWS_ENABLE_FILE_SERVING 0
26 * #define DETWS_ENABLE_AUTH 0
27 * #include <dwserver.h>
28 * @endcode
29 *
30 * **Determinism guarantees**
31 * - All buffers are statically allocated; no heap usage after begin().
32 * - Every operation has O(1) or O(MAX_ROUTES) worst-case time.
33 * - `handle()` is safe to call every Arduino `loop()` iteration.
34 *
35 * @author Douglas Quigg (dstroy0)
36 * @date 2026
37 * @copyright Copyright (C) 2026 Douglas Quigg (dstroy0). AGPL-3.0-or-later.
38 */
39
40#ifndef DETERMINISTICESPASYNCWEBSERVER_H
41#define DETERMINISTICESPASYNCWEBSERVER_H
42
47#if DETWS_ENABLE_WEBSOCKET
49#endif
50#if DETWS_ENABLE_SSE
52#endif
53#if DETWS_ENABLE_MULTIPART
55#endif
56#include <Arduino.h>
57#if DETWS_ENABLE_FILE_SERVING
58#ifdef ARDUINO
59#include <FS.h>
60#else
61#include "FS.h"
62#endif
63#endif
64
65// ---------------------------------------------------------------------------
66// HTTP method enumeration
67// ---------------------------------------------------------------------------
68
69/**
70 * @brief HTTP request methods supported by the router.
71 *
72 * Pass one of these values to DetWebServer::on() to bind a route to a
73 * specific method. PATCH, HEAD, and OPTIONS were added in v1.0 alongside
74 * CORS preflight support.
75 */
76enum class HttpMethod : uint8_t
77{
78 HTTP_GET, ///< Safe, idempotent read
79 HTTP_POST, ///< Non-idempotent create / action
80 HTTP_PUT, ///< Idempotent replace
81 HTTP_DELETE, ///< Idempotent delete
82 HTTP_PATCH, ///< Partial update
83 HTTP_HEAD, ///< Same as GET but no response body
84 HTTP_OPTIONS, ///< Capability query / CORS preflight
85 HTTP_METHOD_UNKNOWN ///< Unrecognized method token → 501 Not Implemented
86};
87
88// ---------------------------------------------------------------------------
89// Handler and route types
90// ---------------------------------------------------------------------------
91
92/**
93 * @brief Callback signature for HTTP request handlers.
94 *
95 * The callback receives the connection slot index and a pointer to the
96 * fully-parsed request. Call DetWebServer::send() or DetWebServer::send_empty()
97 * from inside the callback to write a response.
98 *
99 * @param slot_id Index into the connection pool (0 … MAX_CONNS-1).
100 * @param request Pointer to the parsed HTTP request. Valid only during the
101 * callback; do not cache this pointer.
102 *
103 * @note If the callback returns without calling send(), the framework will
104 * reset the slot automatically (no response is sent to the client).
105 */
106typedef void (*Handler)(uint8_t slot_id, HttpReq *request);
107
108/**
109 * @brief Resolver for `{{name}}` template placeholders used by send_template().
110 *
111 * Called with a placeholder name; returns the replacement string, or nullptr
112 * to substitute an empty string. The pointer must stay valid for the duration
113 * of the send_template() call, and the resolver must be deterministic (it is
114 * invoked twice: once to size the body, once to emit it).
115 */
116typedef const char *(*TemplateVar)(const char *name);
117
118/**
119 * @brief Per-request access-log callback (see DetWebServer::on_request_log()).
120 *
121 * Invoked once per response with the request method/path, the HTTP status code,
122 * and the response body length in bytes. The strings are valid only for the
123 * duration of the call. This is a thin hook - the library does no buffering or
124 * formatting; route the data to Serial, syslog, etc. as you see fit.
125 */
126typedef void (*RequestLogCb)(const char *method, const char *path, int status, int body_len);
127
128/**
129 * @brief Outcome of a middleware function (see @ref Middleware).
130 *
131 * Returning MwResult::MW_NEXT passes the request to the next middleware in the chain and,
132 * once the chain is exhausted, on to the matching route handler. Returning
133 * MwResult::MW_HALT stops the chain: the route handler is NOT invoked, so a middleware
134 * that halts must have already written a response (the dispatcher treats the
135 * request as fully handled).
136 */
137enum class MwResult : uint8_t
138{
139 MW_NEXT = 0, ///< Continue to the next middleware / the route handler.
140 MW_HALT = 1 ///< Stop dispatch; the middleware already sent a response.
141};
142
143/**
144 * @brief Composable pre-dispatch middleware (see DetWebServer::use()).
145 *
146 * Each registered middleware runs - in registration order - on every request
147 * before route matching, receiving the same `(slot_id, request)` pair a handler
148 * does. A middleware may inspect the request, queue response headers
149 * (DetWebServer::add_response_header()), short-circuit by sending a response and
150 * returning MwResult::MW_HALT, or fall through with MwResult::MW_NEXT. Middlewares reference the
151 * application's server instance the same way handlers do (the global object), so
152 * they can call send() / send_empty() to short-circuit.
153 *
154 * @param slot_id Connection slot index (0 … MAX_CONNS-1).
155 * @param request Parsed request; valid only during the call (do not cache).
156 * @return MwResult::MW_NEXT to continue, MwResult::MW_HALT to stop (response already sent).
157 */
158typedef MwResult (*Middleware)(uint8_t slot_id, HttpReq *request);
159
160#if DETWS_ENABLE_WEBSOCKET
161/**
162 * @brief Callback fired when a WebSocket connection is established.
163 *
164 * @param ws_id Index into ws_pool[] for this connection.
165 */
166typedef void (*WsConnectHandler)(uint8_t ws_id);
167
168/**
169 * @brief Callback fired when a WebSocket text or binary frame arrives.
170 *
171 * The payload is in ws_pool[ws_id].buf, null-terminated. Length is in
172 * ws_pool[ws_id].payload_len. Opcode is in ws_pool[ws_id].opcode.
173 *
174 * @param ws_id Index into ws_pool[].
175 */
176typedef void (*WsMessageHandler)(uint8_t ws_id);
177
178/**
179 * @brief Callback fired when a WebSocket connection closes.
180 *
181 * @param ws_id Index into ws_pool[] (slot is still valid during callback).
182 */
183typedef void (*WsCloseHandler)(uint8_t ws_id);
184#endif // DETWS_ENABLE_WEBSOCKET
185
186#if DETWS_ENABLE_SSE
187/**
188 * @brief Callback fired when a new SSE client connects.
189 *
190 * Use sse_send() inside this callback to push an initial event if needed.
191 *
192 * @param sse_id Index into sse_pool[] for this connection.
193 */
194typedef void (*SseConnectHandler)(uint8_t sse_id);
195#endif // DETWS_ENABLE_SSE
196
197// ---------------------------------------------------------------------------
198// Route type discriminator
199// ---------------------------------------------------------------------------
200
201/** @brief Discriminates between HTTP, WebSocket, and SSE route entries. */
202enum class RouteType : uint8_t
203{
204 ROUTE_HTTP, ///< Standard HTTP request/response.
205#if DETWS_ENABLE_WEBSOCKET
206 ROUTE_WS, ///< WebSocket upgrade route.
207#endif
208#if DETWS_ENABLE_SSE
209 ROUTE_SSE, ///< Server-Sent Events route.
210#endif
211#if DETWS_ENABLE_FILE_SERVING
212 ROUTE_STATIC, ///< Static-file subtree mount (serve_static()).
213#endif
214#if DETWS_ENABLE_WEBDAV
215 ROUTE_DAV, ///< WebDAV subtree mount (dav()).
216#endif
217};
218
219// ---------------------------------------------------------------------------
220// begin() / listen() / restart() result codes
221// ---------------------------------------------------------------------------
222
223/**
224 * @brief Result codes for listen(), begin(), and restart().
225 *
226 * Success is a positive value (DetWebServerResult::DETWS_OK). Failures are distinct negative codes
227 * so a caller can tell why startup failed.
228 */
229enum class DetWebServerResult : int32_t
230{
231 DETWS_OK = 1, ///< Success.
232 DETWS_ERR_NO_LISTENERS = -1, ///< begin() called before any listen() / begin(port).
233 DETWS_ERR_LISTENER_FULL = -2, ///< listen(): listener pool (MAX_LISTENERS) is full.
234 DETWS_ERR_LISTEN_FAILED = -3 ///< A listener failed to open (bind/listen/lwIP error).
235};
236
237/**
238 * @brief Internal route entry stored in the routing table.
239 *
240 * Populated by DetWebServer::on(), on_ws(), or on_sse().
241 * Application code does not interact with this struct directly.
242 */
243struct Route
244{
245 char path[MAX_PATH_LEN]; ///< Null-terminated path pattern.
246 RouteType type; ///< HTTP, WS, or SSE.
247 HttpMethod method; ///< HTTP method (RouteType::ROUTE_HTTP only).
248 Handler callback; ///< HTTP handler (RouteType::ROUTE_HTTP only).
249
250#if DETWS_ENABLE_WEBSOCKET
251 WsConnectHandler ws_connect; ///< Fired on upgrade success.
252 WsMessageHandler ws_message; ///< Fired on each data frame.
253 WsCloseHandler ws_close; ///< Fired on close.
254#endif
255
256#if DETWS_ENABLE_SSE
257 SseConnectHandler sse_connect; ///< Fired when client subscribes.
258#endif
259
260#if DETWS_ENABLE_FILE_SERVING
261 fs::FS *static_fs; ///< Filesystem for RouteType::ROUTE_STATIC (else nullptr).
262 const char *static_root; ///< FS root prefix for RouteType::ROUTE_STATIC (must be a persistent string).
263#endif
264
265#if DETWS_ENABLE_AUTH
266 bool auth_required; ///< True when this route requires authentication.
267 bool auth_digest; ///< True for Digest auth; false for Basic.
268 char auth_realm[MAX_AUTH_LEN]; ///< WWW-Authenticate realm string.
269 char auth_user[MAX_AUTH_LEN]; ///< Required username.
270 char auth_pass[MAX_AUTH_LEN]; ///< Required password.
271#endif
272
273 bool is_active; ///< `false` for unused table slots.
274 bool is_wildcard; ///< `true` when path ends with `*`.
275 bool is_param; ///< `true` when the path contains a `:name` segment.
276 bool is_regex; ///< `true` when the path is a regex (see on_regex()).
277 DetIface iface_filter; ///< Interface gate; DetIface::DETIFACE_ANY (0) = match any interface.
278};
279
280// ---------------------------------------------------------------------------
281// Chunked (streaming) response writer
282// ---------------------------------------------------------------------------
283
284struct tcp_pcb; // forward decl (full type pulled in via the transport layer)
285
286class DetWebServer;
287
288/**
289 * @brief Source callback that produces a chunked response body incrementally.
290 *
291 * Passed to DetWebServer::send_chunked() and called repeatedly - possibly across
292 * several server loops, as the TCP send window drains - until it returns 0. Each
293 * call writes up to @p cap bytes of the next body piece into @p buf and returns
294 * the count; the HTTP chunk framing (size line + CRLFs + terminator) is added by
295 * the server. Track your position across calls in @p ctx. This pull/generator
296 * model lets the server page an arbitrarily large body to the socket in constant
297 * memory without ever blocking the worker or truncating at the send window.
298 *
299 * @warning @p ctx must stay valid until the body is fully sent. A body that fits
300 * in a single send window finishes during the send_chunked() call, but a larger
301 * one resumes on later loops, so @p ctx must NOT point at the caller's stack: use
302 * static / global storage (a per-connection instance if requests can overlap), or
303 * generate the body from durable state.
304 *
305 * @param buf destination for the next body bytes.
306 * @param cap maximum bytes to write into @p buf on this call.
307 * @param ctx caller state pointer, passed through from send_chunked().
308 * @return bytes written into @p buf (<= @p cap), or 0 to end the body.
309 */
310typedef size_t (*ChunkSource)(uint8_t *buf, size_t cap, void *ctx);
311
312// ---------------------------------------------------------------------------
313// DetWebServer - the main application class
314// ---------------------------------------------------------------------------
315
316/**
317 * @class DetWebServer
318 * @brief Single-port HTTP server with deterministic, zero-allocation execution.
319 *
320 * ## Typical usage
321 * @code
322 * DetWebServer server;
323 *
324 * void handle_api(uint8_t slot_id, HttpReq *req) {
325 * server.send(slot_id, 200, "application/json", "{\"ok\":true}");
326 * }
327 *
328 * void setup() {
329 * WiFi.begin("SSID", "PASSWORD");
330 * server.on("/api/status", HttpMethod::HTTP_GET, handle_api);
331 * server.set_cors("*");
332 * int32_t result = server.begin(80);
333 * if (result < 0) { } // DetWebServerResult code: startup failed
334 * }
335 *
336 * void loop() {
337 * server.handle(); // call every iteration - O(MAX_CONNS) per call
338 * }
339 * @endcode
340 *
341 * ## Design constraints
342 * - Maximum simultaneous connections: `MAX_CONNS` (default 4).
343 * - Maximum registered routes: `MAX_ROUTES` (default 16).
344 * - Responses are sent synchronously and the TCP connection is closed
345 * immediately after every response (HTTP/1.0 close semantics).
346 */
348{
349 private:
350 Route _routes[MAX_ROUTES]; ///< Flat routing table; searched linearly.
351 uint8_t _route_count; ///< Number of active entries in _routes.
352
353 Handler _not_found_handler; ///< Called when no route matches; may be null.
354 bool _cors_enabled; ///< True after a non-empty set_cors() call.
355 RequestLogCb _log_cb; ///< Per-request access-log hook; may be null.
356
357#if DETWS_ENABLE_STATS
358 uint32_t _stat_requests; ///< Total responses sent.
359 uint32_t _stat_2xx; ///< Responses with a 2xx status.
360 uint32_t _stat_4xx; ///< Responses with a 4xx status.
361 uint32_t _stat_5xx; ///< Responses with a 5xx status.
362#endif
363
364 uint16_t _listen_ports[MAX_LISTENERS]; ///< Ports registered via listen() or begin(port).
365 ConnProto _listen_protos[MAX_LISTENERS]; ///< Protocol for each registered listener.
366 bool _listen_tls[MAX_LISTENERS]; ///< True for TLS listeners (listen_tls()).
367 uint8_t _listener_count; ///< Number of registered listeners.
368
369 /**
370 * @brief Pre-built CORS header block injected into every response.
371 *
372 * Built once by set_cors() to avoid repeated snprintf at dispatch time.
373 */
374 char _cors_header_buf[CORS_HDR_BUF_SIZE];
375
376 /**
377 * @brief Pre-built `Cache-Control: <value>\r\n` line, or "" when unset.
378 *
379 * Set by set_cache_control(); injected into serve_file() / serve_static()
380 * responses beside the ETag. Empty by default (no header emitted).
381 */
382 char _cache_control_buf[CACHE_CONTROL_BUF_SIZE];
383
384 /**
385 * @brief Per-slot buffer for app-supplied custom response headers/cookies.
386 *
387 * Filled via add_response_header() / set_cookie() during a handler and
388 * injected into send() / send_empty() / redirect() just like the CORS
389 * block. Cleared at the start of every dispatch so each request begins
390 * with no carried-over headers.
391 */
392 char _extra_hdr[CONN_POOL_SLOTS][EXTRA_HDR_BUF_SIZE];
393
394#if DETWS_ENABLE_HTTP3
395 // HTTP/3 leaf cert + seed and UDP port, held until begin() starts the QUIC server.
396 const uint8_t *_h3_cert = nullptr;
397 size_t _h3_cert_len = 0;
398 uint8_t _h3_seed[32] = {0};
399 uint16_t _h3_port = DETWS_HTTP3_PORT;
400 bool _h3_enabled = false;
401#endif
402
403 /**
404 * @brief Global middleware chain, run in registration order before dispatch.
405 *
406 * Populated by use(); a middleware returning MwResult::MW_HALT short-circuits the
407 * request. An empty chain (the default) adds no per-request work.
408 */
409 Middleware _middleware[MAX_MIDDLEWARE];
410 uint8_t _middleware_count; ///< Number of active entries in _middleware.
411
412 // --- Built-in rate-limit pre-filter (fixed-window counter) ----------------
413 uint16_t _rl_max; ///< Max requests per window; 0 = rate limiting off.
414 uint32_t _rl_window_ms; ///< Window length in milliseconds.
415 uint32_t _rl_window_start; ///< millis() at the start of the current window.
416 uint16_t _rl_count; ///< Requests counted in the current window.
417
418 /**
419 * @brief Run the global middleware chain for a request.
420 * @return true if a middleware returned MwResult::MW_HALT (a response was sent and
421 * dispatch must stop); false to continue to route matching.
422 */
423 bool run_middleware(uint8_t slot_id, HttpReq *req);
424
425 /**
426 * @brief Built-in fixed-window rate-limit check (see enable_rate_limit()).
427 * @return true if the request was rejected with 429 (response sent, dispatch
428 * must stop); false when rate limiting is disabled or within budget.
429 */
430 bool rate_limit_check(uint8_t slot_id);
431
432 /**
433 * @brief Evaluate whether a route pattern matches a request path.
434 *
435 * Wildcard routes end with `*`; the `*` is replaced by a prefix match.
436 * Exact routes use strcmp.
437 *
438 * @param route Null-terminated route pattern.
439 * @param is_wildcard True if route ends with `*`.
440 * @param req_path Null-terminated path from the parsed request.
441 * @return True if the route matches the request path.
442 */
443 static bool path_matches(const char *route, bool is_wildcard, const char *req_path);
444
445 /// @brief Record a response for stats + the access-log hook. Reads method/path from http_pool[slot_id].
446 void note_response(uint8_t slot_id, int code, int body_len);
447
448#if DETWS_ENABLE_KEEPALIVE
449 /**
450 * @brief Decide whether the current response should keep the connection alive.
451 *
452 * Only a cleanly-parsed request (ParseState::PARSE_COMPLETE) is eligible: HTTP/1.1 keeps
453 * alive unless the client sent `Connection: close`; HTTP/1.0 keeps alive only
454 * with `Connection: keep-alive`. On a true return the slot's request tally is
455 * incremented; the DETWS_KEEPALIVE_MAX_REQUESTS-th request returns false so
456 * the connection is closed deliberately. Always false with keep-alive off.
457 */
458 bool keepalive_eval(uint8_t slot_id);
459#endif
460
461 /**
462 * @brief Finish a response: flush, then close the connection (close path) or
463 * recycle the slot for the next request (keep-alive). Records the
464 * response and resets the HTTP parser either way. Addresses the
465 * connection by slot alone; the transport resolves the pcb internally.
466 *
467 * @param pre_flushed the caller already emitted the final bytes with det_conn_send_flush()
468 * (write+tcp_output coalesced into one marshal), so skip the redundant flush here.
469 */
470 void resp_end(uint8_t slot_id, int code, int body_len, bool keep, bool pre_flushed = false);
471
472 /**
473 * @brief Resolve the Connection response header and report keep-alive intent.
474 *
475 * One owner for the keep-alive decision: returns "Connection: keep-alive\r\n"
476 * or "Connection: close\r\n" and, via @p keep_out, whether the slot is kept
477 * alive. Always reports close when keep-alive is compiled out.
478 */
479 const char *resp_conn_hdr(uint8_t slot_id, bool *keep_out);
480
481 /**
482 * @brief Append the shared response trailer (CORS block, custom headers, the
483 * Connection header, and the terminating blank line) to a header buffer
484 * already holding the status line and per-response headers. @p hlen is
485 * the current length; returns the new total length.
486 */
487 int append_resp_trailer(char *buf, size_t cap, int hlen, uint8_t slot_id, const char *cl);
488
489 /// @brief Resume a pending chunked response: pull + frame chunks until the send window is full, finish when
490 /// drained.
491 void chunk_send_pump(uint8_t slot_id);
492
493#if DETWS_ENABLE_AUTH
494 /// @brief Validate the request's HTTP Basic credentials against route @p r. @return true if authorized.
495 static bool check_basic_auth(uint8_t slot_id, HttpReq *req, const Route *r);
496 /// @brief Validate an `Authorization: Digest` (RFC 7616, SHA-256, qop=auth) request against route @p r.
497 /// @param stale set true when the credentials verify but the nonce has expired (RFC 7616 3.3): the
498 /// caller reissues a fresh challenge with `stale=true` so the client retries without a
499 /// re-prompt. Left untouched on a credential mismatch or forged nonce.
500 bool check_digest_auth(uint8_t slot_id, HttpReq *req, const Route *r, bool *stale);
501 /// @brief Send 401 Unauthorized with a Basic or Digest `WWW-Authenticate` challenge per route @p r.
502 /// @param stale emit `stale=true` in the Digest challenge (expired-nonce transparent retry).
503 void send_unauth(uint8_t slot_id, const Route *r, bool stale = false);
504 /// @brief Per-server Digest keying secret (random at begin()); keys the stateless timestamped nonce.
505 uint8_t _digest_secret[16];
506 /// @brief (Re)seed the Digest keying secret from the CSPRNG.
507 void regen_digest_secret();
508 /// @brief Mint a fresh stateless nonce (issue time + keyed MAC) into @p out (needs cap >= 48).
509 void make_digest_nonce(char *out, size_t cap);
510 /// @brief Verify a client nonce's MAC and freshness. @return true if the MAC is authentic (issued by
511 /// this server); sets @p *expired when the nonce is authentic but older than its lifetime.
512 bool verify_digest_nonce(const char *nonce, bool *expired);
513#endif
514
515#if DETWS_ENABLE_FILE_SERVING
516 /// @brief Dispatch a RouteType::ROUTE_STATIC match: resolve the FS path and serve it (MIME/index/gzip).
517 void serve_static_request(uint8_t slot_id, HttpReq *req, const Route *r);
518 /// @brief Open @p fs_path and stream it as 200 with the given type and optional Content-Encoding.
519 void serve_file_internal(uint8_t slot_id, bool head, fs::FS &file_sys, const char *fs_path,
520 const char *content_type, const char *content_encoding);
521 /// @brief Resume a pending file response: page out one send-buffer window, finishing when drained.
522 void file_send_pump(uint8_t slot_id);
523#endif
524
525#if DETWS_ENABLE_WEBDAV
526 /// @brief If @p req matches a RouteType::ROUTE_DAV mount, handle it as WebDAV and return true.
527 bool try_serve_dav(uint8_t slot_id, HttpReq *req);
528 /// @brief Dispatch a WebDAV request against the mount @p r (resolves the FS path, then the method).
529 void serve_dav_request(uint8_t slot_id, HttpReq *req, const Route *r);
530 /// @brief Send a bodyless WebDAV status with optional extra header lines (each ending in CRLF).
531 void dav_send_status(uint8_t slot_id, int code, const char *extra_headers);
532#if DETWS_ENABLE_STREAM_BODY
533 /// @brief Stream-begin hook: if @p req is a PUT under a DAV mount, open the file and stream the body.
534 bool dav_stream_put_begin(HttpReq *req);
535 /// @brief Stream-data hook: write one body chunk to @p req's slot's DAV PUT file.
536 void dav_stream_put_data(HttpReq *req, const uint8_t *data, size_t len);
537 /// @brief C-callable trampolines (the parser hook takes plain function pointers).
538 static bool dav_put_begin_tramp(HttpReq *req);
539 static void dav_put_data_tramp(HttpReq *req, const uint8_t *data, size_t len);
540 /// @brief Stream-abort hook: close the half-written PUT file if the transfer is torn down early.
541 static void dav_put_abort_tramp(HttpReq *req);
542#endif
543#endif
544
545 /**
546 * @brief Look up and invoke the first matching route for the given slot.
547 *
548 * If CORS is enabled and the method is OPTIONS, the preflight is
549 * short-circuited here with a 204 response. If no route matches, the
550 * not-found handler is invoked (or a default 404 is sent).
551 *
552 * @param slot_id Connection slot to dispatch.
553 */
554 void match_and_execute(uint8_t slot_id);
555
556 /// @brief Route-selection predicate: true if route @p r is active, its path pattern matches
557 /// @p req, and its interface filter admits this slot's connection. Matching a param route
558 /// captures its path parameters into @p req as a side effect (as the inline match did).
559 bool route_admits(const Route *r, uint8_t slot_id, HttpReq *req) const;
560
561 /// @brief Dispatch a route whose path + interface already matched (WS/SSE/STATIC/HTTP + auth).
562 /// @return true when a response was sent (the caller stops); false to keep scanning later routes,
563 /// with @p path_matched / @p allow_buf updated for a possible 405.
564 bool dispatch_matched_route(uint8_t slot_id, HttpReq *req, HttpMethod method, Route *r, bool *path_matched,
565 char *allow_buf, size_t allow_cap);
566
567#if DETWS_ENABLE_CSRF
568 /// @brief Built-in CSRF gate: serve the `GET /csrf` token endpoint and enforce a valid
569 /// `X-CSRF-Token` on every state-changing method. @return true if a response was sent.
570 bool csrf_gate(uint8_t slot_id, HttpReq *req, HttpMethod method);
571#endif
572
573#if DETWS_ENABLE_WEBSOCKET
574 /// @brief Complete (or reject) a RouteType::ROUTE_WS handshake per RFC 6455 §4.2.1. Always responds.
575 void handle_ws_route(uint8_t slot_id, HttpReq *req, HttpMethod method, const Route *r);
576#endif
577
578#if DETWS_ENABLE_AUTH
579 /// @brief Enforce route @p r's auth (lockout gate + Digest/Basic credential check, with lockout
580 /// accounting). @return true if authorized; on failure the 401/429 is already sent.
581 bool authorize_request(uint8_t slot_id, HttpReq *req, const Route *r);
582#endif
583
584#if DETWS_ENABLE_WEBSOCKET
585 /// @brief Invoke the registered WS message handler for a completed frame on @p ws.
586 void ws_dispatch_message(WsConn *ws);
587 /// @brief Invoke the registered WS close handler for @p ws.
588 void ws_dispatch_close(WsConn *ws);
589#endif
590
591 public:
592 /**
593 * @brief Construct a DetWebServer with an empty routing table.
594 *
595 * All route slots are marked inactive. CORS is disabled. The
596 * not-found handler is null (falls back to built-in 404 response).
597 */
598 DetWebServer();
599
600 /**
601 * @brief Register a port to listen on when begin() is called.
602 *
603 * Call this before begin() for each port you want the server to accept
604 * connections on. The @p proto argument tells the session layer which
605 * protocol handler to invoke for events on this port.
606 *
607 * For the common single-HTTP-port case, prefer `begin(80)` which calls
608 * this internally. Use the explicit listen() + begin() form when you
609 * need multiple ports (e.g., HTTP on 80 and Telnet on 23).
610 *
611 * @code
612 * server.listen(80, ConnProto::PROTO_HTTP);
613 * server.listen(23, ConnProto::PROTO_TELNET);
614 * server.begin();
615 * @endcode
616 *
617 * @param port TCP port to open.
618 * @param proto Application protocol; defaults to ConnProto::PROTO_HTTP.
619 * @return the listener id (a non-negative index) on success - pass it to
620 * det_relay_publish() / ssh_forward_begin(); DetWebServerResult::DETWS_ERR_LISTENER_FULL if the pool is
621 * full.
622 */
623 int32_t listen(uint16_t port, ConnProto proto = ConnProto::PROTO_HTTP);
624
625 /**
626 * @brief Initialize all connection slots and open all registered listeners.
627 *
628 * Resets the HTTP parser pool, calls DeterministicAsyncTCP::pool_init(),
629 * then calls listener_add() for each port registered via listen().
630 * Requires at least one prior listen() call. For the common single-port
631 * case use begin(port, cfg) instead.
632 *
633 * @param cfg Optional runtime configuration. Pass nullptr for defaults.
634 * @return DetWebServerResult::DETWS_OK on success; DetWebServerResult::DETWS_ERR_NO_LISTENERS if no ports were
635 * registered; DetWebServerResult::DETWS_ERR_LISTEN_FAILED if a listener could not open.
636 */
637 int32_t begin(const WebServerConfig *cfg = nullptr);
638
639 /**
640 * @brief Convenience overload: register @p port as HTTP and start listening.
641 *
642 * Equivalent to `listen(port); begin(cfg);`. Preserved for backward
643 * compatibility with single-port sketches.
644 *
645 * @param port TCP port to listen on (typically 80).
646 * @param cfg Optional runtime configuration. Pass nullptr for defaults.
647 * @return DetWebServerResult::DETWS_OK on success; a negative DetWebServerResult on failure.
648 */
649 int32_t begin(uint16_t port, const WebServerConfig *cfg = nullptr);
650
651#if DETWS_ENABLE_TLS
652 /**
653 * @brief Load the TLS server certificate + private key (call before begin).
654 *
655 * Initializes the static-pool mbedTLS engine. Required before any TLS
656 * listener will complete a handshake. PEM buffers must include the trailing
657 * NUL in the length; DER is also accepted.
658 *
659 * @return true on success; false if the cert/key/pool setup failed.
660 */
661 bool tls_cert(const uint8_t *cert, size_t cert_len, const uint8_t *key, size_t key_len);
662
663 /**
664 * @brief Register a TLS (HTTPS) HTTP listener on @p port (typically 443).
665 *
666 * Like listen() but connections accepted here run a TLS handshake first.
667 * Call tls_cert() first, then begin(). @return DetWebServerResult::DETWS_OK or an error code.
668 */
669 int32_t listen_tls(uint16_t port);
670
671 /**
672 * @brief Convenience: load cert/key, register a TLS listener, and start.
673 *
674 * Equivalent to `tls_cert(...); listen_tls(port); begin(cfg);`.
675 *
676 * @param port TLS port (typically 443).
677 * @param cert Server certificate (chain).
678 * @param cert_len Length incl. trailing NUL for PEM.
679 * @param key Server private key.
680 * @param key_len Length incl. trailing NUL for PEM.
681 * @param cfg Optional runtime config.
682 * @return DetWebServerResult::DETWS_OK on success; a negative code, or DetWebServerResult::DETWS_ERR_LISTEN_FAILED
683 * if the TLS engine could not initialize.
684 */
685 int32_t begin_tls(uint16_t port, const uint8_t *cert, size_t cert_len, const uint8_t *key, size_t key_len,
686 const WebServerConfig *cfg = nullptr);
687
688#if DETWS_ENABLE_MTLS
689 /**
690 * @brief Require a verified client certificate (mTLS).
691 *
692 * Call after tls_cert() (or begin_tls()) and before connections arrive. Sets
693 * @p ca as the trust anchor and switches the handshake to require a client
694 * certificate chaining to it; a client that presents none, or an untrusted
695 * one, is rejected during the handshake.
696 *
697 * @param ca CA certificate (chain).
698 * @param ca_len Length incl. trailing NUL for PEM.
699 * @return true on success; false if the engine is not ready or the CA failed
700 * to parse.
701 */
702 bool tls_require_client_cert(const uint8_t *ca, size_t ca_len);
703
704 /**
705 * @brief Copy the connecting client's verified certificate subject DN.
706 *
707 * Use inside a handler to identify the mTLS peer (e.g. for authorization or
708 * logging). Valid only on a TLS connection whose handshake required and
709 * verified a client cert.
710 *
711 * @param slot_id Connection slot (the handler's id).
712 * @param out Destination buffer (always NUL-terminated on success).
713 * @param out_len Capacity of @p out.
714 * @return subject length written, or <0 if there is no verified client cert.
715 */
716 int tls_client_subject(uint8_t slot_id, char *out, size_t out_len);
717#endif // DETWS_ENABLE_MTLS
718#endif // DETWS_ENABLE_TLS
719
720#if DETWS_ENABLE_HTTP3
721 /**
722 * @brief Enable the HTTP/3 (QUIC) server: load its Ed25519 leaf certificate + key and choose the
723 * UDP port. Call before begin(); begin() then binds the port and serves HTTP/3 through the same
724 * routes as HTTP/1.1 and HTTP/2. @p cert_der is a DER X.509 leaf whose public key is the Ed25519
725 * key matching @p ed25519_seed (its 32-byte private seed). @return true if stored.
726 *
727 * Profile: TLS_AES_128_GCM_SHA256 + X25519 + Ed25519 (a client offering none of these is refused).
728 */
729 bool h3_cert(const uint8_t *cert_der, size_t cert_len, const uint8_t ed25519_seed[32],
730 uint16_t port = DETWS_HTTP3_PORT);
731
732 /**
733 * @brief Internal: run a completed HTTP/3 request through the shared route dispatcher on the
734 * reserved conn-pool slot (called by the quic_server request trampoline, not by app code). The
735 * response routes back to @p stream_id on @p conn_id via send() -> quic_server_respond.
736 */
737 void dispatch_h3_request(uint32_t conn_id, uint64_t stream_id, const char *method, const char *path,
738 const char *authority, const uint8_t *body, size_t body_len);
739#endif // DETWS_ENABLE_HTTP3
740
741 /**
742 * @brief Gracefully stop the server.
743 *
744 * Aborts all active connections, closes the listener, frees the event
745 * queue, and resets all HTTP parser slots. The WiFi and TCP/IP stack
746 * remain active. Call begin() or restart() to bring the server back up.
747 */
748 void stop();
749
750 /**
751 * @brief Hard-reset all connections and re-open all registered listeners.
752 *
753 * Equivalent to stop() followed by begin(cfg) using the ports and protocols
754 * registered via listen() (or the port passed to begin(port)). The WiFi
755 * and TCP/IP stack are not touched.
756 *
757 * Calling restart() before any listen() / begin(port) has no effect and
758 * returns -1.
759 *
760 * @param cfg Optional new runtime configuration. Pass nullptr to reuse
761 * the compile-time default (CONN_TIMEOUT_MS).
762 */
763 int32_t restart(const WebServerConfig *cfg = nullptr);
764
765 /**
766 * @brief Register a route handler.
767 *
768 * Routes are matched in registration order (first match wins).
769 * A trailing `*` in @p path enables prefix matching: `"/api/"` followed by `*`
770 * matches `"/api/users"`, `"/api/devices"`, etc.
771 *
772 * @param path URL path pattern, e.g. `"/api/status"`, or a prefix ending in a `*` wildcard.
773 * Must be ≤ `MAX_PATH_LEN - 1` characters.
774 * @param method HTTP method this route accepts.
775 * @param callback Function called when this route is matched.
776 *
777 * @note Registering more than MAX_ROUTES routes silently drops extras.
778 */
779 void on(const char *path, HttpMethod method, Handler callback);
780
781 /**
782 * @brief Register a route that only matches on a specific network interface.
783 *
784 * Identical to on(path, method, callback) but the route is invisible unless
785 * the request arrived on @p iface (DetIface::DETIFACE_STA or DetIface::DETIFACE_AP). A
786 * non-matching interface falls through to other routes / 404, so you can,
787 * e.g., expose a provisioning UI only on the softAP and the app API only on
788 * the station link. Requires set_ap_ip() to have been called so connections
789 * can be classified.
790 *
791 * @param path URL path pattern.
792 * @param method HTTP method.
793 * @param callback Handler invoked on a match.
794 * @param iface DetIface::DETIFACE_STA or DetIface::DETIFACE_AP (DetIface::DETIFACE_ANY = no filter).
795 */
796 void on(const char *path, HttpMethod method, Handler callback, DetIface iface);
797
798 /**
799 * @brief Register a route whose path is a regular expression.
800 *
801 * The whole request path must match @p pattern (implicitly anchored). The
802 * matcher is a small, bounded, allocation-free backtracker supporting:
803 * `.` (any char), `*` `+` `?` quantifiers, character classes `[...]` /
804 * `[^...]` with `a-z` ranges, the shorthands `\d \w \s` (and `\D \W \S`),
805 * and `\` to escape a metacharacter. It is **non-capturing** and has no
806 * groups `()` or alternation `|` - use `:name` path parameters (see the
807 * other on() overload notes / http_get_param) when you need to capture.
808 * Matching is bounded by RE_MAX_STEPS and fails closed past that budget.
809 *
810 * @code
811 * server.on_regex("/sensor/[0-9]+", HttpMethod::HTTP_GET, handle_sensor);
812 * server.on_regex("/img/.+\\.png", HttpMethod::HTTP_GET, handle_png);
813 * @endcode
814 *
815 * @param pattern Regex the full path must match (stored, <= MAX_PATH_LEN-1).
816 * @param method HTTP method.
817 * @param callback Handler invoked on a match.
818 */
819 void on_regex(const char *pattern, HttpMethod method, Handler callback);
820
821 /**
822 * @brief Tell the server the softAP IPv4 address for STA/AP route filtering.
823 *
824 * Each accepted connection is tagged DetIface::DETIFACE_AP when its local IP equals
825 * @p ap_ip, else DetIface::DETIFACE_STA. Call once after starting the softAP, e.g.
826 * `server.set_ap_ip(WiFi.softAPIP())` (IPAddress converts to uint32_t).
827 * Without it, every connection is treated as DetIface::DETIFACE_STA.
828 *
829 * @param ap_ip softAP IPv4 address in network byte order (0 to clear).
830 */
831 void set_ap_ip(uint32_t ap_ip);
832
833#if DETWS_ENABLE_AUTH
834 /**
835 * @brief Register a route handler protected by HTTP authentication.
836 *
837 * If the request does not include valid credentials, the library sends
838 * `401 Unauthorized` with the appropriate `WWW-Authenticate` challenge
839 * (`Basic`, or `Digest` with SHA-256 + `qop=auth` per RFC 7616) and the
840 * callback is not invoked.
841 *
842 * @param path URL path pattern.
843 * @param method HTTP method.
844 * @param callback Handler invoked only on successful authentication.
845 * @param realm WWW-Authenticate realm displayed by the browser.
846 * @param user Required username.
847 * @param pass Required password.
848 * @param digest When true, use Digest authentication instead of Basic.
849 */
850 void on(const char *path, HttpMethod method, Handler callback, const char *realm, const char *user,
851 const char *pass, bool digest = false);
852#endif // DETWS_ENABLE_AUTH
853
854#if DETWS_ENABLE_FILE_SERVING
855 /**
856 * @brief Serve a file from any Arduino-compatible filesystem.
857 *
858 * Opens @p fs_path on @p file_sys, sends HTTP 200 with the appropriate
859 * headers (Content-Type, Content-Length), and streams the file body in
860 * FILE_CHUNK_SIZE chunks via tcp_write(). Sends 404 if the file cannot
861 * be opened.
862 *
863 * @param slot_id Connection slot index.
864 * @param file_sys Filesystem reference (e.g. SPIFFS, LittleFS).
865 * @param fs_path Path to the file on the filesystem.
866 * @param content_type MIME type string, e.g. "text/html".
867 */
868 void serve_file(uint8_t slot_id, fs::FS &file_sys, const char *fs_path, const char *content_type);
869
870 /**
871 * @brief Mount a filesystem subtree at a URL prefix (one-call static serving).
872 *
873 * Registers a wildcard route so every request under @p url_prefix is served
874 * from @p fs_root on @p file_sys. The request path beyond the prefix is
875 * appended to @p fs_root; a request ending in `/` (or exactly the prefix)
876 * serves `index.html`. Content-Type is auto-detected from the extension
877 * (see mime_type()). If the client sends `Accept-Encoding: gzip` and a
878 * `<path>.gz` exists, the pre-compressed file is served with
879 * `Content-Encoding: gzip`. Paths containing `..` are rejected (404).
880 *
881 * Only GET and HEAD are served; other methods get 405.
882 *
883 * @code
884 * server.serve_static("/", LittleFS, "/www"); // SPA from flash
885 * server.serve_static("/assets/", LittleFS, "/assets");
886 * @endcode
887 *
888 * @param url_prefix URL prefix to mount (with or without a trailing `*`).
889 * @param file_sys Filesystem reference (must outlive the server).
890 * @param fs_root Root directory on the filesystem (persistent string).
891 */
892 void serve_static(const char *url_prefix, fs::FS &file_sys, const char *fs_root);
893#endif // DETWS_ENABLE_FILE_SERVING
894
895#if DETWS_ENABLE_WEBDAV
896 /**
897 * @brief Mount a filesystem subtree as a WebDAV share (RFC 4918).
898 *
899 * Registers a wildcard route so every request under @p url_prefix is handled
900 * as WebDAV against @p fs_root on @p file_sys. The supported methods are
901 * OPTIONS, PROPFIND (Depth 0/1), GET, HEAD, PUT, DELETE, MKCOL, COPY, MOVE,
902 * and advisory LOCK/UNLOCK; a client such as rclone, cadaver, curl, or a
903 * mounted network drive can browse and edit files. The request path beyond
904 * the prefix is appended to @p fs_root (paths containing `..` are rejected).
905 *
906 * Limits (see DETWS_ENABLE_WEBDAV): PROPFIND builds a 207 into a
907 * DETWS_WEBDAV_BUF_SIZE buffer and lists at most DETWS_WEBDAV_MAX_ENTRIES
908 * children; PUT buffers the body (bounded by BODY_BUF_SIZE); COPY handles
909 * files (not collections); locks are advisory (issued, not enforced);
910 * PROPPATCH is unsupported. Combine with per-route auth and HTTPS before
911 * exposing a writable share.
912 *
913 * @code
914 * server.dav("/dav", LittleFS, "/dav"); // dav://<ip>/dav -> /dav on flash
915 * @endcode
916 *
917 * @param url_prefix URL prefix to mount (with or without a trailing `*`).
918 * @param file_sys Filesystem reference (must outlive the server).
919 * @param fs_root Root directory on the filesystem (persistent string).
920 */
921 void dav(const char *url_prefix, fs::FS &file_sys, const char *fs_root);
922#endif // DETWS_ENABLE_WEBDAV
923
924 /**
925 * @brief Register a fallback handler for unmatched requests.
926 *
927 * Called instead of sending a built-in 404 when no route matches.
928 * The callback may call send() to return a custom error page.
929 *
930 * @param callback Handler to invoke on a 404 condition.
931 */
932 void on_not_found(Handler callback);
933
934 /**
935 * @brief Install a per-request access-log callback (one hook, no buffering).
936 *
937 * @p cb is invoked once per response with the method, path, status code, and
938 * response body length. Pass nullptr to remove. See @ref RequestLogCb.
939 */
941
942 /**
943 * @brief Register a middleware to run before every request is dispatched.
944 *
945 * Middlewares run in registration order (see @ref Middleware) ahead of route
946 * matching, after the built-in rate-limit check. Up to MAX_MIDDLEWARE may be
947 * registered; further calls are ignored. Use this to add cross-cutting
948 * behavior - request logging, custom auth, header injection, feature gating -
949 * composed independently of individual routes.
950 *
951 * @code
952 * static MwResult log_mw(uint8_t slot, HttpReq *req) {
953 * Serial.printf("%s %s\n", req->method, req->path);
954 * return MwResult::MW_NEXT; // fall through to the handler
955 * }
956 * server.use(log_mw);
957 * @endcode
958 *
959 * @param mw Middleware function pointer (must not be nullptr).
960 */
961 void use(Middleware mw);
962
963 /**
964 * @brief Enable a built-in fixed-window request rate limiter.
965 *
966 * Counts all incoming requests in a sliding fixed window; once more than
967 * @p max_requests arrive within @p window_ms the server answers further
968 * requests in that window with `429 Too Many Requests` (plus a `Retry-After`
969 * header) instead of dispatching them. The check runs before the middleware
970 * chain and route matching, so it bounds work under flood. State is a few
971 * per-server counters (no heap, no per-IP table) - a global throttle suited
972 * to a small device behind a trusted LAN. For connection-level flood defense
973 * see also `DETWS_ENABLE_ACCEPT_THROTTLE`.
974 *
975 * @param max_requests Requests allowed per window. Pass 0 to disable.
976 * @param window_ms Window length in milliseconds (must be > 0).
977 */
978 void enable_rate_limit(uint16_t max_requests, uint32_t window_ms);
979
980#if DETWS_ENABLE_STATS
981 /**
982 * @brief Send a JSON runtime-stats snapshot and close the connection.
983 *
984 * Body: uptime_ms, total requests, 2xx/4xx/5xx counts, active connection-pool
985 * slots, and (on ESP32) free heap. Wire it to a route:
986 * @code
987 * server.on("/stats", HttpMethod::HTTP_GET, [](uint8_t id, HttpReq *) { server.stats(id); });
988 * @endcode
989 *
990 * @param slot_id Connection slot to respond on.
991 */
992 void stats(uint8_t slot_id);
993#endif
994
995#if DETWS_ENABLE_METRICS
996 /**
997 * @brief Respond with runtime metrics in Prometheus text exposition format.
998 *
999 * Convenience for a `/metrics` route: emits the stats counters as Prometheus
1000 * gauges/counters (Content-Type `text/plain; version=0.0.4`) so a Prometheus
1001 * server can scrape the device.
1002 * @code
1003 * server.on("/metrics", HttpMethod::HTTP_GET, [](uint8_t id, HttpReq *) { server.metrics(id); });
1004 * @endcode
1005 *
1006 * @param slot_id Connection slot to respond on.
1007 */
1008 void metrics(uint8_t slot_id);
1009#endif
1010
1011 /**
1012 * @brief Enable CORS by pre-building the Access-Control headers.
1013 *
1014 * Once called, every response produced by send() and send_empty()
1015 * includes the CORS headers. OPTIONS requests are intercepted and
1016 * answered with 204 automatically (preflight short-circuit).
1017 *
1018 * @param origin `Access-Control-Allow-Origin` value, e.g. `"*"` or
1019 * `"https://example.com"`. Pass `""` to disable CORS.
1020 */
1021 void set_cors(const char *origin);
1022
1023 /**
1024 * @brief Set the `Cache-Control` header emitted for static files.
1025 *
1026 * Applies to serve_file() / serve_static() responses (beside the ETag), so
1027 * browsers can cache assets and revalidate cheaply with `If-None-Match`.
1028 * Examples: `"no-cache"` (cache but always revalidate), `"max-age=3600"`,
1029 * `"public, max-age=31536000, immutable"`. Pass `""` / `nullptr` to disable.
1030 *
1031 * @param value `Cache-Control` directive, or empty/null to emit no header.
1032 */
1033 void set_cache_control(const char *value);
1034
1035 /**
1036 * @brief Drive the server - call every Arduino `loop()` iteration.
1037 *
1038 * On ESP32 `begin()` spawns the server worker task(s) (see DETWS_WORKER_COUNT),
1039 * which run the pipeline on their own core; `handle()` is then a no-op and your
1040 * `loop()` is free for application code. On host builds (and if no worker task
1041 * is running) `handle()` drives one service iteration inline, so existing
1042 * sketches and the native tests keep working unchanged.
1043 *
1044 * One service iteration (see service_once()):
1045 * 1. Calls `DeterministicAsyncTCP::check_timeouts()` to kill stale
1046 * connections.
1047 * 2. Drains the event queue (connections, data, disconnects, errors).
1048 * 3. Scans all connection slots for `ParseState::PARSE_COMPLETE` requests and
1049 * dispatches them to the matching route handler.
1050 * 4. Auto-sends 400 for any slot stuck in `ParseState::PARSE_ERROR`.
1051 * 5. Auto-sends 413 for any slot stuck in `ParseState::PARSE_ENTITY_TOO_LARGE`.
1052 * 6. Auto-sends 414 for any slot stuck in `ParseState::PARSE_URI_TOO_LONG`.
1053 *
1054 * Threading note: with the worker task running, route/WS/SSE handlers execute
1055 * in the worker task. Do server I/O from handlers; pushing from `loop()` (e.g.
1056 * SSE broadcast on a timer) runs concurrently with the worker and is made
1057 * thread-safe in a later phase.
1058 */
1059 void handle();
1060
1061 /**
1062 * @brief Run exactly one service iteration for worker @p worker_id (the body
1063 * driven by that worker's task, or by handle() when no task is running).
1064 *
1065 * Services only the connection slots owned by @p worker_id, so multiple workers
1066 * run disjoint slot sets in parallel. At DETWS_WORKER_COUNT=1 worker 0 owns
1067 * every slot. Public so the worker task can invoke it; application code should
1068 * call handle() rather than this directly.
1069 */
1070 void service_once(int worker_id = 0);
1071
1072 /**
1073 * @brief The instance-bound HTTP poll pump for one slot (the HTTP ProtoHandler's on_poll).
1074 *
1075 * Installed into the HTTP handler at begin() via http_proto_set_poll() so the worker dispatch
1076 * loop pumps HTTP through the same uniform ProtoHandler seam as every other protocol - there is no
1077 * HTTP special case in the loop. Runs the file/chunk send pumps, the WebSocket + SSE drains, the
1078 * keep-alive re-parse, and dispatches a completed request into this server's routes. Public only so
1079 * the poll trampoline can reach it (like service_once); application code never calls it directly.
1080 * @param slot_id Connection slot to pump.
1081 */
1082 void http_poll_slot(uint8_t slot_id);
1083
1084 /**
1085 * @brief Run @p fn(@p arg) on the worker that owns connection @p slot.
1086 *
1087 * The thread-safe way to push to a connection from outside a handler - e.g. an
1088 * SSE broadcast or a ws_send from loop() or a sensor task. Calling the send API
1089 * directly from another task would race the worker that owns the slot; instead
1090 * wrap the send in @p fn and defer it, and it runs single-threaded in the
1091 * owning worker's context. @p arg must stay valid until the callback runs. On
1092 * host builds (no worker task) it runs inline immediately.
1093 *
1094 * @return false if the slot is invalid or the worker's defer queue is full.
1095 */
1096 bool defer(uint8_t slot, detws_deferred_fn fn, void *arg);
1097
1098 /**
1099 * @brief Send an HTTP response with a body and close the connection.
1100 *
1101 * Writes status line, Content-Type, Content-Length, optional CORS
1102 * headers, and the payload; then calls tcp_close (tcp_abort on failure).
1103 * Always calls http_reset() at the end to free the parser slot.
1104 *
1105 * @param slot_id Connection slot index returned by the router.
1106 * @param code HTTP status code (200, 404, 500, …).
1107 * @param content_type MIME type string, e.g. `"application/json"`.
1108 * @param payload Null-terminated response body.
1109 *
1110 * @note If the underlying PCB has already been freed (e.g. by a
1111 * concurrent timeout), this function is a no-op that just
1112 * resets the slot.
1113 */
1114 void send(uint8_t slot_id, int code, const char *content_type, const char *payload);
1115
1116 /**
1117 * @brief Send an HTTP response with an explicit-length (possibly binary) body.
1118 *
1119 * Same as send() above but the body length is given, so the body may contain NUL
1120 * bytes (protobuf, gRPC-web frames, octet-stream, images). @p body_len is bounded
1121 * by the single-write limit (65535); larger bodies need the chunked/file path.
1122 *
1123 * @param slot_id Connection slot index returned by the router.
1124 * @param code HTTP status code.
1125 * @param content_type MIME type string, e.g. `"application/grpc-web+proto"`.
1126 * @param body Response body (may contain NULs); not required to be terminated.
1127 * @param body_len Number of body octets.
1128 */
1129 void send(uint8_t slot_id, int code, const char *content_type, const uint8_t *body, size_t body_len);
1130
1131 /**
1132 * @brief Send a headers-only HTTP response and close the connection.
1133 *
1134 * Equivalent to send() with an empty body and Content-Length: 0.
1135 * Useful for 204 No Content, 304 Not Modified, HEAD responses, and
1136 * CORS preflight replies.
1137 *
1138 * @param slot_id Connection slot index.
1139 * @param code HTTP status code.
1140 */
1141 void send_empty(uint8_t slot_id, int code);
1142
1143 /**
1144 * @brief Send an HTTP redirect (Location header, empty body) and close.
1145 *
1146 * Convenience for the common `/`→`/index.html` or canonical-host case,
1147 * previously hand-rolled via send_empty() plus a manual Location header.
1148 *
1149 * @param slot_id Connection slot index.
1150 * @param code Redirect status: 301, 302, 303, 307, or 308. Any other
1151 * value is treated as 302 Found.
1152 * @param location Value for the `Location` response header.
1153 */
1154 void redirect(uint8_t slot_id, int code, const char *location);
1155
1156 /**
1157 * @brief Send a response body with `{{name}}` placeholders substituted.
1158 *
1159 * Streams @p tmpl to the client, replacing each `{{name}}` token with the
1160 * string returned by @p resolver (nullptr → empty). The body is never
1161 * buffered whole: it is walked twice - once to compute Content-Length, once
1162 * to write - so memory use is constant regardless of body size. A `{{` with
1163 * no matching `}}` (or a name longer than 32 chars) is emitted literally.
1164 *
1165 * @param slot_id Connection slot index.
1166 * @param code HTTP status code.
1167 * @param content_type Response Content-Type.
1168 * @param tmpl Null-terminated template text.
1169 * @param resolver Placeholder resolver (see TemplateVar), or nullptr.
1170 */
1171 void send_template(uint8_t slot_id, int code, const char *content_type, const char *tmpl, TemplateVar resolver);
1172
1173 /**
1174 * @brief Stream a response body of unknown length via chunked transfer.
1175 *
1176 * Writes the status line and headers (including `Transfer-Encoding: chunked`,
1177 * plus any CORS / queued custom headers), then pulls the body from @p source
1178 * one piece at a time, adding the chunk framing and the terminating chunk. The
1179 * body is never buffered whole and the send paces with the TCP window - paging
1180 * across server loops as it drains - so output size is unbounded in constant
1181 * memory and a body larger than the send buffer is never truncated. This is the
1182 * complement to send(), which needs the full payload up front. A HEAD request
1183 * sends the headers only (@p source is not called).
1184 *
1185 * @param slot_id Connection slot index.
1186 * @param code HTTP status code.
1187 * @param content_type Response Content-Type.
1188 * @param source Generator that produces the body (must not be nullptr).
1189 * @param ctx Opaque state handed to @p source; see @ref ChunkSource
1190 * for the lifetime requirement (must outlive the response).
1191 */
1192 void send_chunked(uint8_t slot_id, int code, const char *content_type, ChunkSource source, void *ctx = nullptr);
1193
1194 /**
1195 * @brief Queue a custom response header for the next send on this slot.
1196 *
1197 * Call from inside a handler before send() / send_empty() / redirect().
1198 * The header is appended to a fixed per-slot buffer (EXTRA_HDR_BUF_SIZE)
1199 * and emitted verbatim as `Name: value\r\n`. Headers that would overflow
1200 * the buffer are dropped whole (never truncated mid-line). The buffer is
1201 * cleared automatically at the start of each request.
1202 *
1203 * @param slot_id Connection slot index.
1204 * @param name Header field name (no `:` or CRLF).
1205 * @param value Header field value (no CRLF).
1206 */
1207 void add_response_header(uint8_t slot_id, const char *name, const char *value);
1208
1209 /**
1210 * @brief Queue a `Set-Cookie` response header for the next send on this slot.
1211 *
1212 * Emits `Set-Cookie: name=value\r\n`, or `Set-Cookie: name=value; attrs\r\n`
1213 * when @p attrs is non-null (e.g. `"Path=/; HttpOnly; Max-Age=3600"`).
1214 * Shares the per-slot buffer with add_response_header().
1215 *
1216 * @param slot_id Connection slot index.
1217 * @param name Cookie name.
1218 * @param value Cookie value.
1219 * @param attrs Optional `;`-separated attribute string, or nullptr.
1220 */
1221 void set_cookie(uint8_t slot_id, const char *name, const char *value, const char *attrs = nullptr);
1222
1223 /**
1224 * @brief Discard any headers/cookies queued for this slot.
1225 *
1226 * @param slot_id Connection slot index.
1227 */
1228 void clear_response_headers(uint8_t slot_id);
1229
1230 /**
1231 * @brief Guess a `Content-Type` from a path's file extension.
1232 *
1233 * Small static extension→type table covering the common web asset types
1234 * (html, css, js, json, svg, png, jpg, gif, ico, txt, wasm, woff2, …).
1235 * Case-insensitive on the extension. Falls back to
1236 * `"application/octet-stream"` when the extension is unknown or absent.
1237 *
1238 * @param path File path or name (e.g. "/css/site.css").
1239 * @return Static content-type string (never null).
1240 */
1241 static const char *mime_type(const char *path);
1242
1243#if DETWS_ENABLE_DIAG
1244 /**
1245 * @brief Send the diagnostic JSON and close the connection.
1246 *
1247 * Responds with 200 application/json containing the compile-time feature
1248 * flags and all capacity constants. Only available when
1249 * DETWS_ENABLE_DIAG is set to 1 - disable before deploying to production.
1250 *
1251 * @param slot_id Connection slot index.
1252 */
1253 void diag(uint8_t slot_id);
1254#endif
1255
1256#if DETWS_ENABLE_WEBSOCKET
1257 // -----------------------------------------------------------------------
1258 // WebSocket API
1259 // -----------------------------------------------------------------------
1260
1261 /**
1262 * @brief Register a WebSocket upgrade route.
1263 *
1264 * When a GET request arrives for @p path with `Upgrade: websocket`, the
1265 * library performs the RFC 6455 handshake automatically and fires
1266 * @p on_connect. Subsequent frames fire @p on_message. Closing the
1267 * connection fires @p on_close.
1268 *
1269 * Ping frames are answered with Pong automatically; no handler needed.
1270 *
1271 * @param path URL path the client connects to, e.g. `"/ws"`.
1272 * @param on_connect Fired once when the handshake completes. May be nullptr.
1273 * @param on_message Fired for each text or binary frame. Must not be nullptr.
1274 * @param on_close Fired when the connection closes. May be nullptr.
1275 */
1276 void on_ws(const char *path, WsConnectHandler on_connect, WsMessageHandler on_message, WsCloseHandler on_close);
1277
1278 /**
1279 * @brief Send a text frame to a WebSocket client.
1280 *
1281 * @param ws_id Index into ws_pool[] (from the WsConnectHandler or WsMessageHandler).
1282 * @param text Null-terminated UTF-8 string to send.
1283 */
1284 void ws_send_text(uint8_t ws_id, const char *text);
1285
1286 /**
1287 * @brief Send a binary frame to a WebSocket client.
1288 *
1289 * @param ws_id Index into ws_pool[].
1290 * @param data Payload bytes.
1291 * @param len Payload length in bytes; must be <= WS_FRAME_SIZE.
1292 */
1293 void ws_send_binary(uint8_t ws_id, const uint8_t *data, uint16_t len);
1294
1295 /**
1296 * @brief Initiate a graceful WebSocket close.
1297 *
1298 * Sends a Close frame with WsCloseCode::WS_CLOSE_NORMAL and marks the slot WsParseState::WS_CLOSED.
1299 * The on_close handler fires on the next handle() call.
1300 *
1301 * @param ws_id Index into ws_pool[].
1302 */
1303 void ws_disconnect(uint8_t ws_id);
1304#endif // DETWS_ENABLE_WEBSOCKET
1305
1306#if DETWS_ENABLE_SSE
1307 // -----------------------------------------------------------------------
1308 // Server-Sent Events API
1309 // -----------------------------------------------------------------------
1310
1311 /**
1312 * @brief Register a Server-Sent Events endpoint.
1313 *
1314 * When a GET request arrives for @p path, the library sends the SSE
1315 * headers and keeps the connection open. @p on_connect fires so the
1316 * handler can push an initial event with sse_send().
1317 *
1318 * @param path URL path, e.g. `"/events"`.
1319 * @param on_connect Fired when a client subscribes. May be nullptr.
1320 */
1321 void on_sse(const char *path, SseConnectHandler on_connect);
1322
1323 /**
1324 * @brief Push an event to one SSE client.
1325 *
1326 * Formats and sends `event: ...\ndata: ...\nid: ...\n\n` to the client
1327 * on @p sse_id. Any field may be nullptr to omit it from the output.
1328 * The data field is required; passing nullptr sends nothing.
1329 *
1330 * @param sse_id Index into sse_pool[].
1331 * @param data Event data string (required).
1332 * @param event Optional event name (sets the `event:` field).
1333 * @param id Optional event ID (sets the `id:` field).
1334 */
1335 void sse_send(uint8_t sse_id, const char *data, const char *event = nullptr, const char *id = nullptr);
1336
1337 /**
1338 * @brief Push an event to all connected SSE clients on a given path.
1339 *
1340 * Iterates sse_pool[] and calls sse_send() for every active client
1341 * whose path matches @p path.
1342 *
1343 * @param path SSE endpoint path, e.g. `"/events"`.
1344 * @param data Event data string.
1345 * @param event Optional event name.
1346 * @param id Optional event ID.
1347 */
1348 void sse_broadcast(const char *path, const char *data, const char *event = nullptr, const char *id = nullptr);
1349#endif // DETWS_ENABLE_SSE
1350};
1351
1352#endif
#define CONN_POOL_SLOTS
DetIface
Network interface a connection arrived on (for per-route filtering).
#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 DETWS_HTTP3_PORT
UDP port the HTTP/3 (QUIC) server binds by default (used by DetWebServer::h3_cert).
#define MAX_MIDDLEWARE
Maximum globally-registered middleware functions.
#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 EXTRA_HDR_BUF_SIZE
Per-connection buffer for app-supplied custom response headers and cookies.
#define MAX_ROUTES
Maximum simultaneously registered routes.
Single-port HTTP server with deterministic, zero-allocation execution.
Definition dwserver.h:348
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.
Definition response.cpp:289
void set_cache_control(const char *value)
Set the Cache-Control header emitted for static files.
Definition dwserver.cpp:882
void on_regex(const char *pattern, HttpMethod method, Handler callback)
Register a route whose path is a regular expression.
Definition dwserver.cpp:787
void on(const char *path, HttpMethod method, Handler callback)
Register a route handler.
Definition dwserver.cpp:759
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.
Definition dwserver.cpp:707
int32_t listen(uint16_t port, ConnProto proto=ConnProto::PROTO_HTTP)
Register a port to listen on when begin() is called.
Definition dwserver.cpp:411
void add_response_header(uint8_t slot_id, const char *name, const char *value)
Queue a custom response header for the next send on this slot.
Definition response.cpp:276
void clear_response_headers(uint8_t slot_id)
Discard any headers/cookies queued for this slot.
Definition response.cpp:306
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.
Definition dwserver.cpp:848
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 send_chunked(uint8_t slot_id, int code, const char *content_type, ChunkSource source, void *ctx=nullptr)
Stream a response body of unknown length via chunked transfer.
Definition response.cpp:137
void on_request_log(RequestLogCb cb)
Install a per-request access-log callback (one hook, no buffering).
Definition dwserver.cpp:259
void service_once(int worker_id=0)
Run exactly one service iteration for worker worker_id (the body driven by that worker's task,...
static const char * mime_type(const char *path)
Guess a Content-Type from a path's file extension.
Definition response.cpp:317
void use(Middleware mw)
Register a middleware to run before every request is dispatched.
void enable_rate_limit(uint16_t max_requests, uint32_t window_ms)
Enable a built-in fixed-window request rate limiter.
void set_cors(const char *origin)
Enable CORS by pre-building the Access-Control headers.
Definition dwserver.cpp:866
void redirect(uint8_t slot_id, int code, const char *location)
Send an HTTP redirect (Location header, empty body) and close.
void send_template(uint8_t slot_id, int code, const char *content_type, const char *tmpl, TemplateVar resolver)
Send a response body with {{name}} placeholders substituted.
Definition response.cpp:90
void set_ap_ip(uint32_t ap_ip)
Tell the server the softAP IPv4 address for STA/AP route filtering.
Definition dwserver.cpp:782
int32_t begin(const WebServerConfig *cfg=nullptr)
Initialize all connection slots and open all registered listeners.
Definition dwserver.cpp:495
DetWebServer()
Construct a DetWebServer with an empty routing table.
Definition dwserver.cpp:236
void stop()
Gracefully stop the server.
Definition dwserver.cpp:715
size_t(* ChunkSource)(uint8_t *buf, size_t cap, void *ctx)
Source callback that produces a chunked response body incrementally.
Definition dwserver.h:310
DetWebServerResult
Result codes for listen(), begin(), and restart().
Definition dwserver.h:230
@ 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.
Definition dwserver.h:106
void(* RequestLogCb)(const char *method, const char *path, int status, int body_len)
Per-request access-log callback (see DetWebServer::on_request_log()).
Definition dwserver.h:126
const char *(* TemplateVar)(const char *name)
Resolver for {{name}} template placeholders used by send_template().
Definition dwserver.h:116
RouteType
Discriminates between HTTP, WebSocket, and SSE route entries.
Definition dwserver.h:203
@ ROUTE_HTTP
Standard HTTP request/response.
HttpMethod
HTTP request methods supported by the router.
Definition dwserver.h:77
@ 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.
MwResult(* Middleware)(uint8_t slot_id, HttpReq *request)
Composable pre-dispatch middleware (see DetWebServer::use()).
Definition dwserver.h:158
MwResult
Outcome of a middleware function (see Middleware).
Definition dwserver.h:138
@ MW_HALT
Stop dispatch; the middleware already sent a response.
@ MW_NEXT
Continue to the next middleware / the route handler.
Layer 6 (Presentation) - zero-heap JSON: a bounded writer and top-level reader.
In-place multipart/form-data parser (RFC 7578).
Layer 6 (Presentation) - wires the transport ring buffer to the HTTP parser.
Layer 5 (Session) - event queue dispatcher and session lifecycle.
Layer 6 (Presentation) – Server-Sent Events connection pool.
Fully-parsed HTTP/1.1 request.
Internal route entry stored in the routing table.
Definition dwserver.h:244
bool is_param
true when the path contains a :name segment.
Definition dwserver.h:275
RouteType type
HTTP, WS, or SSE.
Definition dwserver.h:246
bool is_regex
true when the path is a regex (see on_regex()).
Definition dwserver.h:276
Handler callback
HTTP handler (RouteType::ROUTE_HTTP only).
Definition dwserver.h:248
DetIface iface_filter
Interface gate; DetIface::DETIFACE_ANY (0) = match any interface.
Definition dwserver.h:277
HttpMethod method
HTTP method (RouteType::ROUTE_HTTP only).
Definition dwserver.h:247
bool is_active
false for unused table slots.
Definition dwserver.h:273
bool is_wildcard
true when path ends with *.
Definition dwserver.h:274
char path[MAX_PATH_LEN]
Null-terminated path pattern.
Definition dwserver.h:245
Runtime-tunable server parameters.
WebSocket connection state stored in ws_pool[].
Definition websocket.h:125
void ws_close(WsConn *ws, WsCloseCode code)
Send a Close frame and mark the slot WsParseState::WS_CLOSED.
Layer 6 (Presentation) – WebSocket frame parser and connection pool.
Layer 5 (Session) - server worker identity.
void(* detws_deferred_fn)(void *arg)
Deferred callback signature.
Definition worker.h:92