DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
http_parser.cpp
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 http_parser.cpp
6 * @brief Standalone HTTP/1.1 request parser - implementation.
7 *
8 * No dependency on transport, session, or lwIP. Consumes one byte at a
9 * time via http_parser_feed(); the presentation layer is responsible for
10 * pulling bytes out of whatever transport buffer it uses.
11 */
12
13#include "http_parser.h"
14#include "network_drivers/network/ip.h" // validate a recovered proxy client IP (v4/v6)
15
17
18#if DETWS_ENABLE_STREAM_BODY
19// Streaming-body hooks (OTA / file upload), owned by one instance (internal linkage): null
20// unless the application installs them. One named owner, unreachable cross-TU. (The http_pool[]
21// request table is the shared cross-TU substrate.)
22struct HttpParserCtx
23{
24 HttpStreamBeginCb stream_begin = nullptr;
25 HttpStreamDataCb stream_data = nullptr;
26 HttpStreamAbortCb stream_abort = nullptr;
27};
28static HttpParserCtx s_hp;
29
30void http_parser_set_stream_hooks(HttpStreamBeginCb begin, HttpStreamDataCb data, HttpStreamAbortCb abort)
31{
32 s_hp.stream_begin = begin;
33 s_hp.stream_data = data;
34 s_hp.stream_abort = abort;
35}
36#endif // DETWS_ENABLE_STREAM_BODY
37
38// ---------------------------------------------------------------------------
39// FNV-1a hash constants for HTTP version validation
40// ---------------------------------------------------------------------------
41// Precomputed at compile time via constexpr; zero runtime cost.
42// The hash of the 8-byte version token ("HTTP/1.0" or "HTTP/1.1") is
43// compared against the accumulated _version_hash when CR terminates the
44// version field.
45
46static constexpr uint32_t FNV_OFFSET = 2166136261u;
47static constexpr uint32_t FNV_PRIME = 16777619u;
48
49static constexpr uint32_t fnv1a(const char *s, uint32_t h = FNV_OFFSET)
50{
51 return *s ? fnv1a(s + 1, (h ^ (uint8_t)*s) * FNV_PRIME) : h;
52}
53
54static constexpr uint32_t HASH_HTTP10 = fnv1a("HTTP/1.0");
55static constexpr uint32_t HASH_HTTP11 = fnv1a("HTTP/1.1");
56
57// ---------------------------------------------------------------------------
58// RFC 7230 character-class table (hot path)
59// ---------------------------------------------------------------------------
60//
61// The per-byte parser classifies every request byte, so the three character
62// classes below are folded into one 256-entry table built at compile time (it
63// lands in flash .rodata). A hot-path check is then a single table load + a mask
64// bit, instead of the range compares + 15-case switch it replaces:
65// 0x01 tchar - method + header field-name (RFC 7230 §3.2.6)
66// 0x02 vchar - request-target path/query bytes (RFC 5234 VCHAR = %x21-7E)
67// 0x04 field-value - header field-value bytes (RFC 7230 §3.2: VCHAR/SP/HTAB/obs-text)
68
69static constexpr uint8_t CC_TCHAR = 0x01;
70static constexpr uint8_t CC_VCHAR = 0x02;
71static constexpr uint8_t CC_FIELD_VALUE = 0x04;
72
73// The 256-entry class table, one const byte per input octet (lands in flash .rodata). A plain literal so it is
74// standard-independent (the arduino-esp32 build is gnu++11, where a constexpr loop-built table is ill-formed).
75// Each entry ORs the classes that octet belongs to; regenerate via tools if the character classes ever change:
76// tchar = ALPHA/DIGIT/"!#$%&'*+-.^_`|~" vchar = %x21-7E field-value = HTAB/%x20-7E/obs-text(%x80-FF)
77static const uint8_t kCharClass[256] = {
78 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
79 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x07, 0x06, 0x07, 0x07, 0x07,
80 0x07, 0x07, 0x06, 0x06, 0x07, 0x07, 0x06, 0x07, 0x07, 0x06, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
81 0x07, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
82 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x06, 0x06, 0x06, 0x07,
83 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
84 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x06, 0x07, 0x06, 0x07, 0x00, 0x04, 0x04, 0x04, 0x04, 0x04,
85 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
86 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
87 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
88 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
89 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
90 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
91 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
92};
93
94static inline bool is_tchar(uint8_t b)
95{
96 return (kCharClass[b] & CC_TCHAR) != 0;
97}
98static inline bool is_vchar(uint8_t b)
99{
100 return (kCharClass[b] & CC_VCHAR) != 0;
101}
102static inline bool is_field_value_char(uint8_t b)
103{
104 return (kCharClass[b] & CC_FIELD_VALUE) != 0;
105}
106
107/**
108 * @brief Split a raw query string into key=value pairs.
109 *
110 * Operates in-place on `req->query[]`. Pairs are `&`-separated; key and
111 * value are split on the first `=`. Keys or values longer than their
112 * respective limits are silently truncated - the path itself remains valid.
113 */
114static void parse_query_params(HttpReq *req)
115{
116 const char *qs = req->query;
117 size_t len = req->query_idx;
118 size_t i = 0;
119
120 while (i < len && req->query_count < MAX_QUERY_PARAMS)
121 {
122 QueryParam *qp = &req->query_params[req->query_count];
123 size_t key_idx = 0;
124 size_t val_idx = 0;
125 bool in_val = false;
126
127 while (i < len)
128 {
129 char c = qs[i++];
130 if (c == '&')
131 break;
132 if (c == '=' && !in_val)
133 {
134 in_val = true;
135 continue;
136 }
137 if (!in_val && key_idx < QUERY_KEY_LEN - 1)
138 qp->key[key_idx++] = c;
139 else if (in_val && val_idx < QUERY_VAL_LEN - 1)
140 qp->val[val_idx++] = c;
141 }
142
143 if (key_idx > 0)
144 req->query_count++;
145 }
146}
147
149{
150 uint8_t id = req->slot_id;
151#if DETWS_ENABLE_STREAM_BODY
152 // A streamed body that never reached ParseState::PARSE_COMPLETE is being torn down (peer
153 // reset / timeout / error): let the sink release its resource before we wipe
154 // the state. The normal-completion reset runs while parse_state==ParseState::PARSE_COMPLETE
155 // (the handler already finished the sink), so this fires only on abort.
156 if (req->body_streaming && req->parse_state != ParseState::PARSE_COMPLETE && s_hp.stream_abort)
157 s_hp.stream_abort(req);
158#endif
159 *req = {}; // zero all fields
160 req->slot_id = id; // restore slot identity
162 req->_version_hash = FNV_OFFSET; // seed the FNV-1a accumulator
163}
164
165void http_parser_feed(HttpReq *p, uint8_t byte)
166{
167 // Terminal states (PARSE_COMPLETE / PARSE_ERROR / PARSE_ENTITY_TOO_LARGE / PARSE_URI_TOO_LONG) have no case
168 // below, so they fall through to `default:` and no-op - no separate guard switch on the per-byte hot path.
169 char c = (char)byte;
170
171 switch (p->parse_state)
172 {
173
175 if (c == ' ')
176 {
178 p->current_token_idx = 0;
179 }
180 else if (!is_tchar(byte))
181 {
182 // RFC 7230 §3.1.1: method = token; any non-tchar is malformed
184 }
185 else if (p->current_token_idx < sizeof(p->method) - 1)
186 {
187 p->method[p->current_token_idx++] = c;
188 }
189 else
190 {
192 }
193 break;
194
196 if (c == ' ')
197 {
199 }
200 else if (c == '?')
201 {
203 }
204 else if (!is_vchar(byte))
205 {
206 // RFC 3986 §3.3: path chars must be visible ASCII (or pct-encoded)
208 }
209 else if (p->path_idx < MAX_PATH_LEN - 1)
210 {
211 p->path[p->path_idx++] = c;
212 }
213 else
214 {
216 }
217 break;
218
220 if (c == ' ')
221 {
222 parse_query_params(p);
224 }
225 else if (!is_vchar(byte))
226 {
227 // Control chars and NUL are not valid query-string bytes
229 }
230 else if (p->query_idx < MAX_QUERY_LEN - 1)
231 {
232 p->query[p->query_idx++] = c;
233 }
234 // Silently truncate - query overflow is a capacity limit, not a protocol error
235 break;
236
238 if (c == '\r')
239 {
240 if (p->_version_hash == HASH_HTTP11)
242 else if (p->_version_hash == HASH_HTTP10)
244 else
247 }
248 else
249 {
250 p->_version_hash = (p->_version_hash ^ byte) * FNV_PRIME;
251 }
252 break;
253
255 if (c == '\n')
256 {
258 p->current_token_idx = 0;
259 }
260 else
261 {
263 }
264 break;
265
267 if (c == '\r')
268 {
269 if (p->current_token_idx == 0)
270 {
271 // Blank line - end of headers
273 }
274 else
275 {
276 // CR mid-key: malformed (RFC 7230 §3.2 requires CRLF after value)
278 }
279 }
280 else if (c == ':')
281 {
282 // Terminate the scratch key so Host / Content-Length detection works
283 // regardless of whether this header is stored (header_count < MAX).
285 p->cur_key[k] = '\0';
287 p->current_token_idx = 0;
288#if DETWS_CAPTURE_AUTH_HEADER
289 // The Authorization value (Digest / JWT bearer) exceeds MAX_VAL_LEN,
290 // so capture it whole into a dedicated buffer independent of scratch.
291 p->cur_is_auth = (strcasecmp(p->cur_key, "Authorization") == 0);
292 if (p->cur_is_auth)
293 p->auth_idx = 0;
294#endif
295 }
296 else if (!is_tchar(byte))
297 {
298 // RFC 7230 §3.2: field-name = token; any non-tchar is malformed
300 }
301 else
302 {
303 uint8_t h = p->header_count;
304 if (p->current_token_idx < MAX_KEY_LEN - 1)
305 {
306 // Always capture into the scratch key; also store into the
307 // header slot when one is still available.
308 p->cur_key[p->current_token_idx] = c;
309 if (h < MAX_HEADERS)
310 p->headers[h].key[p->current_token_idx] = c;
312 }
313 // An over-long key is silently capped (a capacity limit, not an
314 // error): the scratch/stored key is already full and the excess is
315 // ignored. A truncated key cannot match the short Host/Content-Length
316 // names, and not failing the request keeps long but valid header names
317 // (CORS, Sec-WebSocket-Extensions, ...) working. Mirrors the value path.
318 }
319 break;
320
322 // Strip leading OWS (SP or HTAB) after the colon - RFC 9110 §5.6.3
323 if ((c == ' ' || c == '\t') && p->current_token_idx == 0)
324 break;
325 if (c == '\r')
326 {
327 uint8_t h = p->header_count;
328
329 // Terminate the scratch value so detection sees a clean C string.
330 size_t vlen = p->current_token_idx < MAX_VAL_LEN ? p->current_token_idx : MAX_VAL_LEN - 1;
331 p->cur_val[vlen] = '\0';
332#if DETWS_CAPTURE_AUTH_HEADER
333 if (p->cur_is_auth)
334 {
335 p->authorization[p->auth_idx] = '\0';
336 p->cur_is_auth = false;
337 }
338#endif
339
340 // Host / Content-Length detection works off the scratch copies, so
341 // it is correct even for headers past MAX_HEADERS (RFC 7230 §5.4,
342 // §3.3.2).
343 if (strcasecmp(p->cur_key, "Host") == 0)
344 p->host_count++;
345
346 if (strcasecmp(p->cur_key, "Content-Length") == 0)
347 {
348 // RFC 7230 §3.3.2: Content-Length = 1*DIGIT.
349 size_t cl = 0;
350 bool valid = (p->cur_val[0] != '\0');
351 for (const char *q = p->cur_val; *q; q++)
352 {
353 if (*q < '0' || *q > '9')
354 {
355 valid = false;
356 break;
357 }
358 cl = cl * 10 + (size_t)(*q - '0');
359 }
360 // A non-numeric value, or a second Content-Length whose value
361 // disagrees with the first, is a fatal framing error (request
362 // smuggling vector) → 400.
363 if (!valid || (p->content_length_count > 0 && cl != p->content_length))
364 {
366 break;
367 }
368 p->content_length = cl;
370 }
371
372 // RFC 9112 §6.1/§6.3: this server does not decode chunked request bodies,
373 // and a Transfer-Encoding present with (or instead of) Content-Length is a
374 // request-smuggling vector - the chunked octets would otherwise be left in
375 // the buffer and reparsed as the next request. Reject any request bearing
376 // Transfer-Encoding (fail closed).
377 if (strcasecmp(p->cur_key, "Transfer-Encoding") == 0)
378 {
380 break;
381 }
382
383 if (h < MAX_HEADERS)
384 p->header_count++;
385
387 p->current_token_idx = 0;
388 }
389 else if (!is_field_value_char(byte))
390 {
391 // RFC 7230 §3.2: control chars and NUL are not valid in field values
393 }
394 else
395 {
396#if DETWS_CAPTURE_AUTH_HEADER
397 // Capture the full Authorization value (Digest / JWT) past MAX_VAL_LEN.
398 if (p->cur_is_auth && p->auth_idx < DETWS_AUTH_HDR_CAP - 1)
399 p->authorization[p->auth_idx++] = c;
400#endif
401 if (p->current_token_idx < MAX_VAL_LEN - 1)
402 {
403 // Always capture into the scratch value; also store into the
404 // header slot when one is still available.
405 uint8_t h = p->header_count;
406 p->cur_val[p->current_token_idx] = c;
407 if (h < MAX_HEADERS)
408 p->headers[h].val[p->current_token_idx] = c;
410 }
411 // Silently truncate the scratch/stored value - capacity limit, not an error.
412 }
413 break;
414
416 /*
417 * Consumes the LF of the blank-line CRLF that ends the header block.
418 * Decides the next state based on Content-Length:
419 * > BODY_BUF_SIZE → 413 Payload Too Large
420 * == 0 → ParseState::PARSE_COMPLETE (no body)
421 * else → ParseState::PARSE_BODY
422 */
423 if (c == '\n')
424 {
425 // RFC 7230 §5.4: a request MUST NOT carry more than one Host header
426 // (always enforced); an HTTP/1.1 request MUST carry exactly one Host
427 // header (enforced only when DETWS_ENFORCE_HOST_HEADER is set).
428 bool host_violation = (p->host_count > 1);
429#if DETWS_ENFORCE_HOST_HEADER
430 if (p->version == HttpVersion::HTTP_11 && p->host_count == 0)
431 host_violation = true;
432#endif
433 if (host_violation)
435#if DETWS_ENABLE_STREAM_BODY
436 // Streaming sink (OTA / upload): all headers are parsed here, so the
437 // hook can match method/path/Authorization and begin a sink (Update
438 // or a file). If it accepts, the body streams in chunks and the size
439 // cap is bypassed; the matching route handler still runs at COMPLETE.
440 else if (p->content_length > 0 && s_hp.stream_begin && s_hp.stream_begin(p))
441 {
442 p->body_streaming = true;
444 }
445#endif
446 else if (p->content_length > BODY_BUF_SIZE)
448 else if (p->content_length == 0)
449 {
450 p->body[0] = '\0';
452 }
453 else
454 {
456 }
457 }
458 else
459 {
461 }
462 break;
463
465 // Body is opaque data - no character validation.
466#if DETWS_ENABLE_STREAM_BODY
467 if (p->body_streaming)
468 {
469 // Reuse body[] as a flush buffer: fill it, then hand whole chunks to
470 // the sink. No BODY_BUF_SIZE cap on the total - the body never lives
471 // in RAM all at once.
472 p->body[p->body_len++] = byte;
473 if (p->body_len == BODY_BUF_SIZE)
474 {
475 if (s_hp.stream_data)
476 s_hp.stream_data(p, p->body, p->body_len);
477 p->body_len = 0;
478 }
479 p->body_bytes_read++;
480 if (p->body_bytes_read >= p->content_length)
481 {
482 if (p->body_len && s_hp.stream_data)
483 s_hp.stream_data(p, p->body, p->body_len); // flush the tail
484 p->body_len = 0;
485 p->body[0] = '\0';
487 }
488 break;
489 }
490#endif
491 if (p->body_len < BODY_BUF_SIZE)
492 p->body[p->body_len++] = byte;
493 p->body_bytes_read++;
494 if (p->body_bytes_read >= p->content_length)
495 {
496 p->body[p->body_len] = '\0';
498 }
499 break;
500
501 default:
502 break;
503 }
504}
505
506const char *http_get_header(const HttpReq *req, const char *key)
507{
508 for (uint8_t i = 0; i < req->header_count; i++)
509 {
510 if (strcasecmp(req->headers[i].key, key) == 0)
511 return req->headers[i].val;
512 }
513 return nullptr;
514}
515
516bool http_get_cookie(const HttpReq *req, const char *name, char *out, size_t out_size)
517{
518 if (out == nullptr || out_size == 0)
519 return false;
520 out[0] = '\0';
521 if (req == nullptr || name == nullptr || name[0] == '\0')
522 return false;
523
524 // RFC 6265 4.2.1: the request "Cookie" header is "name1=value1; name2=value2".
525 // Names are case-sensitive; a value may be DQUOTE-wrapped.
526 const char *c = http_get_header(req, "Cookie");
527 if (c == nullptr)
528 return false;
529 size_t nlen = strnlen(name, MAX_VAL_LEN); // a matchable cookie-name span cannot exceed a header value
530
531 const char *p = c;
532 while (*p != '\0')
533 {
534 while (*p == ' ' || *p == '\t' || *p == ';') // skip inter-pair separators/spaces
535 p++;
536 if (*p == '\0')
537 break;
538 const char *eq = p;
539 while (*eq != '\0' && *eq != '=' && *eq != ';') // cookie-name runs up to '='
540 eq++;
541 if (*eq == '=' && (size_t)(eq - p) == nlen && strncmp(p, name, nlen) == 0)
542 {
543 const char *v = eq + 1;
544 const char *end = v;
545 while (*end != '\0' && *end != ';') // value runs up to the next ';'
546 end++;
547 while (end > v && (end[-1] == ' ' || end[-1] == '\t')) // trim trailing OWS
548 end--;
549 size_t vlen = (size_t)(end - v);
550 if (vlen >= 2 && v[0] == '"' && v[vlen - 1] == '"') // strip a quoted cookie-value
551 {
552 v++;
553 vlen -= 2;
554 }
555 if (vlen >= out_size)
556 vlen = out_size - 1;
557 memcpy(out, v, vlen);
558 out[vlen] = '\0';
559 return true;
560 }
561 p = eq;
562 while (*p != '\0' && *p != ';') // advance past this pair
563 p++;
564 }
565 return false;
566}
567
568// Extract and validate a Forwarded / X-Forwarded-For client-address token from
569// [s, s+n) into out (canonical text). Accepts IPv4 with an optional ":port", a
570// bracketed IPv6 "[2001:db8::1]:port" (RFC 7239 §6), and a bare IPv6 (the de-facto
571// X-Forwarded-For form). The candidate is confirmed with det_ip_parse, so "unknown",
572// an obfuscated "_id" identifier (RFC 7239 §6.3), or any malformed token returns
573// false. Returns true and writes the RFC 5952 canonical address on success.
574static bool fwd_extract_client(const char *s, size_t n, char *out, size_t cap)
575{
576 // Trim leading/trailing OWS and a wrapping DQUOTE (RFC 7239 quotes the v6+port form).
577 while (n > 0 && (*s == ' ' || *s == '\t'))
578 {
579 s++;
580 n--;
581 }
582 while (n > 0 && (s[n - 1] == ' ' || s[n - 1] == '\t'))
583 n--;
584 if (n >= 2 && s[0] == '"' && s[n - 1] == '"')
585 {
586 s++;
587 n -= 2;
588 }
589 if (n == 0)
590 return false;
591
592 char tok[DET_IP_STR_MAX];
593 size_t tlen = 0;
594 if (s[0] == '[')
595 {
596 // Bracketed IPv6: take the text between '[' and ']'; a trailing ":port" is ignored.
597 size_t i = 1;
598 for (; i < n && s[i] != ']'; i++)
599 {
600 if (tlen + 1 >= sizeof(tok))
601 return false;
602 tok[tlen++] = s[i];
603 }
604 if (i >= n) // unterminated bracket
605 return false;
606 }
607 else
608 {
609 // A single colon means "IPv4:port" (address up to the colon); two or more colons
610 // mean a bare IPv6 literal (kept whole - no port stripping).
611 int colons = 0;
612 for (size_t i = 0; i < n; i++)
613 if (s[i] == ':')
614 colons++;
615 size_t take = n;
616 if (colons <= 1)
617 for (size_t i = 0; i < n; i++)
618 if (s[i] == ':')
619 {
620 take = i;
621 break;
622 }
623 if (take == 0 || take + 1 > sizeof(tok))
624 return false;
625 memcpy(tok, s, take);
626 tlen = take;
627 }
628 tok[tlen] = '\0';
629
630 DetIp ip;
631 if (!det_ip_parse(tok, &ip)) // rejects "unknown" / "_obf" / malformed
632 return false;
633 return det_ip_format(&ip, out, cap) > 0; // false if out is too small for the canonical text
634}
635
636bool http_forwarded_client(const HttpReq *req, char *ip_out, size_t ip_cap, bool *is_https)
637{
638 if (is_https)
639 *is_https = false;
640 if (!ip_out || ip_cap == 0 || !req)
641 return false;
642 ip_out[0] = '\0';
643
644 // Prefer RFC 7239 "Forwarded" (the leftmost element is the original client):
645 // Forwarded: for=192.0.2.60;proto=https, for=198.51.100.1
646 const char *fwd = http_get_header(req, "Forwarded");
647 if (fwd)
648 {
649 // First element = up to the first ','. Within it, find for= and proto=.
650 const char *elem_end = strchr(fwd, ',');
651 size_t elen = elem_end ? (size_t)(elem_end - fwd) : strnlen(fwd, MAX_VAL_LEN);
652 // proto=
653 if (is_https)
654 {
655 // Only the first element's proto= matters, so this is a single check.
656 const char *hit = strstr(fwd, "proto=");
657 if (hit && (size_t)(hit - fwd) < elen)
658 *is_https = (strncasecmp(hit + 6, "https", 5) == 0);
659 }
660 // for=
661 const char *f = strstr(fwd, "for=");
662 if (f && (size_t)(f - fwd) < elen)
663 {
664 const char *fv = f + 4;
665 const char *fend = fv;
666 size_t lim = elen - (size_t)(fv - fwd);
667 size_t k = 0;
668 while (k < lim && fend[k] != ';' && fend[k] != ',')
669 k++;
670 if (fwd_extract_client(fv, k, ip_out, ip_cap))
671 return true;
672 }
673 }
674
675 // De-facto X-Forwarded-For (comma list; leftmost = original client) + X-Forwarded-Proto.
676 if (is_https)
677 {
678 const char *xfp = http_get_header(req, "X-Forwarded-Proto");
679 if (xfp && strncasecmp(xfp, "https", 5) == 0)
680 *is_https = true;
681 }
682 const char *xff = http_get_header(req, "X-Forwarded-For");
683 if (xff)
684 {
685 const char *end = strchr(xff, ',');
686 size_t len = end ? (size_t)(end - xff) : strnlen(xff, MAX_VAL_LEN);
687 if (fwd_extract_client(xff, len, ip_out, ip_cap))
688 return true;
689 }
690 return false;
691}
692
693const char *http_get_query(const HttpReq *req, const char *key)
694{
695 for (uint8_t i = 0; i < req->query_count; i++)
696 {
697 if (strcmp(req->query_params[i].key, key) == 0)
698 return req->query_params[i].val;
699 }
700 return nullptr;
701}
702
703bool http_get_form(const HttpReq *req, const char *key, char *out, size_t out_size)
704{
705 if (out == nullptr || out_size == 0)
706 return false;
707 out[0] = '\0';
708 if (req == nullptr || key == nullptr)
709 return false;
710
711 // Only urlencoded bodies (allow a trailing "; charset=..." suffix).
712 const char *ct = http_get_header(req, "Content-Type");
713 if (ct == nullptr || strncasecmp(ct, "application/x-www-form-urlencoded", 33) != 0)
714 return false;
715
716 const char *body = (const char *)req->body;
717 size_t len = req->body_len;
718 size_t key_len = strnlen(key, len + 1); // a matchable body key cannot exceed the body length
719 size_t i = 0;
720
721 while (i < len)
722 {
723 size_t ks = i;
724 while (i < len && body[i] != '=' && body[i] != '&')
725 i++;
726 bool key_matches = (i - ks == key_len) && (strncmp(body + ks, key, key_len) == 0);
727
728 size_t vs = i;
729 size_t ve = i;
730 if (i < len && body[i] == '=')
731 {
732 vs = ++i;
733 while (i < len && body[i] != '&')
734 i++;
735 ve = i;
736 }
737 if (i < len && body[i] == '&')
738 i++;
739
740 if (key_matches)
741 {
742 size_t vlen = ve - vs;
743 if (vlen > out_size - 1)
744 vlen = out_size - 1;
745 memcpy(out, body + vs, vlen);
746 out[vlen] = '\0';
747 return true;
748 }
749 }
750 return false;
751}
752
753const char *http_get_param(const HttpReq *req, const char *key)
754{
755 if (req == nullptr || key == nullptr)
756 return nullptr;
757 for (uint8_t i = 0; i < req->path_param_count; i++)
758 {
759 if (strcmp(req->path_params[i].key, key) == 0)
760 return req->path_params[i].val;
761 }
762 return nullptr;
763}
#define CONN_POOL_SLOTS
#define MAX_VAL_LEN
Maximum header field-value length.
#define MAX_QUERY_PARAMS
Maximum number of parsed query-string parameters.
#define MAX_QUERY_LEN
Maximum raw query-string length (everything after ?).
#define QUERY_VAL_LEN
Maximum query-parameter value length.
#define BODY_BUF_SIZE
Maximum request body bytes stored in HttpReq::body.
#define MAX_HEADERS
Maximum HTTP headers stored per request.
#define MAX_PATH_LEN
Maximum URL path length (including leading /).
#define QUERY_KEY_LEN
Maximum query-parameter key length.
#define MAX_KEY_LEN
Maximum header field-name length (e.g. "Content-Type").
#define DETWS_AUTH_HDR_CAP
const char * http_get_param(const HttpReq *req, const char *key)
Look up a captured path parameter by name (case-sensitive).
void http_parser_reset(HttpReq *req)
Reset a parser context to the initial (ParseState::PARSE_METHOD) state.
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_get_cookie(const HttpReq *req, const char *name, char *out, size_t out_size)
Read a named cookie from the request Cookie header (RFC 6265 4.2.1).
void http_parser_feed(HttpReq *p, uint8_t byte)
Feed one byte to the parser state machine.
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...
bool http_get_form(const HttpReq *req, const char *key, char *out, size_t out_size)
Look up an application/x-www-form-urlencoded body field by name.
const char * http_get_query(const HttpReq *req, const char *key)
Look up a query parameter value by name (case-sensitive).
Standalone HTTP/1.1 request parser - no transport dependency.
@ PARSE_QUERY
Reading the raw query string (after ?).
@ PARSE_METHOD
Reading the HTTP method (GET, POST, …).
@ 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_HEADER_VAL
Reading a header field value.
@ PARSE_VERSION
Accumulating HTTP/1.x - hashed for validation.
@ PARSE_EXPECT_BODY_LF
Consuming the LF of the blank-line CRLF.
@ PARSE_HEADER_KEY
Reading a header field name.
@ PARSE_PATH
Reading the URL path component.
@ PARSE_URI_TOO_LONG
Path exceeds MAX_PATH_LEN → 414.
@ PARSE_EXPECT_LF
Consuming the LF of a header-line CRLF pair.
@ HTTP_10
HTTP/1.0 - close semantics by default.
@ HTTP_11
HTTP/1.1 - persistent connection by default.
@ HTTP_UNKNOWN
Version string did not match any known token.
size_t det_ip_format(const DetIp *ip, char *out, size_t cap)
Format ip into out as its RFC 5952 canonical text.
Definition ip.cpp:388
bool det_ip_parse(const char *s, DetIp *out)
Parse an IPv4 or IPv6 textual address (RFC 4291 §2.2) into out.
Definition ip.cpp:350
Layer 3 (Network) - a family-tagged IP address (IPv4 or IPv6) with RFC-faithful text parsing,...
#define DET_IP_STR_MAX
Longest text an det_ip_format can produce, including the NUL (RFC 5952 v4-mapped).
Definition ip.h:58
A v4 or v6 address in network (big-endian) byte order.
Definition ip.h:52
char val[MAX_VAL_LEN]
Field value, null-terminated.
Definition http_parser.h:92
char key[MAX_KEY_LEN]
Field name, null-terminated.
Definition http_parser.h:91
Fully-parsed HTTP/1.1 request.
QueryParam query_params[MAX_QUERY_PARAMS]
Parsed key=value pairs.
Header headers[MAX_HEADERS]
Captured header fields.
char query[MAX_QUERY_LEN]
Raw query string (after ?).
char method[DETWS_METHOD_BUF_SIZE]
HTTP method, null-terminated (OPTIONS, or WebDAV methods when enabled).
uint32_t _version_hash
FNV-1a accumulator for version validation (internal).
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[].
size_t current_token_idx
Write cursor shared by key/value sub-states.
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).
char cur_key[MAX_KEY_LEN]
Field-name of the in-progress header.
uint8_t path_param_count
Valid entries in path_params[].
uint8_t slot_id
Transport slot index (set by presentation layer).
uint8_t content_length_count
Number of Content-Length fields seen (RFC 7230 §3.3.2).
size_t body_len
Bytes stored in body[] (≤ BODY_BUF_SIZE).
HttpVersion version
Protocol version parsed from the request line.
uint8_t host_count
Number of Host fields seen (RFC 7230 §5.4).
char cur_val[MAX_VAL_LEN]
Field-value of the in-progress header.
char path[MAX_PATH_LEN]
URL path, null-terminated; no query string.
uint8_t query_count
Valid entries in query_params[].
size_t body_bytes_read
Body bytes received (may exceed BODY_BUF_SIZE).
size_t query_idx
Write cursor into query[].
A single parsed query-string parameter.
Definition http_parser.h:97
char val[QUERY_VAL_LEN]
Parameter value (empty string if absent).
Definition http_parser.h:99
char key[QUERY_KEY_LEN]
Parameter name, null-terminated.
Definition http_parser.h:98