31#if PC_ENABLE_FILE_SERVING
62bool pc_file_holds_slot(uint8_t slot)
64 return s_file.send[slot].active;
83 strftime(out, cap,
"%a, %d %b %Y %H:%M:%S GMT", &tmv);
92static bool http_not_modified_since(time_t mtime,
const char *ims)
94 if (mtime <= 0 || !ims)
105 if (sscanf(ims,
"%*3s, %d %3s %d %d:%d:%d", &day, mon, &year, &hh, &mm, &ss) != 6)
109 static const char MONTHS[] =
"JanFebMarAprMayJunJulAugSepOctNovDec";
110 const char *mp = strstr(MONTHS, mon);
113 if (!mp || ((mp - MONTHS) % 3) != 0)
117 int imon = (int)(mp - MONTHS) / 3;
125 int fy = tf.tm_year + 1900;
130 if (tf.tm_mon != imon)
132 return tf.tm_mon < imon;
134 if (tf.tm_mday != day)
136 return tf.tm_mday < day;
138 if (tf.tm_hour != hh)
140 return tf.tm_hour < hh;
144 return tf.tm_min < mm;
146 return tf.tm_sec <= ss;
152static bool inm_matches(
const char *inm,
const char *etag)
157 while (*inm ==
' ' || *inm ==
'\t')
165 size_t etlen = strnlen(etag, 40);
169 while (*p ==
' ' || *p ==
'\t' || *p ==
',')
178 if (tag[0] ==
'W' && tag[1] ==
'/')
184 const char *end = strchr(tag + 1,
'"');
185 if (end && (
size_t)(end - tag + 1) == etlen && strncmp(tag, etag, etlen) == 0)
190 const char *comma = strchr(p,
',');
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)
203 fs::File f = file_sys.open(fs_path,
"r");
206 send(slot_id, 404, PC_MIME_TEXT_PLAIN,
"Not Found");
210 if (!pc_conn_active(slot_id))
217 size_t file_size = f.size();
220 const char *cl = pc_resp_conn_hdr(slot_id, &keep);
225 if (content_encoding)
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);
242 time_t mtime = f.getLastWrite();
244 pc_sb sb_etag = {etag,
sizeof(etag), 0,
true};
246 pc_sb_hex(&sb_etag, (uint64_t)((
unsigned)file_size), 1);
248 pc_sb_hex(&sb_etag, (uint64_t)((
unsigned long)mtime), 1);
256 char lastmod_line[17 +
sizeof(lm_date)];
257 lastmod_line[0] =
'\0';
261 pc_sb sb_lastmod_line = {lastmod_line,
sizeof(lastmod_line), 0,
true};
262 pc_sb_put(&sb_lastmod_line,
"Last-Modified: ");
267 lastmod_line[0] =
'\0';
272 bool not_modified = inm ? inm_matches(inm, etag)
278 pc_sb sb_h304 = {h304,
sizeof(h304), 0,
true};
279 pc_sb_put(&sb_h304,
"HTTP/1.1 304 Not Modified\r\nETag: ");
284 pc_sb_put(&sb_h304, _cors_enabled ? _cors_header_buf :
"");
289 pc_resp_end(slot_id, 304, 0, keep,
true);
293 pc_sb sb_etag_line = {etag_line,
sizeof(etag_line), 0,
true};
302 const char *etag_line =
"";
303 const char *lastmod_line =
"";
308 size_t body_len = file_size;
310 const char *accept_ranges =
"";
312 range_line[0] =
'\0';
315 accept_ranges =
"Accept-Ranges: bytes\r\n";
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 :
"");
333 pc_resp_end(slot_id, 416, 0, keep,
true);
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));
344 pc_sb_u32(&sb_range_line, (uint32_t)((
unsigned)r_end));
346 pc_sb_u32(&sb_range_line, (uint32_t)((
unsigned)file_size));
350 range_line[0] =
'\0';
352 f.seek((uint32_t)r_start);
358 pc_sb sb_header = {header,
sizeof(header), 0,
true};
360 pc_sb_i64(&sb_header, (int64_t)(status));
363 pc_sb_put(&sb_header,
"\r\nContent-Type: ");
365 pc_sb_put(&sb_header,
"\r\nContent-Length: ");
366 pc_sb_u32(&sb_header, (uint32_t)((
unsigned)body_len));
373 pc_sb_put(&sb_header, _cache_control_buf);
374 pc_sb_put(&sb_header, _cors_enabled ? _cors_header_buf :
"");
386 if (head || body_len == 0)
389 pc_resp_end(slot_id, status, 0, keep);
397 FileSend &s = s_file.send[slot_id];
400 s.remaining = body_len;
402 s.total = (int)body_len;
405 file_send_pump(slot_id);
412void PC::file_send_pump(uint8_t slot_id)
414 FileSend &s = s_file.send[slot_id];
424 if (!pc_conn_active(slot_id))
437 while (s.remaining > 0)
445 size_t want = s.remaining <
sizeof(chunk) ? s.remaining : sizeof(chunk);
450 size_t n = s.file.read(chunk, want);
458 s.file.seek((uint32_t)s.off);
470 pc_resp_end(slot_id, s.status, s.total, s.keep);
473void PC::serve_file(uint8_t slot_id, fs::FS &file_sys,
const char *fs_path,
const char *content_type)
475 serve_file_internal(slot_id,
req_is_head(slot_id), file_sys, fs_path, content_type,
nullptr);
478void PC::serve_static(
const char *url_prefix, fs::FS &file_sys,
const char *fs_root)
494 pc_sb sb_pat = {pat,
sizeof(pat), 0,
true};
496 if (n == 0 || url_prefix[n - 1] !=
'*')
505 Route *r = &_routes[_route_count++];
507 r->
type = RouteType::ROUTE_STATIC;
509 r->static_fs = &file_sys;
510 r->static_root = fs_root;
513void PC::serve_static_request(uint8_t slot_id,
HttpReq *req,
const Route *r)
519 send(slot_id, 404, PC_MIME_TEXT_PLAIN,
"Not Found");
528 if (plen > 0 && r->
path[plen - 1] ==
'*')
535 if (strstr(sub,
".."))
537 send(slot_id, 404, PC_MIME_TEXT_PLAIN,
"Not Found");
541 const char *root = r->static_root ? r->static_root :
"";
543 bool root_slash = (rlen > 0 && root[rlen - 1] ==
'/');
544 if (root_slash && sub[0] ==
'/')
548 bool sub_slash = (sub[0] ==
'/');
549 const char *sep = (root_slash || sub_slash) ?
"" :
"/";
553 bool dir = (slen == 0) || (sub[slen - 1] ==
'/');
558 pc_sb sb_path = {fs_path,
sizeof(fs_path), 0,
true};
568 send(slot_id, 404, PC_MIME_TEXT_PLAIN,
"Not Found");
578 if (ae && strstr(ae,
"gzip"))
581 pc_sb sb_gz = {gz,
sizeof(gz), 0,
true};
589 if (gn > 0 && gn < (
int)
sizeof(gz) && r->static_fs->exists(gz))
591 serve_file_internal(slot_id, head, *r->static_fs, gz, ctype,
"gzip");
596 serve_file_internal(slot_id, head, *r->static_fs, fs_path, ctype,
nullptr);
static const char * mime_type(const char *path)
Guess a Content-Type from a path's file extension.
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.
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").
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.
void pc_sb_i64(pc_sb *b, int64_t v)
Append v as signed decimal (64-bit), with a leading '-' when negative.
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.
void pc_sb_u32(pc_sb *b, uint32_t v)
Append v as decimal (no leading zeros; "0" for zero).
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.
RouteType type
HTTP, WS, or SSE.
HttpMethod method
HTTP method (RouteType::ROUTE_HTTP only).
char path[MAX_PATH_LEN]
Null-terminated path pattern.
Bump-append target; ok latches false once an append would overflow cap.
void pc_conn_touch_active(uint8_t slot_id)
Refresh slot's idle-timeout timestamp while a response body is in flight.
void pc_conn_flush(uint8_t slot)
Flush queued bytes / finish the send on slot (TLS-aware).
u16_t pc_conn_sndbuf(uint8_t slot)
Bytes that can currently be queued for sending on slot.
bool pc_conn_send(uint8_t slot, const void *data, u16_t len)
Send len bytes on connection slot (copies data; TLS-aware).
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.
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).