ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
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 PC_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 // PC_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
46#define PC_FNV_OFFSET 2166136261u
47#define PC_FNV_PRIME 16777619u
48
49static constexpr uint32_t fnv1a(const char *s, uint32_t h = PC_FNV_OFFSET)
50{
51 return *s ? fnv1a(s + 1, (h ^ (uint8_t)*s) * PC_FNV_PRIME) : h;
52}
53
54#define PC_HASH_HTTP10 (fnv1a("HTTP/1.0"))
55#define PC_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
69#define PC_CC_TCHAR 0x01
70#define PC_CC_VCHAR 0x02
71#define PC_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] & PC_CC_TCHAR) != 0;
97}
98static inline bool is_vchar(uint8_t b)
99{
100 return (kCharClass[b] & PC_CC_VCHAR) != 0;
101}
102static inline bool is_field_value_char(uint8_t b)
103{
104 return (kCharClass[b] & PC_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 {
132 break;
133 }
134 if (c == '=' && !in_val)
135 {
136 in_val = true;
137 continue;
138 }
139 if (!in_val && key_idx < QUERY_KEY_LEN - 1)
140 {
141 qp->key[key_idx++] = c;
142 }
143 else if (in_val && val_idx < QUERY_VAL_LEN - 1)
144 {
145 qp->val[val_idx++] = c;
146 }
147 }
148
149 if (key_idx > 0)
150 {
151 req->query_count++;
152 }
153 }
154}
155
157{
158 uint8_t id = req->slot_id;
159#if PC_ENABLE_STREAM_BODY
160 // A streamed body that never reached ParseState::PARSE_COMPLETE is being torn down (peer
161 // reset / timeout / error): let the sink release its resource before we wipe
162 // the state. The normal-completion reset runs while parse_state==ParseState::PARSE_COMPLETE
163 // (the handler already finished the sink), so this fires only on abort.
164 if (req->body_streaming && req->parse_state != ParseState::PARSE_COMPLETE && s_hp.stream_abort)
165 {
166 s_hp.stream_abort(req);
167 }
168#endif
169 *req = {}; // zero all fields
170 req->slot_id = id; // restore slot identity
172 req->_version_hash = PC_FNV_OFFSET; // seed the FNV-1a accumulator
173}
174
175void http_parser_feed(HttpReq *p, uint8_t byte)
176{
177 // Terminal states (PARSE_COMPLETE / PARSE_ERROR / PARSE_ENTITY_TOO_LARGE / PARSE_URI_TOO_LONG) have no case
178 // below, so they fall through to `default:` and no-op - no separate guard switch on the per-byte hot path.
179 char c = (char)byte;
180
181 switch (p->parse_state)
182 {
183
185 if (c == ' ')
186 {
188 p->current_token_idx = 0;
189 }
190 else if (!is_tchar(byte))
191 {
192 // RFC 7230 §3.1.1: method = token; any non-tchar is malformed
194 }
195 else if (p->current_token_idx < sizeof(p->method) - 1)
196 {
197 p->method[p->current_token_idx++] = c;
198 }
199 else
200 {
202 }
203 break;
204
206 if (c == ' ')
207 {
209 }
210 else if (c == '?')
211 {
213 }
214 else if (!is_vchar(byte))
215 {
216 // RFC 3986 §3.3: path chars must be visible ASCII (or pct-encoded)
218 }
219 else if (p->path_idx < MAX_PATH_LEN - 1)
220 {
221 p->path[p->path_idx++] = c;
222 }
223 else
224 {
226 }
227 break;
228
230 if (c == ' ')
231 {
232 parse_query_params(p);
234 }
235 else if (!is_vchar(byte))
236 {
237 // Control chars and NUL are not valid query-string bytes
239 }
240 else if (p->query_idx < MAX_QUERY_LEN - 1)
241 {
242 p->query[p->query_idx++] = c;
243 }
244 // Silently truncate - query overflow is a capacity limit, not a protocol error
245 break;
246
248 if (c == '\r')
249 {
251 {
253 }
254 else if (p->_version_hash == PC_HASH_HTTP10)
255 {
257 }
258 else
259 {
261 }
263 }
264 else
265 {
266 p->_version_hash = (p->_version_hash ^ byte) * PC_FNV_PRIME;
267 }
268 break;
269
271 if (c == '\n')
272 {
274 p->current_token_idx = 0;
275 }
276 else
277 {
279 }
280 break;
281
283 if (c == '\r')
284 {
285 if (p->current_token_idx == 0)
286 {
287 // Blank line - end of headers
289 }
290 else
291 {
292 // CR mid-key: malformed (RFC 7230 §3.2 requires CRLF after value)
294 }
295 }
296 else if (c == ':')
297 {
298 // Terminate the scratch key so Host / Content-Length detection works
299 // regardless of whether this header is stored (header_count < MAX).
301 p->cur_key[k] = '\0';
303 p->current_token_idx = 0;
304#if PC_CAPTURE_AUTH_HEADER
305 // The Authorization value (Digest / JWT bearer) exceeds MAX_VAL_LEN,
306 // so capture it whole into a dedicated buffer independent of scratch.
307 p->cur_is_auth = (strcasecmp(p->cur_key, "Authorization") == 0);
308 if (p->cur_is_auth)
309 {
310 p->auth_idx = 0;
311 }
312#endif
313 }
314 else if (!is_tchar(byte))
315 {
316 // RFC 7230 §3.2: field-name = token; any non-tchar is malformed
318 }
319 else
320 {
321 uint8_t h = p->header_count;
322 if (p->current_token_idx < MAX_KEY_LEN - 1)
323 {
324 // Always capture into the scratch key; also store into the
325 // header slot when one is still available.
326 p->cur_key[p->current_token_idx] = c;
327 if (h < MAX_HEADERS)
328 {
329 p->headers[h].key[p->current_token_idx] = c;
330 }
332 }
333 // An over-long key is silently capped (a capacity limit, not an
334 // error): the scratch/stored key is already full and the excess is
335 // ignored. A truncated key cannot match the short Host/Content-Length
336 // names, and not failing the request keeps long but valid header names
337 // (CORS, Sec-WebSocket-Extensions, ...) working. Mirrors the value path.
338 }
339 break;
340
342 // Strip leading OWS (SP or HTAB) after the colon - RFC 9110 §5.6.3
343 if ((c == ' ' || c == '\t') && p->current_token_idx == 0)
344 {
345 break;
346 }
347 if (c == '\r')
348 {
349 uint8_t h = p->header_count;
350
351 // Terminate the scratch value so detection sees a clean C string.
352 size_t vlen = p->current_token_idx < MAX_VAL_LEN ? p->current_token_idx : MAX_VAL_LEN - 1;
353 p->cur_val[vlen] = '\0';
354#if PC_CAPTURE_AUTH_HEADER
355 if (p->cur_is_auth)
356 {
357 p->authorization[p->auth_idx] = '\0';
358 p->cur_is_auth = false;
359 }
360#endif
361
362 // Host / Content-Length detection works off the scratch copies, so
363 // it is correct even for headers past MAX_HEADERS (RFC 7230 §5.4,
364 // §3.3.2).
365 if (strcasecmp(p->cur_key, "Host") == 0)
366 {
367 p->host_count++;
368 }
369
370 if (strcasecmp(p->cur_key, "Content-Length") == 0)
371 {
372 // RFC 7230 §3.3.2: Content-Length = 1*DIGIT.
373 size_t cl = 0;
374 bool valid = (p->cur_val[0] != '\0');
375 for (const char *q = p->cur_val; *q; q++)
376 {
377 if (*q < '0' || *q > '9')
378 {
379 valid = false;
380 break;
381 }
382 cl = cl * 10 + (size_t)(*q - '0');
383 }
384 // A non-numeric value, or a second Content-Length whose value
385 // disagrees with the first, is a fatal framing error (request
386 // smuggling vector) → 400.
387 if (!valid || (p->content_length_count > 0 && cl != p->content_length))
388 {
390 break;
391 }
392 p->content_length = cl;
394 }
395
396 // RFC 9112 §6.1/§6.3: this server does not decode chunked request bodies,
397 // and a Transfer-Encoding present with (or instead of) Content-Length is a
398 // request-smuggling vector - the chunked octets would otherwise be left in
399 // the buffer and reparsed as the next request. Reject any request bearing
400 // Transfer-Encoding (fail closed).
401 if (strcasecmp(p->cur_key, "Transfer-Encoding") == 0)
402 {
404 break;
405 }
406
407 if (h < MAX_HEADERS)
408 {
409 p->header_count++;
410 }
411
413 p->current_token_idx = 0;
414 }
415 else if (!is_field_value_char(byte))
416 {
417 // RFC 7230 §3.2: control chars and NUL are not valid in field values
419 }
420 else
421 {
422#if PC_CAPTURE_AUTH_HEADER
423 // Capture the full Authorization value (Digest / JWT) past MAX_VAL_LEN.
424 if (p->cur_is_auth && p->auth_idx < PC_AUTH_HDR_CAP - 1)
425 {
426 p->authorization[p->auth_idx++] = c;
427 }
428#endif
429 if (p->current_token_idx < MAX_VAL_LEN - 1)
430 {
431 // Always capture into the scratch value; also store into the
432 // header slot when one is still available.
433 uint8_t h = p->header_count;
434 p->cur_val[p->current_token_idx] = c;
435 if (h < MAX_HEADERS)
436 {
437 p->headers[h].val[p->current_token_idx] = c;
438 }
440 }
441 // Silently truncate the scratch/stored value - capacity limit, not an error.
442 }
443 break;
444
446 /*
447 * Consumes the LF of the blank-line CRLF that ends the header block.
448 * Decides the next state based on Content-Length:
449 * > BODY_BUF_SIZE → 413 Payload Too Large
450 * == 0 → ParseState::PARSE_COMPLETE (no body)
451 * else → ParseState::PARSE_BODY
452 */
453 if (c == '\n')
454 {
455 // RFC 7230 §5.4: a request MUST NOT carry more than one Host header
456 // (always enforced); an HTTP/1.1 request MUST carry exactly one Host
457 // header (enforced only when PC_ENFORCE_HOST_HEADER is set).
458 bool host_violation = (p->host_count > 1);
459#if PC_ENFORCE_HOST_HEADER
460 if (p->version == HttpVersion::HTTP_11 && p->host_count == 0)
461 {
462 host_violation = true;
463 }
464#endif
465 if (host_violation)
466 {
468 }
469#if PC_ENABLE_STREAM_BODY
470 // Streaming sink (OTA / upload): all headers are parsed here, so the
471 // hook can match method/path/Authorization and begin a sink (Update
472 // or a file). If it accepts, the body streams in chunks and the size
473 // cap is bypassed; the matching route handler still runs at COMPLETE.
474 else if (p->content_length > 0 && s_hp.stream_begin && s_hp.stream_begin(p))
475 {
476 p->body_streaming = true;
478 }
479#endif
480 else if (p->content_length > BODY_BUF_SIZE)
481 {
483 }
484 else if (p->content_length == 0)
485 {
486 p->body[0] = '\0';
488 }
489 else
490 {
492 }
493 }
494 else
495 {
497 }
498 break;
499
501 // Body is opaque data - no character validation.
502#if PC_ENABLE_STREAM_BODY
503 if (p->body_streaming)
504 {
505 // Reuse body[] as a flush buffer: fill it, then hand whole chunks to
506 // the sink. No BODY_BUF_SIZE cap on the total - the body never lives
507 // in RAM all at once.
508 p->body[p->body_len++] = byte;
509 if (p->body_len == BODY_BUF_SIZE)
510 {
511 if (s_hp.stream_data)
512 {
513 s_hp.stream_data(p, p->body, p->body_len);
514 }
515 p->body_len = 0;
516 }
517 p->body_bytes_read++;
518 if (p->body_bytes_read >= p->content_length)
519 {
520 if (p->body_len && s_hp.stream_data)
521 {
522 s_hp.stream_data(p, p->body, p->body_len); // flush the tail
523 }
524 p->body_len = 0;
525 p->body[0] = '\0';
527 }
528 break;
529 }
530#endif
531 if (p->body_len < BODY_BUF_SIZE)
532 {
533 p->body[p->body_len++] = byte;
534 }
535 p->body_bytes_read++;
536 if (p->body_bytes_read >= p->content_length)
537 {
538 p->body[p->body_len] = '\0';
540 }
541 break;
542
543 default:
544 break;
545 }
546}
547
548const char *http_get_header(const HttpReq *req, const char *key)
549{
550 for (uint8_t i = 0; i < req->header_count; i++)
551 {
552 if (strcasecmp(req->headers[i].key, key) == 0)
553 {
554 return req->headers[i].val;
555 }
556 }
557 return nullptr;
558}
559
560bool http_get_cookie(const HttpReq *req, const char *name, char *out, size_t out_size)
561{
562 if (out == nullptr || out_size == 0)
563 {
564 return false;
565 }
566 out[0] = '\0';
567 if (req == nullptr || name == nullptr || name[0] == '\0')
568 {
569 return false;
570 }
571
572 // RFC 6265 4.2.1: the request "Cookie" header is "name1=value1; name2=value2".
573 // Names are case-sensitive; a value may be DQUOTE-wrapped.
574 const char *c = http_get_header(req, "Cookie");
575 if (c == nullptr)
576 {
577 return false;
578 }
579 size_t nlen = strnlen(name, MAX_VAL_LEN); // a matchable cookie-name span cannot exceed a header value
580
581 const char *p = c;
582 while (*p != '\0')
583 {
584 while (*p == ' ' || *p == '\t' || *p == ';') // skip inter-pair separators/spaces
585 {
586 p++;
587 }
588 if (*p == '\0')
589 {
590 break;
591 }
592 const char *eq = p;
593 while (*eq != '\0' && *eq != '=' && *eq != ';') // cookie-name runs up to '='
594 {
595 eq++;
596 }
597 if (*eq == '=' && (size_t)(eq - p) == nlen && strncmp(p, name, nlen) == 0)
598 {
599 const char *v = eq + 1;
600 const char *end = v;
601 while (*end != '\0' && *end != ';') // value runs up to the next ';'
602 {
603 end++;
604 }
605 while (end > v && (end[-1] == ' ' || end[-1] == '\t')) // trim trailing OWS
606 {
607 end--;
608 }
609 size_t vlen = (size_t)(end - v);
610 if (vlen >= 2 && v[0] == '"' && v[vlen - 1] == '"') // strip a quoted cookie-value
611 {
612 v++;
613 vlen -= 2;
614 }
615 if (vlen >= out_size)
616 {
617 vlen = out_size - 1;
618 }
619 memcpy(out, v, vlen);
620 out[vlen] = '\0';
621 return true;
622 }
623 p = eq;
624 while (*p != '\0' && *p != ';') // advance past this pair
625 {
626 p++;
627 }
628 }
629 return false;
630}
631
632// Extract and validate a Forwarded / X-Forwarded-For client-address token from
633// [s, s+n) into out (canonical text). Accepts IPv4 with an optional ":port", a
634// bracketed IPv6 "[2001:db8::1]:port" (RFC 7239 §6), and a bare IPv6 (the de-facto
635// X-Forwarded-For form). The candidate is confirmed with pc_ip_parse, so "unknown",
636// an obfuscated "_id" identifier (RFC 7239 §6.3), or any malformed token returns
637// false. Returns true and writes the RFC 5952 canonical address on success.
638static bool fwd_extract_client(const char *s, size_t n, char *out, size_t cap)
639{
640 // Trim leading/trailing OWS and a wrapping DQUOTE (RFC 7239 quotes the v6+port form).
641 while (n > 0 && (*s == ' ' || *s == '\t'))
642 {
643 s++;
644 n--;
645 }
646 while (n > 0 && (s[n - 1] == ' ' || s[n - 1] == '\t'))
647 {
648 n--;
649 }
650 if (n >= 2 && s[0] == '"' && s[n - 1] == '"')
651 {
652 s++;
653 n -= 2;
654 }
655 if (n == 0)
656 {
657 return false;
658 }
659
660 char tok[PC_IP_STR_MAX];
661 size_t tlen = 0;
662 if (s[0] == '[')
663 {
664 // Bracketed IPv6: take the text between '[' and ']'; a trailing ":port" is ignored.
665 // Reachable: n is bounded by MAX_VAL_LEN-1 (47) only when this is the whole stored
666 // header value. Via "Forwarded: for=[...]" the "for=" prefix consumes 4 of those 47
667 // bytes before n is even measured, capping bracket content at 43 - short of the 46
668 // needed to trip this guard. But fwd_extract_client() is also called directly on
669 // X-Forwarded-For (no "for=" prefix), so the full 47 bytes are available there:
670 // '[' + 46 non-']' content bytes drives tlen to sizeof(tok)-1 (45) and trips the guard
671 // on the 46th byte.
672 size_t i = 1;
673 for (; i < n && s[i] != ']'; i++)
674 {
675 if (tlen + 1 >= sizeof(tok))
676 {
677 return false;
678 }
679 tok[tlen++] = s[i];
680 }
681 if (i >= n) // unterminated bracket
682 {
683 return false;
684 }
685 }
686 else
687 {
688 // A single colon means "IPv4:port" (address up to the colon); two or more colons
689 // mean a bare IPv6 literal (kept whole - no port stripping).
690 int colons = 0;
691 for (size_t i = 0; i < n; i++)
692 {
693 if (s[i] == ':')
694 {
695 colons++;
696 }
697 }
698 size_t take = n;
699 if (colons <= 1)
700 {
701 for (size_t i = 0; i < n; i++)
702 {
703 if (s[i] == ':')
704 {
705 take = i;
706 break;
707 }
708 }
709 }
710 if (take == 0 || take + 1 > sizeof(tok))
711 {
712 return false;
713 }
714 memcpy(tok, s, take);
715 tlen = take;
716 }
717 tok[tlen] = '\0';
718
719 pc_ip ip;
720 if (!pc_ip_parse(tok, &ip)) // rejects "unknown" / "_obf" / malformed
721 {
722 return false;
723 }
724 return pc_ip_format(&ip, out, cap) > 0; // false if out is too small for the canonical text
725}
726
727bool http_forwarded_client(const HttpReq *req, char *ip_out, size_t ip_cap, bool *is_https)
728{
729 if (is_https)
730 {
731 *is_https = false;
732 }
733 if (!ip_out || ip_cap == 0 || !req)
734 {
735 return false;
736 }
737 ip_out[0] = '\0';
738
739 // Prefer RFC 7239 "Forwarded" (the leftmost element is the original client):
740 // Forwarded: for=192.0.2.60;proto=https, for=198.51.100.1
741 const char *fwd = http_get_header(req, "Forwarded");
742 if (fwd)
743 {
744 // First element = up to the first ','. Within it, find for= and proto=.
745 const char *elem_end = strchr(fwd, ',');
746 size_t elen = elem_end ? (size_t)(elem_end - fwd) : strnlen(fwd, MAX_VAL_LEN);
747 // proto=
748 if (is_https)
749 {
750 // Only the first element's proto= matters, so this is a single check.
751 const char *hit = strstr(fwd, "proto=");
752 if (hit && (size_t)(hit - fwd) < elen)
753 {
754 *is_https = (strncasecmp(hit + 6, "https", 5) == 0);
755 }
756 }
757 // for=
758 const char *f = strstr(fwd, "for=");
759 if (f && (size_t)(f - fwd) < elen)
760 {
761 const char *fv = f + 4;
762 const char *fend = fv;
763 size_t lim = elen - (size_t)(fv - fwd);
764 size_t k = 0;
765 // fend[k]==',' is unreachable here: lim is derived from elen, which is itself the
766 // offset of the first ',' (or end of string when there is none) - so k can never
767 // walk far enough to land on a comma without k<lim failing first.
768 while (k < lim && fend[k] != ';' && fend[k] != ',') // GCOVR_EXCL_BR_LINE see above: the ',' arm can't fire
769 {
770 k++;
771 }
772 if (fwd_extract_client(fv, k, ip_out, ip_cap))
773 {
774 return true;
775 }
776 }
777 }
778
779 // De-facto X-Forwarded-For (comma list; leftmost = original client) + X-Forwarded-Proto.
780 if (is_https)
781 {
782 const char *xfp = http_get_header(req, "X-Forwarded-Proto");
783 if (xfp && strncasecmp(xfp, "https", 5) == 0)
784 {
785 *is_https = true;
786 }
787 }
788 const char *xff = http_get_header(req, "X-Forwarded-For");
789 if (xff)
790 {
791 const char *end = strchr(xff, ',');
792 size_t len = end ? (size_t)(end - xff) : strnlen(xff, MAX_VAL_LEN);
793 if (fwd_extract_client(xff, len, ip_out, ip_cap))
794 {
795 return true;
796 }
797 }
798 return false;
799}
800
801const char *http_get_query(const HttpReq *req, const char *key)
802{
803 for (uint8_t i = 0; i < req->query_count; i++)
804 {
805 if (strcmp(req->query_params[i].key, key) == 0)
806 {
807 return req->query_params[i].val;
808 }
809 }
810 return nullptr;
811}
812
813bool http_get_form(const HttpReq *req, const char *key, char *out, size_t out_size)
814{
815 if (out == nullptr || out_size == 0)
816 {
817 return false;
818 }
819 out[0] = '\0';
820 if (req == nullptr || key == nullptr)
821 {
822 return false;
823 }
824
825 // Only urlencoded bodies (allow a trailing "; charset=..." suffix).
826 const char *ct = http_get_header(req, "Content-Type");
827 if (ct == nullptr || strncasecmp(ct, "application/x-www-form-urlencoded", 33) != 0)
828 {
829 return false;
830 }
831
832 const char *body = (const char *)req->body;
833 size_t len = req->body_len;
834 size_t key_len = strnlen(key, len + 1); // a matchable body key cannot exceed the body length
835 size_t i = 0;
836
837 while (i < len)
838 {
839 size_t ks = i;
840 while (i < len && body[i] != '=' && body[i] != '&')
841 {
842 i++;
843 }
844 bool key_matches = (i - ks == key_len) && (strncmp(body + ks, key, key_len) == 0);
845
846 size_t vs = i;
847 size_t ve = i;
848 if (i < len && body[i] == '=')
849 {
850 vs = ++i;
851 while (i < len && body[i] != '&')
852 {
853 i++;
854 }
855 ve = i;
856 }
857 // body[i] != '&' here is unreachable: if the first scan above stopped because
858 // body[i]=='&' (no '=' before it), that is already the '&' we're checking for. If it
859 // instead stopped on '=', the '=' block's own scan only ever stops early on '&' - it
860 // has no '=' check - so it too can only leave body[i]=='&' when i<len. The remaining
861 // stop condition for both scans, i==len, is excluded by the i<len test itself. So
862 // whenever i < len still holds at this point, body[i] can only be '&'.
863 if (i < len && body[i] == '&') // GCOVR_EXCL_BR_LINE see above: the not-'&' arm can't fire
864 {
865 i++;
866 }
867
868 if (key_matches)
869 {
870 size_t vlen = ve - vs;
871 if (vlen > out_size - 1)
872 {
873 vlen = out_size - 1;
874 }
875 memcpy(out, body + vs, vlen);
876 out[vlen] = '\0';
877 return true;
878 }
879 }
880 return false;
881}
882
883const char *http_get_param(const HttpReq *req, const char *key)
884{
885 if (req == nullptr || key == nullptr)
886 {
887 return nullptr;
888 }
889 for (uint8_t i = 0; i < req->path_param_count; i++)
890 {
891 if (strcmp(req->path_params[i].key, key) == 0)
892 {
893 return req->path_params[i].val;
894 }
895 }
896 return nullptr;
897}
#define BODY_BUF_SIZE
Definition c2_defaults.h:67
#define MAX_HEADERS
Definition c2_defaults.h:64
const char * http_get_param(const HttpReq *req, const char *key)
Look up a captured path parameter by name (case-sensitive).
#define PC_HASH_HTTP10
#define PC_CC_TCHAR
void http_parser_reset(HttpReq *req)
Reset a parser context to the initial (ParseState::PARSE_METHOD) state.
#define PC_CC_VCHAR
const char * http_get_header(const HttpReq *req, const char *key)
Look up a header value by name (case-insensitive).
#define PC_FNV_OFFSET
#define PC_CC_FIELD_VALUE
#define PC_HASH_HTTP11
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.
#define PC_FNV_PRIME
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 pc_ip_format(const pc_ip *ip, char *out, size_t cap)
Format ip into out as its RFC 5952 canonical text.
Definition ip.cpp:488
bool pc_ip_parse(const char *s, pc_ip *out)
Parse an IPv4 or IPv6 textual address (RFC 4291 §2.2) into out.
Definition ip.cpp:436
Layer 3 (Network) - a family-tagged IP address (IPv4 or IPv6) with RFC-faithful text parsing,...
#define PC_IP_STR_MAX
Longest text an pc_ip_format can produce, including the NUL (RFC 5952 v4-mapped).
Definition ip.h:58
#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 PC_AUTH_HDR_CAP
#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").
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 ?).
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.
char method[PC_METHOD_BUF_SIZE]
HTTP method, null-terminated (OPTIONS, or WebDAV methods when enabled).
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
A v4 or v6 address in network (big-endian) byte order.
Definition ip.h:52