ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
file_serving.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 file_serving.cpp
6 * @brief Filesystem-backed static file serving for PC (GET/HEAD of an fs::FS path).
7 *
8 * Split out of protocore.cpp (single-purpose server files). Covers the conditional-GET validators
9 * (ETag / Last-Modified / If-None-Match / If-Modified-Since), byte-range requests (RFC 7233),
10 * pre-compressed .gz variants, and the cross-loop file-send pump that pages a large body out
11 * without truncating or blocking the worker. The shared RFC 1123 date helper (http_rfc1123)
12 * lives here because WEBDAV requires FILE_SERVING, so this TU is its single always-present home.
13 * Behavior is identical to the pre-split code - a pure move.
14 */
15
16#include "network_drivers/transport/tcp.h" // conn_pool, pc_conn_*, TcpConn/ConnState
17#include "protocore.h"
18#include "server/http_range.h" // http_parse_byte_range (shared with the edge cache)
20#include "shared_primitives/mime.h" // mime_type, PC_MIME_*
21#include "shared_primitives/strbuf.h" // pc_sb frame builder
22#include "shared_primitives/time_compat.h" // pc_gmtime_r (portable reentrant UTC)
23#include <stdio.h> // snprintf, sscanf
24#include <string.h> // strncasecmp, strchr, strstr, strncmp, strnlen
25#include <time.h> // strftime (RFC 1123 / conditional-GET dates) (RFC 1123 / conditional-GET dates)
26
27// ---------------------------------------------------------------------------
28// File serving
29// ---------------------------------------------------------------------------
30
31#if PC_ENABLE_FILE_SERVING
32
33// ---------------------------------------------------------------------------
34// File-send state - owned here
35// ---------------------------------------------------------------------------
36//
37// A file larger than the TCP send window cannot go out in one dispatch: tcp_write returns ERR_MEM
38// once the window fills and the remainder would be dropped. serve_file_internal sends the headers,
39// opens the file and hands it to this per-slot state; file_send_pump pages out at most
40// pc_conn_sndbuf() bytes per worker loop and resumes as the window drains. One transfer per slot.
41// Nothing outside this file can name the state: the poll asks pc_file_holds_slot().
42
43// Per-slot file-send continuation: the open file and how much of it is left.
44struct FileSend
45{
46 fs::File file; ///< open source file (held across loops).
47 size_t off; ///< absolute file offset of the next byte to send.
48 size_t remaining; ///< body bytes still to send.
49 int status; ///< response status (200 / 206) for note_response.
50 int total; ///< total body length, for the access log.
51 bool keep; ///< keep-alive vs close at completion.
52 bool active; ///< a transfer is in progress on this slot.
53};
54
55/** @brief The file-send state this TU owns, one entry per connection slot. */
56struct FileCtx
57{
58 FileSend send[MAX_CONNS];
59};
60static FileCtx s_file;
61
62bool pc_file_holds_slot(uint8_t slot)
63{
64 return s_file.send[slot].active;
65}
66
67// HTTP-date helpers (shared by file serving's Last-Modified / If-Modified-Since and
68// WebDAV's getlastmodified / creationdate). WEBDAV requires FILE_SERVING, so this is
69// the single home for both. Format a time_t as an RFC 1123 GMT date; leaves @p out
70// empty when the timestamp is zero/unavailable.
71void http_rfc1123(time_t t, char *out, size_t cap)
72{
73 out[0] = '\0';
74 if (t <= 0)
75 {
76 return;
77 }
78 struct tm tmv;
79 if (!pc_gmtime_r(&t, &tmv)) // reentrant: never the shared static buffer (worker-safe)
80 {
81 return;
82 }
83 strftime(out, cap, "%a, %d %b %Y %H:%M:%S GMT", &tmv);
84}
85
86// True if a resource last modified at @p mtime is NOT newer than the client's
87// If-Modified-Since date @p ims (RFC 1123 form), i.e. a conditional GET should
88// answer 304. Parses the date by hand (sscanf, no stdlib) and compares the two
89// broken-down times field by field, so no timegm()/epoch round-trip is needed.
90// Returns false (serve 200) when there is no usable date - mtime is 0 (no clock),
91// @p ims is absent, or it does not parse.
92static bool http_not_modified_since(time_t mtime, const char *ims)
93{
94 if (mtime <= 0 || !ims)
95 {
96 return false;
97 }
98 char mon[4] = {0};
99 int day = 0;
100 int year = 0;
101 int hh = 0;
102 int mm = 0;
103 int ss = 0;
104 // "Sun, 06 Nov 1994 08:49:37 GMT" - skip the weekday, read the rest.
105 if (sscanf(ims, "%*3s, %d %3s %d %d:%d:%d", &day, mon, &year, &hh, &mm, &ss) != 6)
106 {
107 return false;
108 }
109 static const char MONTHS[] = "JanFebMarAprMayJunJulAugSepOctNovDec";
110 const char *mp = strstr(MONTHS, mon);
111 // Must align to a 3-char month boundary: a malformed token like "ebM" appears in
112 // the table at a non-multiple-of-3 offset and would otherwise mis-parse as a month.
113 if (!mp || ((mp - MONTHS) % 3) != 0)
114 {
115 return false;
116 }
117 int imon = (int)(mp - MONTHS) / 3; // 0-based, matches struct tm tm_mon
118
119 struct tm tf;
120 if (!pc_gmtime_r(&mtime, &tf)) // reentrant: never the shared static buffer (worker-safe)
121 {
122 return false;
123 }
124 // Compare file (tf) vs If-Modified-Since fields, most significant first.
125 int fy = tf.tm_year + 1900;
126 if (fy != year)
127 {
128 return fy < year;
129 }
130 if (tf.tm_mon != imon)
131 {
132 return tf.tm_mon < imon;
133 }
134 if (tf.tm_mday != day)
135 {
136 return tf.tm_mday < day;
137 }
138 if (tf.tm_hour != hh)
139 {
140 return tf.tm_hour < hh;
141 }
142 if (tf.tm_min != mm)
143 {
144 return tf.tm_min < mm;
145 }
146 return tf.tm_sec <= ss;
147}
148
149// RFC 9110 13.1.2: If-None-Match comparison. Supports "*" (matches any current
150// representation), a comma-separated list of entity-tags, and weak comparison
151// (an inbound W/"x" matches our strong "x"). @p etag is our tag, quotes included.
152static bool inm_matches(const char *inm, const char *etag)
153{
154 // Leading OWS is stripped by the HTTP/1.x byte parser, but NOT on a semantic ingress
155 // (HTTP/2 / HTTP/3): those hand over HPACK/QPACK-decoded values verbatim, so a
156 // `if-none-match: <SP>"abc"` reaches here with the whitespace intact.
157 while (*inm == ' ' || *inm == '\t')
158 {
159 inm++;
160 }
161 if (inm[0] == '*')
162 {
163 return true; // "*" matches the existing representation
164 }
165 size_t etlen = strnlen(etag, 40);
166 const char *p = inm;
167 while (*p)
168 {
169 while (*p == ' ' || *p == '\t' || *p == ',')
170 {
171 p++;
172 }
173 if (!*p)
174 {
175 break;
176 }
177 const char *tag = p;
178 if (tag[0] == 'W' && tag[1] == '/') // weak validator: ignore the W/ prefix
179 {
180 tag += 2;
181 }
182 if (tag[0] == '"')
183 {
184 const char *end = strchr(tag + 1, '"');
185 if (end && (size_t)(end - tag + 1) == etlen && strncmp(tag, etag, etlen) == 0)
186 {
187 return true;
188 }
189 }
190 const char *comma = strchr(p, ',');
191 if (!comma)
192 {
193 break;
194 }
195 p = comma + 1;
196 }
197 return false;
198}
199
200void PC::serve_file_internal(uint8_t slot_id, bool head, fs::FS &file_sys, const char *fs_path,
201 const char *content_type, const char *content_encoding)
202{
203 fs::File f = file_sys.open(fs_path, "r");
204 if (!f)
205 {
206 send(slot_id, 404, PC_MIME_TEXT_PLAIN, "Not Found");
207 return;
208 }
209
210 if (!pc_conn_active(slot_id))
211 {
212 f.close();
213 http_reset(slot_id);
214 return;
215 }
216
217 size_t file_size = f.size();
218
219 bool keep;
220 const char *cl = pc_resp_conn_hdr(slot_id, &keep);
221
222 // Optional Content-Encoding line (e.g. gzip for pre-compressed assets).
223 char enc_line[40];
224 enc_line[0] = '\0';
225 if (content_encoding)
226 {
227 pc_sb sb_enc_line = {enc_line, sizeof(enc_line), 0, true};
228 pc_sb_put(&sb_enc_line, "Content-Encoding: ");
229 pc_sb_put(&sb_enc_line, content_encoding);
230 pc_sb_put(&sb_enc_line, "\r\n");
231 if (pc_sb_finish(&sb_enc_line) == 0)
232 {
233 enc_line[0] = '\0';
234 }
235 }
236
237#if PC_ENABLE_ETAG
238 // Conditional GET. Strong validator (ETag) from size + mtime; plus a
239 // Last-Modified date validator. A conditional request answers 304 when either
240 // the client's If-None-Match matches the ETag, or - per RFC 9110, only when no
241 // If-None-Match is present - its If-Modified-Since is not older than the file.
242 time_t mtime = f.getLastWrite();
243 char etag[40];
244 pc_sb sb_etag = {etag, sizeof(etag), 0, true};
245 pc_sb_put(&sb_etag, "\"");
246 pc_sb_hex(&sb_etag, (uint64_t)((unsigned)file_size), 1);
247 pc_sb_put(&sb_etag, "-");
248 pc_sb_hex(&sb_etag, (uint64_t)((unsigned long)mtime), 1);
249 pc_sb_put(&sb_etag, "\"");
250 if (pc_sb_finish(&sb_etag) == 0)
251 {
252 etag[0] = '\0';
253 }
254
255 char lm_date[40];
256 char lastmod_line[17 + sizeof(lm_date)]; // "Last-Modified: " + date + "\r\n" + NUL
257 lastmod_line[0] = '\0';
258 http_rfc1123(mtime, lm_date, sizeof(lm_date));
259 if (lm_date[0])
260 {
261 pc_sb sb_lastmod_line = {lastmod_line, sizeof(lastmod_line), 0, true};
262 pc_sb_put(&sb_lastmod_line, "Last-Modified: ");
263 pc_sb_put(&sb_lastmod_line, lm_date);
264 pc_sb_put(&sb_lastmod_line, "\r\n");
265 if (pc_sb_finish(&sb_lastmod_line) == 0)
266 {
267 lastmod_line[0] = '\0';
268 }
269 }
270
271 const char *inm = http_get_header(&http_pool[slot_id], "If-None-Match");
272 bool not_modified = inm ? inm_matches(inm, etag)
273 : http_not_modified_since(mtime, http_get_header(&http_pool[slot_id], "If-Modified-Since"));
274 if (not_modified)
275 {
276 f.close();
277 char h304[RESP_HDR_BUF_SIZE];
278 pc_sb sb_h304 = {h304, sizeof(h304), 0, true};
279 pc_sb_put(&sb_h304, "HTTP/1.1 304 Not Modified\r\nETag: ");
280 pc_sb_put(&sb_h304, etag);
281 pc_sb_put(&sb_h304, "\r\n");
282 pc_sb_put(&sb_h304, lastmod_line);
283 pc_sb_put(&sb_h304, _cache_control_buf);
284 pc_sb_put(&sb_h304, _cors_enabled ? _cors_header_buf : "");
285 pc_sb_put(&sb_h304, cl);
286 pc_sb_put(&sb_h304, "\r\n");
287 int n304 = (int)pc_sb_finish(&sb_h304);
288 pc_conn_send_flush(slot_id, h304, (u16_t)n304); // 304s are frequent (cache revalidation): one marshal
289 pc_resp_end(slot_id, 304, 0, keep, /*pre_flushed=*/true);
290 return;
291 }
292 char etag_line[48];
293 pc_sb sb_etag_line = {etag_line, sizeof(etag_line), 0, true};
294 pc_sb_put(&sb_etag_line, "ETag: ");
295 pc_sb_put(&sb_etag_line, etag);
296 pc_sb_put(&sb_etag_line, "\r\n");
297 if (pc_sb_finish(&sb_etag_line) == 0)
298 {
299 etag_line[0] = '\0';
300 }
301#else
302 const char *etag_line = "";
303 const char *lastmod_line = "";
304#endif
305
306 // Default: full 200 response covering the whole file.
307 int status = 200;
308 size_t body_len = file_size;
309 size_t body_off = 0; // file offset the body starts at (nonzero for a Range)
310 const char *accept_ranges = "";
311 char range_line[64];
312 range_line[0] = '\0';
313
314#if PC_ENABLE_RANGE
315 accept_ranges = "Accept-Ranges: bytes\r\n"; // advertise range support on every file response
316 size_t r_start = 0;
317 size_t r_end = 0;
318 int rr = http_parse_byte_range(http_get_header(&http_pool[slot_id], "Range"), file_size, &r_start, &r_end);
319 if (rr < 0)
320 {
321 // Unsatisfiable range -> 416 with Content-Range: bytes */<size>.
322 f.close();
323 char h416[RESP_HDR_BUF_SIZE];
324 pc_sb sb_h416 = {h416, sizeof(h416), 0, true};
325 pc_sb_put(&sb_h416, "HTTP/1.1 416 Range Not Satisfiable\r\nContent-Range: bytes */");
326 pc_sb_u32(&sb_h416, (uint32_t)((unsigned)file_size));
327 pc_sb_put(&sb_h416, "\r\nContent-Length: 0\r\n");
328 pc_sb_put(&sb_h416, _cors_enabled ? _cors_header_buf : "");
329 pc_sb_put(&sb_h416, cl);
330 pc_sb_put(&sb_h416, "\r\n");
331 int n416 = (int)pc_sb_finish(&sb_h416);
332 pc_conn_send_flush(slot_id, h416, (u16_t)n416);
333 pc_resp_end(slot_id, 416, 0, keep, /*pre_flushed=*/true);
334 return;
335 }
336 if (rr > 0)
337 {
338 status = 206;
339 body_len = r_end - r_start + 1;
340 pc_sb sb_range_line = {range_line, sizeof(range_line), 0, true};
341 pc_sb_put(&sb_range_line, "Content-Range: bytes ");
342 pc_sb_u32(&sb_range_line, (uint32_t)((unsigned)r_start));
343 pc_sb_put(&sb_range_line, "-");
344 pc_sb_u32(&sb_range_line, (uint32_t)((unsigned)r_end));
345 pc_sb_put(&sb_range_line, "/");
346 pc_sb_u32(&sb_range_line, (uint32_t)((unsigned)file_size));
347 pc_sb_put(&sb_range_line, "\r\n");
348 if (pc_sb_finish(&sb_range_line) == 0)
349 {
350 range_line[0] = '\0';
351 }
352 f.seek((uint32_t)r_start);
353 body_off = r_start;
354 }
355#endif
356
357 char header[RESP_HDR_BUF_SIZE];
358 pc_sb sb_header = {header, sizeof(header), 0, true};
359 pc_sb_put(&sb_header, "HTTP/1.1 ");
360 pc_sb_i64(&sb_header, (int64_t)(status));
361 pc_sb_put(&sb_header, " ");
362 pc_sb_put(&sb_header, status_text(status));
363 pc_sb_put(&sb_header, "\r\nContent-Type: ");
364 pc_sb_put(&sb_header, content_type);
365 pc_sb_put(&sb_header, "\r\nContent-Length: ");
366 pc_sb_u32(&sb_header, (uint32_t)((unsigned)body_len));
367 pc_sb_put(&sb_header, "\r\n");
368 pc_sb_put(&sb_header, accept_ranges);
369 pc_sb_put(&sb_header, range_line);
370 pc_sb_put(&sb_header, enc_line);
371 pc_sb_put(&sb_header, etag_line);
372 pc_sb_put(&sb_header, lastmod_line);
373 pc_sb_put(&sb_header, _cache_control_buf);
374 pc_sb_put(&sb_header, _cors_enabled ? _cors_header_buf : "");
375 pc_sb_put(&sb_header, cl);
376 pc_sb_put(&sb_header, "\r\n");
377 int hlen = (int)pc_sb_finish(&sb_header);
378 if (hlen == 0)
379 {
380 header[0] = '\0';
381 }
382
383 pc_conn_send(slot_id, header, (u16_t)hlen);
384
385 // HEAD or empty body: headers only, finish now.
386 if (head || body_len == 0)
387 {
388 f.close();
389 pc_resp_end(slot_id, status, 0, keep);
390 return;
391 }
392
393 // Hand the body to the cross-loop pump: it pages out at most one send-buffer
394 // window now and resumes on later loops as the window drains, so a file larger
395 // than TCP_SND_BUF is never truncated. The pump owns the file and calls
396 // pc_resp_end() at completion - do not close f or end the response here.
397 FileSend &s = s_file.send[slot_id];
398 s.file = f; // shared handle on ARDUINO; the local f going out of scope keeps it open
399 s.off = body_off;
400 s.remaining = body_len;
401 s.status = status;
402 s.total = (int)body_len;
403 s.keep = keep;
404 s.active = true;
405 file_send_pump(slot_id);
406}
407
408// Page out a pending file response across worker loops: send up to pc_conn_sndbuf()
409// bytes now and return; the next loop resumes (woken by the sent callback) until the
410// whole body has been queued, then finish the response. Bounded per loop, never
411// truncates, never blocks the worker.
412void PC::file_send_pump(uint8_t slot_id)
413{
414 FileSend &s = s_file.send[slot_id];
415 // GCOVR_EXCL_START unreachable: both callers already established the state - serve_file_internal
416 // sets s.active immediately before its call, and the poll loop in protocore.cpp only pumps a slot
417 // whose s_file.send[i].active is set. Kept so the pump is safe to call unconditionally.
418 if (!s.active)
419 {
420 return;
421 }
422 // GCOVR_EXCL_STOP
423
424 if (!pc_conn_active(slot_id))
425 {
426 // Connection went away mid-transfer: drop the source and the continuation.
427 s.file.close();
428 s.active = false;
429 return;
430 }
431
432 // A file body still being paged out is active, not idle: keep the CONN_TIMEOUT_MS idle sweep
433 // off it so a transient send stall on a large file cannot reap the slot mid-transfer.
434 pc_conn_touch_active(slot_id);
435
436 uint8_t chunk[FILE_CHUNK_SIZE];
437 while (s.remaining > 0)
438 {
439 u16_t avail = pc_conn_sndbuf(slot_id);
440 if (avail == 0)
441 {
442 pc_conn_flush(slot_id); // push what is queued; resume on a later loop
443 return;
444 }
445 size_t want = s.remaining < sizeof(chunk) ? s.remaining : sizeof(chunk);
446 if (want > avail)
447 {
448 want = avail;
449 }
450 size_t n = s.file.read(chunk, want);
451 if (n == 0)
452 {
453 s.remaining = 0; // read error / short file: stop (response will be short)
454 break;
455 }
456 if (!pc_conn_send(slot_id, chunk, (u16_t)n))
457 {
458 s.file.seek((uint32_t)s.off); // un-read the bytes that did not go out; retry next loop
459 pc_conn_flush(slot_id);
460 return;
461 }
462 s.off += n;
463 s.remaining -= n;
464 }
465
466 // Whole body queued: finish the response (flush, keep-alive/close, log, reset).
467 s.file.close();
468 s.active = false;
469 pc_conn_flush(slot_id);
470 pc_resp_end(slot_id, s.status, s.total, s.keep);
471}
472
473void PC::serve_file(uint8_t slot_id, fs::FS &file_sys, const char *fs_path, const char *content_type)
474{
475 serve_file_internal(slot_id, req_is_head(slot_id), file_sys, fs_path, content_type, nullptr);
476}
477
478void PC::serve_static(const char *url_prefix, fs::FS &file_sys, const char *fs_root)
479{
480 if (_route_count >= MAX_ROUTES)
481 {
482 return;
483 }
484
485 // Store the pattern as a wildcard so path_matches() does a prefix match.
486 //
487 // The pattern is built BEFORE a route slot is taken, because a prefix that does not fit must
488 // not be registered at all. Formatting this with snprintf truncated an over-long prefix to
489 // MAX_PATH_LEN-1 and dropped the '*' with it, quietly turning a subtree mount into an
490 // exact-match route for a path the caller never named - a route that serves something other
491 // than what was asked for is worse than a route that does not exist.
492 char pat[MAX_PATH_LEN];
493 size_t n = strnlen(url_prefix, MAX_PATH_LEN);
494 pc_sb sb_pat = {pat, sizeof(pat), 0, true};
495 pc_sb_put(&sb_pat, url_prefix);
496 if (n == 0 || url_prefix[n - 1] != '*')
497 {
498 pc_sb_put(&sb_pat, "*"); // not already a wildcard: append one
499 }
500 if (pc_sb_finish(&sb_pat) == 0)
501 {
502 return; // prefix + wildcard does not fit: register nothing
503 }
504
505 Route *r = &_routes[_route_count++];
506 fill_route_base(r, pat);
507 r->type = RouteType::ROUTE_STATIC;
509 r->static_fs = &file_sys;
510 r->static_root = fs_root;
511}
512
513void PC::serve_static_request(uint8_t slot_id, HttpReq *req, const Route *r)
514{
515 // GCOVR_EXCL_START a RouteType::ROUTE_STATIC route always carries static_fs: serve_static() takes
516 // the filesystem by reference and stores its address, so this null-guard cannot fire.
517 if (!r->static_fs)
518 {
519 send(slot_id, 404, PC_MIME_TEXT_PLAIN, "Not Found");
520 return;
521 }
522 // GCOVR_EXCL_STOP
523
524 // Request path beyond the mount prefix (route path minus its trailing '*'). plen == 0 is
525 // unreachable: serve_static() always stores at least "*" (it appends the wildcard when the
526 // prefix lacks one), so the pattern is never empty.
527 size_t plen = strnlen(r->path, MAX_PATH_LEN);
528 if (plen > 0 && r->path[plen - 1] == '*') // GCOVR_EXCL_BR_LINE plen == 0 unreachable (see above)
529 {
530 plen--;
531 }
532 const char *sub = (strnlen(req->path, MAX_PATH_LEN) >= plen) ? req->path + plen : "";
533
534 // Reject path traversal before touching the filesystem.
535 if (strstr(sub, ".."))
536 {
537 send(slot_id, 404, PC_MIME_TEXT_PLAIN, "Not Found");
538 return;
539 }
540
541 const char *root = r->static_root ? r->static_root : "";
542 size_t rlen = strnlen(root, MAX_PATH_LEN);
543 bool root_slash = (rlen > 0 && root[rlen - 1] == '/');
544 if (root_slash && sub[0] == '/') // avoid a doubled separator
545 {
546 sub++;
547 }
548 bool sub_slash = (sub[0] == '/');
549 const char *sep = (root_slash || sub_slash) ? "" : "/";
550
551 // Directory or bare-prefix request → index.html.
552 size_t slen = strnlen(sub, MAX_PATH_LEN);
553 bool dir = (slen == 0) || (sub[slen - 1] == '/');
554
555 // A path that does not fit is refused, not truncated: a clipped path names a different file,
556 // and serving one the caller never asked for is worse than a 404.
557 char fs_path[256];
558 pc_sb sb_path = {fs_path, sizeof(fs_path), 0, true};
559 pc_sb_put(&sb_path, root);
560 pc_sb_put(&sb_path, sep);
561 pc_sb_put(&sb_path, sub);
562 if (dir)
563 {
564 pc_sb_lit(&sb_path, "index.html");
565 }
566 if (pc_sb_finish(&sb_path) == 0)
567 {
568 send(slot_id, 404, PC_MIME_TEXT_PLAIN, "Not Found");
569 return;
570 }
571
572 const char *ctype = mime_type(fs_path);
573 bool head = req_is_head(slot_id);
574
575 // Pre-compressed variant: serve <path>.gz if the client accepts gzip and it
576 // exists. Content-Type stays that of the original (uncompressed) resource.
577 const char *ae = http_get_header(req, "Accept-Encoding");
578 if (ae && strstr(ae, "gzip"))
579 {
580 char gz[260];
581 pc_sb sb_gz = {gz, sizeof(gz), 0, true};
582 pc_sb_put(&sb_gz, fs_path);
583 pc_sb_put(&sb_gz, ".gz");
584 int gn = (int)pc_sb_finish(&sb_gz);
585 // Neither length half can fail: snprintf cannot return negative for "%s.gz", and fs_path is
586 // a 256-byte buffer, so gn is at most 258 and always under gz's 260. Both are kept because
587 // the two buffer sizes are independent constants. The exclusion is per-line, so it also
588 // drops the exists() halves - those ARE exercised both ways (see the gzip tests).
589 if (gn > 0 && gn < (int)sizeof(gz) && r->static_fs->exists(gz)) // GCOVR_EXCL_BR_LINE see above
590 {
591 serve_file_internal(slot_id, head, *r->static_fs, gz, ctype, "gzip");
592 return;
593 }
594 }
595
596 serve_file_internal(slot_id, head, *r->static_fs, fs_path, ctype, nullptr);
597}
598#endif // PC_ENABLE_FILE_SERVING
#define MAX_CONNS
Definition c2_defaults.h:47
#define MAX_ROUTES
Definition c2_defaults.h:61
static const char * mime_type(const char *path)
Guess a Content-Type from a path's file extension.
Definition response.cpp:431
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.
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).
Shared single-range Range: bytes=... parser (RFC 7233), used by static file serving and the edge cach...
Shared HTTP Content-Type ("MIME") string constants (one source of truth).
void http_reset(uint8_t slot_id)
Reset the HTTP parser for a connection slot.
void fill_route_base(Route *r, const char *path)
Register a route in the route table.
const char * status_text(int code)
Convert an HTTP status code to its standard reason phrase.
bool req_is_head(uint8_t slot_id)
True if the request in slot slot_id used the HEAD method (send headers, no body).
Layer 7 (Application) - public HTTP routing API.
@ HTTP_GET
Safe, idempotent read.
#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 /).
Library-private declarations shared between protocore.cpp and the src/server/*.cpp request-handler tr...
void http_rfc1123(time_t t, char *out, size_t cap)
Format t as an RFC 1123 GMT date into out (cap bytes); out is emptied for t <= 0.
Bounded no-heap string builder that fails closed on overflow (one shared copy).
size_t pc_sb_finish(pc_sb *b)
NUL-terminate and return the built length, or 0 if the build overflowed.
Definition strbuf.h:648
void pc_sb_hex(pc_sb *b, uint64_t v, unsigned min_digits)
Append v as lowercase hex, zero-padded to at least min_digits (printf "%0Nx").
Definition strbuf.h:297
void pc_sb_lit(pc_sb *b, const char(&s)[N])
Append a string literal. The array parameter deduces N, so the length is a constant.
Definition strbuf.h:76
void pc_sb_i64(pc_sb *b, int64_t v)
Append v as signed decimal (64-bit), with a leading '-' when negative.
Definition strbuf.h:315
void pc_sb_put(pc_sb *b, const char *s)
Append NUL-terminated s; leaves the buffer untouched and clears ok if it would not fit.
Definition strbuf.h:60
void pc_sb_u32(pc_sb *b, uint32_t v)
Append v as decimal (no leading zeros; "0" for zero).
Definition strbuf.h:303
Fully-parsed HTTP/1.1 request.
char path[MAX_PATH_LEN]
URL path, null-terminated; no query string.
Internal route entry stored in the routing table.
Definition protocore.h:244
RouteType type
HTTP, WS, or SSE.
Definition protocore.h:246
HttpMethod method
HTTP method (RouteType::ROUTE_HTTP only).
Definition protocore.h:247
char path[MAX_PATH_LEN]
Null-terminated path pattern.
Definition protocore.h:245
Bump-append target; ok latches false once an append would overflow cap.
Definition strbuf.h:30
void pc_conn_touch_active(uint8_t slot_id)
Refresh slot's idle-timeout timestamp while a response body is in flight.
Definition tcp.cpp:945
void pc_conn_flush(uint8_t slot)
Flush queued bytes / finish the send on slot (TLS-aware).
Definition tcp.cpp:561
u16_t pc_conn_sndbuf(uint8_t slot)
Bytes that can currently be queued for sending on slot.
Definition tcp.cpp:542
bool pc_conn_send(uint8_t slot, const void *data, u16_t len)
Send len bytes on connection slot (copies data; TLS-aware).
Definition tcp.cpp:500
bool pc_conn_send_flush(uint8_t slot, const void *data, u16_t len)
Send len bytes on slot and flush in a single tcpip_thread round-trip.
Definition tcp.cpp:518
Layer 4 (Transport) - TCP connection pool, ring buffers, and lwIP integration.
Reentrant UTC broken-down time, portable across the host and target toolchains.
struct tm * pc_gmtime_r(const time_t *epoch, struct tm *out)
Convert epoch to broken-down UTC in caller storage (reentrant).
Definition time_compat.h:28