38#if DETWS_ENABLE_WEBSOCKET
41#elif DETWS_ENABLE_AUTH
47#if DETWS_ENABLE_WEBSOCKET
49static const char WS_MAGIC[] =
"258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
61static const char *status_text(
int code)
72 return "Moved Permanently";
76 return "Not Modified";
80 return "Unauthorized";
86 return "Method Not Allowed";
88 return "Request Timeout";
92 return "Payload Too Large";
94 return "URI Too Long";
96 return "Too Many Requests";
98 return "Internal Server Error";
100 return "Not Implemented";
102 return "Service Unavailable";
120 if (strcmp(m,
"POST") == 0)
122 if (strcmp(m,
"PUT") == 0)
124 if (strcmp(m,
"DELETE") == 0)
126 if (strcmp(m,
"PATCH") == 0)
128 if (strcmp(m,
"HEAD") == 0)
130 if (strcmp(m,
"OPTIONS") == 0)
142 _cors_header_buf[0] =
'\0';
150#if DETWS_ENABLE_WEBSOCKET
164 return begin(_port, cfg);
172#if DETWS_ENABLE_WEBSOCKET
194static void fill_route_base(
Route *r,
const char *path)
199 size_t len = strlen(r->
path);
207 Route *r = &_routes[_route_count++];
208 fill_route_base(r, path);
216 const char *realm,
const char *user,
const char *pass)
220 Route *r = &_routes[_route_count++];
221 fill_route_base(r, path);
225 r->auth_required =
true;
235#if DETWS_ENABLE_WEBSOCKET
236void DetWebServer::on_ws(
const char *path,
237 WsConnectHandler on_connect,
238 WsMessageHandler on_message,
239 WsCloseHandler on_close)
243 Route *r = &_routes[_route_count++];
244 fill_route_base(r, path);
246 r->ws_connect = on_connect;
247 r->ws_message = on_message;
248 r->ws_close = on_close;
253void DetWebServer::on_sse(
const char *path, SseConnectHandler on_connect)
257 Route *r = &_routes[_route_count++];
258 fill_route_base(r, path);
260 r->sse_connect = on_connect;
266 _not_found_handler = callback;
284 if (!origin || origin[0] ==
'\0')
286 _cors_enabled =
false;
287 _cors_header_buf[0] =
'\0';
291 "Access-Control-Allow-Origin: %s\r\n"
292 "Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS\r\n"
293 "Access-Control-Allow-Headers: Content-Type\r\n",
295 _cors_enabled =
true;
309bool DetWebServer::path_matches(
const char *route,
bool is_wildcard,
const char *req_path)
312 return strcmp(route, req_path) == 0;
315 size_t prefix_len = strlen(route) - 1;
316 return strncmp(route, req_path, prefix_len) == 0;
337#if DETWS_ENABLE_WEBSOCKET
346 for (uint8_t r = 0; r < _route_count; r++)
348 if (_routes[r].type == ROUTE_WS && _routes[r].ws_message)
350 _routes[r].ws_message(ws->
ws_id);
358 for (uint8_t r = 0; r < _route_count; r++)
360 if (_routes[r].type == ROUTE_WS && _routes[r].
ws_close)
362 _routes[r].ws_close(ws->
ws_id);
382 match_and_execute(i);
388 send(i, 400,
"text/plain",
"Bad Request");
392 send(i, 413,
"text/plain",
"Payload Too Large");
396 send(i, 414,
"text/plain",
"URI Too Long");
406void DetWebServer::diag(uint8_t slot_id)
408 send(slot_id, 200,
"application/json", DETWS_DIAG_JSON);
416#if DETWS_ENABLE_WEBSOCKET
424static const size_t WS_MAX_KEY_LEN = 64;
426static bool ws_accept_key(
const char *client_key,
char *out)
428 size_t key_len = strnlen(client_key, WS_MAX_KEY_LEN + 1);
429 if (key_len > WS_MAX_KEY_LEN)
434 size_t magic_len =
sizeof(WS_MAGIC) - 1;
435 char concat[WS_MAX_KEY_LEN +
sizeof(WS_MAGIC)];
436 memcpy(concat, client_key, key_len);
437 memcpy(concat + key_len, WS_MAGIC, magic_len);
440 sha1((
const uint8_t *)concat, key_len + magic_len, digest);
451static bool ws_do_upgrade(uint8_t slot_id,
HttpReq *req,
452 WsConnectHandler on_connect)
459 if (!ws_accept_key(client_key, accept))
467 int hlen = snprintf(hdr,
sizeof(hdr),
468 "HTTP/1.1 101 Switching Protocols\r\n"
469 "Upgrade: websocket\r\n"
470 "Connection: Upgrade\r\n"
471 "Sec-WebSocket-Accept: %s\r\n\r\n",
474 tcp_write(conn->
pcb, hdr, (u16_t)hlen, TCP_WRITE_FLAG_COPY);
475 tcp_output(conn->
pcb);
484 tcp_arg(conn->
pcb,
nullptr);
491 on_connect(ws->
ws_id);
505static bool sse_do_upgrade(uint8_t slot_id,
HttpReq *req,
506 SseConnectHandler on_connect)
512 static const char SSE_HDR[] =
513 "HTTP/1.1 200 OK\r\n"
514 "Content-Type: text/event-stream\r\n"
515 "Cache-Control: no-cache\r\n"
516 "Connection: keep-alive\r\n\r\n";
518 tcp_write(conn->
pcb, SSE_HDR, (u16_t)(
sizeof(SSE_HDR) - 1),
519 TCP_WRITE_FLAG_COPY);
520 tcp_output(conn->
pcb);
523 const char *path = req->
path;
529 tcp_arg(conn->
pcb,
nullptr);
546void DetWebServer::match_and_execute(uint8_t slot_id)
561 send(slot_id, 501,
"text/plain",
"Not Implemented");
565#if DETWS_ENABLE_WEBSOCKET
567 bool is_ws_upgrade = (method ==
HTTP_GET)
569 && (strcasecmp(upgrade_hdr,
"websocket") == 0);
572 for (uint8_t i = 0; i < _route_count; i++)
574 Route *r = &_routes[i];
580#if DETWS_ENABLE_WEBSOCKET
581 if (r->
type == ROUTE_WS)
585 send(slot_id, 400,
"text/plain",
"WebSocket upgrade required");
588 if (!ws_do_upgrade(slot_id, req, r->ws_connect))
589 send(slot_id, 503,
"text/plain",
"Service Unavailable");
595 if (r->
type == ROUTE_SSE)
597 if (!sse_do_upgrade(slot_id, req, r->sse_connect))
598 send(slot_id, 503,
"text/plain",
"Service Unavailable");
607 if (r->auth_required && !check_basic_auth(slot_id, req, r))
609 send_unauth(slot_id, r->auth_realm);
617 if (_not_found_handler)
618 _not_found_handler(slot_id, req);
620 send(slot_id, 404,
"text/plain",
"Not Found");
648 int payload_len = (int)strlen(payload);
651 int hlen = snprintf(header,
sizeof(header),
653 "Content-Type: %s\r\n"
654 "Content-Length: %d\r\n"
656 "Connection: close\r\n\r\n",
657 code, status_text(code), content_type, payload_len,
658 _cors_enabled ? _cors_header_buf :
"");
660 struct tcp_pcb *pcb = conn->
pcb;
665 tcp_arg(pcb,
nullptr);
669 tcp_write(pcb, header, (u16_t)hlen, TCP_WRITE_FLAG_COPY);
670 tcp_write(pcb, payload, (u16_t)payload_len, TCP_WRITE_FLAG_COPY);
673 if (tcp_close(pcb) != ERR_OK)
699 int hlen = snprintf(header,
sizeof(header),
701 "Content-Length: 0\r\n"
703 "Connection: close\r\n\r\n",
704 code, status_text(code), _cors_enabled ? _cors_header_buf :
"");
706 struct tcp_pcb *pcb = conn->
pcb;
707 tcp_arg(pcb,
nullptr);
711 tcp_write(pcb, header, (u16_t)hlen, TCP_WRITE_FLAG_COPY);
714 if (tcp_close(pcb) != ERR_OK)
724#if DETWS_ENABLE_WEBSOCKET
725void DetWebServer::ws_send_text(uint8_t ws_id,
const char *text)
732 uint16_t len = (uint16_t)strlen(text);
737 tcp_output(conn->
pcb);
741void DetWebServer::ws_send_binary(uint8_t ws_id,
const uint8_t *data, uint16_t len)
752 tcp_output(conn->
pcb);
756void DetWebServer::ws_disconnect(uint8_t ws_id)
764 tcp_output(conn->
pcb);
774void DetWebServer::sse_send(uint8_t sse_id,
const char *data,
775 const char *event,
const char *
id)
784 tcp_output(conn->
pcb);
788void DetWebServer::sse_broadcast(
const char *path,
const char *data,
789 const char *event,
const char *
id)
795 if (strcmp(
sse_pool[i].path, path) != 0)
802 tcp_output(conn->
pcb);
813void DetWebServer::send_unauth(uint8_t slot_id,
const char *realm)
822 static const char body[] =
"Unauthorized";
824 int hlen = snprintf(header,
sizeof(header),
825 "HTTP/1.1 401 Unauthorized\r\n"
826 "WWW-Authenticate: Basic realm=\"%s\"\r\n"
827 "Content-Type: text/plain\r\n"
828 "Content-Length: %d\r\n"
830 "Connection: close\r\n\r\n",
831 realm, (
int)(
sizeof(body) - 1),
832 _cors_enabled ? _cors_header_buf :
"");
834 struct tcp_pcb *pcb = conn->
pcb;
835 tcp_arg(pcb,
nullptr);
839 tcp_write(pcb, header, (u16_t)hlen, TCP_WRITE_FLAG_COPY);
840 tcp_write(pcb, body, (u16_t)(
sizeof(body) - 1), TCP_WRITE_FLAG_COPY);
843 if (tcp_close(pcb) != ERR_OK)
849bool DetWebServer::check_basic_auth(uint8_t ,
HttpReq *req,
const Route *r)
852 if (!auth_hdr || strncmp(auth_hdr,
"Basic ", 6) != 0)
857 if (n == 0 || n >=
sizeof(decoded))
861 const char *colon = (
const char *)memchr(decoded,
':', n);
865 size_t ulen = (size_t)(colon - (
const char *)decoded);
866 const char *pass = colon + 1;
868 return (ulen == strlen(r->auth_user))
869 && (memcmp(decoded, r->auth_user, ulen) == 0)
870 && (strcmp(pass, r->auth_pass) == 0);
878#if DETWS_ENABLE_FILE_SERVING
879void DetWebServer::serve_file(uint8_t slot_id, fs::FS &file_sys,
880 const char *fs_path,
const char *content_type)
882 fs::File f = file_sys.open(fs_path,
"r");
885 send(slot_id, 404,
"text/plain",
"Not Found");
897 size_t file_size = f.size();
900 int hlen = snprintf(header,
sizeof(header),
901 "HTTP/1.1 200 OK\r\n"
902 "Content-Type: %s\r\n"
903 "Content-Length: %d\r\n"
905 "Connection: close\r\n\r\n",
906 content_type, (
int)file_size,
907 _cors_enabled ? _cors_header_buf :
"");
909 struct tcp_pcb *pcb = conn->
pcb;
910 tcp_arg(pcb,
nullptr);
914 tcp_write(pcb, header, (u16_t)hlen, TCP_WRITE_FLAG_COPY);
918 while ((n = f.read(chunk,
sizeof(chunk))) > 0)
919 tcp_write(pcb, chunk, (u16_t)n, TCP_WRITE_FLAG_COPY);
923 if (tcp_close(pcb) != ERR_OK)
#define WS_HDR_BUF_SIZE
Stack buffer for the HTTP 101 Switching Protocols response sent during the WebSocket handshake.
#define RESP_HDR_BUF_SIZE
Stack buffer for HTTP response header lines in send() / send_empty() / send_unauth() / serve_file().
#define FILE_CHUNK_SIZE
Bytes read from the filesystem and passed to tcp_write() per loop().
#define MAX_PATH_LEN
Maximum URL path length (including leading /).
#define MAX_AUTH_LEN
Maximum username or password length for HTTP Basic Authentication.
#define MAX_SSE_CONNS
Maximum simultaneous SSE connections.
#define CORS_HDR_BUF_SIZE
Size of the pre-built CORS header block stored in DetWebServer.
#define MAX_CONNS
Maximum simultaneous TCP connections.
#define MAX_WS_CONNS
Maximum simultaneous WebSocket connections.
#define MAX_ROUTES
Maximum simultaneously registered routes.
Layer 7 (Application) — public HTTP routing API.
@ ROUTE_HTTP
Standard HTTP request/response.
HttpMethod
HTTP request methods supported by the router.
@ HTTP_PUT
Idempotent replace.
@ HTTP_DELETE
Idempotent delete.
@ HTTP_GET
Safe, idempotent read.
@ HTTP_OPTIONS
Capability query / CORS preflight.
@ HTTP_POST
Non-idempotent create / action.
@ HTTP_HEAD
Same as GET but no response body.
@ HTTP_PATCH
Partial update.
void(* Handler)(uint8_t slot_id, HttpReq *request)
Callback signature for HTTP request handlers.
void base64_encode(const uint8_t *src, size_t src_len, char *dst)
Encode src_len bytes of src as Base64.
size_t base64_decode(const char *src, uint8_t *dst)
Decode a null-terminated Base64 string.
void on(const char *path, HttpMethod method, Handler callback)
Register a route handler.
void handle()
Drive the server — call every Arduino loop() iteration.
int32_t restart(const WebServerConfig *cfg=nullptr)
Hard-reset all connections and reinitialise the server on the same port that was passed to begin().
int32_t begin(uint16_t port, const WebServerConfig *cfg=nullptr)
Initialise all connection slots and open the TCP listener.
void send(uint8_t slot_id, int code, const char *content_type, const char *payload)
Send an HTTP response with a body and close the connection.
void on_not_found(Handler callback)
Register a fallback handler for unmatched requests.
void send_empty(uint8_t slot_id, int code)
Send a headers-only HTTP response and close the connection.
void set_cors(const char *origin)
Enable CORS by pre-building the Access-Control headers.
static bool heap_available()
True if the largest contiguous free heap block >= heap_needed().
static size_t heap_needed()
Bytes of contiguous heap that begin() will allocate.
DetWebServer()
Construct a DetWebServer with an empty routing table.
void stop()
Gracefully stop the server.
static bool heap_available()
Always returns true — no heap allocation means no pre-flight needed.
static int32_t init(uint16_t port, const WebServerConfig *cfg=nullptr)
Initialise the TCP stack, create the event queue, and begin listening.
static size_t heap_needed()
Always returns 0 — the library makes no heap allocations.
static void stop()
Stop the server: abort all connections, close the listener, free the queue.
const char * http_get_header(const HttpReq *req, const char *key)
Look up a header value by name (case-insensitive).
HttpReq http_pool[MAX_CONNS]
Pool of parser contexts, one per transport slot.
@ PARSE_COMPLETE
Full request parsed; ready for dispatch.
@ PARSE_URI_TOO_LONG
Path exceeds MAX_PATH_LEN → 414.
@ PARSE_ENTITY_TOO_LARGE
Content-Length > BODY_BUF_SIZE → 413.
@ PARSE_ERROR
Unrecoverable parse failure → 400.
void http_reset(uint8_t slot_id)
Reset the HTTP parser for a connection slot.
void server_tick()
Drive the session layer for one Arduino loop iteration.
void sha1(const uint8_t *data, size_t len, uint8_t digest[SHA1_DIGEST_LEN])
Compute a SHA-1 digest over an arbitrary byte buffer.
Software SHA-1 implementation — no platform dependencies.
#define SHA1_DIGEST_LEN
SHA-1 digest length in bytes.
void sse_init()
Initialise all SSE pool slots to inactive.
bool sse_write(SseConn *sse, const char *data, const char *event, const char *id)
Write one SSE event record to a client.
SseConn sse_pool[MAX_SSE_CONNS]
Pool of SSE connection state, one per MAX_SSE_CONNS.
SseConn * sse_find(uint8_t slot_id)
Find the SseConn for a given TCP slot, or nullptr.
SseConn * sse_alloc(uint8_t slot_id, const char *path)
Allocate an SseConn and bind it to a TCP slot.
Fully-parsed HTTP/1.1 request.
char method[8]
HTTP method, null-terminated (max 7: OPTIONS).
char path[MAX_PATH_LEN]
URL path, null-terminated; no query string.
Internal route entry stored in the routing table.
RouteType type
HTTP, WS, or SSE.
Handler callback
HTTP handler (ROUTE_HTTP only).
HttpMethod method
HTTP method (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.
SSE connection state stored in sse_pool[].
uint8_t sse_id
Index into sse_pool[] (set at init).
uint8_t slot_id
Owning TCP slot in conn_pool[].
A single TCP connection context.
volatile ConnState state
Lifecycle state; volatile for inter-task visibility.
struct tcp_pcb * pcb
lwIP PCB; null when slot is free.
Runtime-tunable server parameters.
WebSocket connection state stored in ws_pool[].
uint8_t ws_id
Index into ws_pool[] (set at init).
uint8_t slot_id
Owning TCP slot in conn_pool[].
WsParseState parse_state
Current frame parser state.
TcpConn conn_pool[MAX_CONNS]
Static pool of connection contexts. Defined in transport.cpp.
@ CONN_FREE
Slot is available; no PCB is attached.
@ CONN_ACTIVE
Live connection; PCB is valid.
void ws_free(uint8_t slot_id)
Free the WsConn associated with a TCP slot.
bool ws_send_frame(WsConn *ws, WsOpcode opcode, const uint8_t *payload, uint16_t len)
Send a WebSocket frame to the client.
void ws_close(WsConn *ws, WsCloseCode code)
Send a Close frame and mark the slot 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 WS_HEADER1, ready for the next frame.
WsConn * ws_alloc(uint8_t slot_id)
Allocate a WsConn slot and bind it to a TCP slot.
WsConn ws_pool[MAX_WS_CONNS]
Pool of WebSocket connection state, one per MAX_WS_CONNS.
void ws_init()
Initialise 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.
@ WS_OP_TEXT
UTF-8 text payload.
@ WS_OP_BINARY
Binary payload.
@ WS_CLOSE_NORMAL
Normal closure.