ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
edge_cache_proxy.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 edge_cache_proxy.cpp
6 * @brief CDN edge-cache tier - server glue. See edge_cache_proxy.h.
7 */
8
10#include "shared_primitives/strbuf.h" // pc_sb frame builder
11
12#if PC_ENABLE_EDGE_CACHE
13
14#include "network_drivers/presentation/http/http_parser/http_parser.h" // HttpReq, http_get_header, http_pool
15#include "network_drivers/presentation/presentation.h" // pc_http_set_edge_poll
16#include "network_drivers/transport/client.h" // pc_client_*
17#include "network_drivers/transport/tcp.h" // pc_conn_active
18#include "protocore.h" // PC, Middleware, MwResult, ChunkSource
19#include "services/system/clock.h" // pc_millis
21#if PC_ENABLE_DBM
22#include "services/web/edge_cache/edge_cache_sd.h" // L2 SD tier
23#endif
24#include "server/http_range.h" // http_parse_byte_range (Range/206 support)
25#include "services/net/http_client/http_client.h" // http_client_parse_url
26#include "shared_primitives/mime.h" // PC_MIME_TEXT_PLAIN
27#if PC_ENABLE_EDGE_ORIGIN_TLS
28#include "network_drivers/tls/tls.h" // pc_tls_client_session_* (TLS upstream origin fetch)
29#include <mbedtls/ssl.h> // MBEDTLS_ERR_SSL_WANT_READ / WANT_WRITE
30#endif
31#if PC_ENABLE_EDGE_MESH
32#include "network_drivers/session/proto_handler.h" // ProtoHandler / proto_register (PROTO_MESH serving)
33#include "services/web/edge_cache/edge_mesh.h" // mesh sibling-cache codec + peer-query engine
34#endif
35#include <stdio.h>
36#include <string.h>
37
38namespace
39{
40struct EdgeRouteMap
41{
42 bool used;
43 char prefix[MAX_PATH_LEN];
44 char origin_host[PC_EDGE_ORIGIN_URL_MAX];
45 uint16_t origin_port;
46 bool https; ///< fetch this origin over TLS (PC_ENABLE_EDGE_ORIGIN_TLS)
47};
48
49#if PC_ENABLE_EDGE_MESH
50// A fetch runs the mesh phase (query siblings) first on a full miss, then falls to the origin.
51enum class EdgeFetchPhase : uint8_t
52{
53 MESH,
54 ORIGIN,
55};
56#endif
57
58struct EdgeFetchSlot
59{
60 bool used;
61 EdgeFetch f;
62 uint8_t client_slot;
63 bool revalidate;
64 EdgeEntry *reval_entry; // the stale entry being revalidated (nullptr for a plain miss)
65 const EdgeFetchTransport *transport; // plaintext or TLS transport, chosen per route at start_fetch
66 char canon[PC_EDGE_KEY_MAX];
67 EdgeRouteMap *route; // the origin route (stable in s_ctx.maps) - lets the origin fetch begin later
68 char path[MAX_PATH_LEN]; // request path/query captured at mw time (http_pool[slot] is reused by poll time)
69 char query[MAX_QUERY_LEN];
70#if PC_ENABLE_EDGE_MESH
71 EdgeFetchPhase phase;
72 EdgeMeshFetch mf;
73 uint8_t peer_idx; // sibling currently being queried
74 uint8_t mreq[PC_EDGE_MESH_REQ_MAX]; // the mesh request, built once (reused across peers)
75 size_t mreq_len;
76#endif
77};
78
79struct EdgePending
80{
81 bool active;
82 uint8_t fetch_idx;
83};
84
85// The ChunkSource ctx for a paged send; must outlive the response, so it lives in the owned Ctx.
86struct EdgeServeCursor
87{
88 bool active;
89 EdgeEntry *entry;
90 uint32_t off, end;
91};
92
93#if PC_ENABLE_EDGE_MESH
94// A configured sibling peer to query on a local miss.
95struct MeshPeer
96{
97 bool used;
98 char host[PC_MESH_HOST_MAX];
99 uint16_t port;
100};
101
102// One in-flight inbound peer-serve connection: accumulate the request, answer from the local cache, page out.
103struct MeshConn
104{
105 bool active;
106 uint8_t conn_slot;
107 uint16_t req_len; // request bytes accumulated
108 uint8_t reqbuf[PC_EDGE_MESH_REQ_MAX];
109 bool responded; // the whole response is built (out_len) and paging out
110 uint16_t out_off, out_len;
111 uint8_t outbuf[PC_EDGE_MESH_RESP_MAX];
112};
113#endif
114
115// The single owned file-static: all of this subsystem's mutable state.
116struct EdgeCacheProxyCtx
117{
118 PC *server;
119 bool registered;
120 EdgeCacheStore store;
121 EdgeRouteMap maps[PC_EDGE_MAP_MAX];
122 EdgeFetchSlot fetches[PC_EDGE_FETCH_SLOTS];
123 EdgePending pending[MAX_CONNS];
124 EdgeServeCursor serve[MAX_CONNS];
125 EdgeFetchTransport transport;
126#if PC_ENABLE_EDGE_ORIGIN_TLS
127 EdgeFetchTransport transport_tls; // TLS binding over pc_tls_csess, used for https routes
128 int tls_cid; // underlying pc_client cid of the in-flight TLS fetch (singleton session)
129 bool tls_peer_closed; // latched when the TLS session reports closed / errored
130#endif
131 char reqbuf[MAX_PATH_LEN + MAX_QUERY_LEN + 256]; // scratch for one origin request line (freed by send)
132#if PC_ENABLE_RANGE
133 // The client's Range header, captured per slot at middleware time. serve_hit runs for a miss/stale
134 // entry from the poll loop *after* the async fetch has reused http_pool[slot], so the original request
135 // is no longer readable there - the window must be resolved against this captured copy.
136 char range_hdr[MAX_CONNS][48];
137#endif
138#if PC_ENABLE_DBM
139 pc_dbm *l2; // the persistent L2 tier (nullptr = L1-only)
140 uint8_t sd_buf[PC_EDGE_SD_VALUE_MAX]; // serialize/deserialize scratch for one L2 value
141#endif
142#if PC_ENABLE_EDGE_MESH
143 MeshPeer peers[PC_MESH_MAX_PEERS]; // static sibling list queried on a full miss
144 MeshConn mesh_conns[PC_MESH_MAX_CONNS];
145 char mesh_hdrs[PC_MESH_HDRS_MAX]; // scratch: a served request's header snapshot (serve is single-threaded)
146 bool mesh_registered; // the PROTO_MESH serving handler is installed
147#endif
148};
149EdgeCacheProxyCtx s_ctx;
150
151#if PC_ENABLE_DBM
152// L1 write-back hook: spill an evicted victim to L2 (edge_sd_put skips no-validator / oversize entries).
153void edge_on_evict(void *ctx, const EdgeEntry *victim)
154{
155 (void)ctx;
156 if (s_ctx.l2 && edge_sd_put(s_ctx.l2, victim, s_ctx.sd_buf, sizeof(s_ctx.sd_buf)))
157 {
158 s_ctx.store.stats.l2_spills++;
159 }
160}
161#endif
162
163// --- pc_client transport seam -------------------------------------------------------------------
164int t_open(void *c, const char *host, uint16_t port, uint32_t timeout)
165{
166 (void)c;
167 return pc_client_open(host, port, timeout);
168}
169bool t_send(void *c, int cid, const void *d, size_t l)
170{
171 (void)c;
172 return pc_client_send(cid, d, l);
173}
174size_t t_read(void *c, int cid, uint8_t *b, size_t cap)
175{
176 (void)c;
177 return pc_client_read(cid, b, cap);
178}
179bool t_closed(void *c, int cid)
180{
181 (void)c;
182 return pc_client_is_closed(cid);
183}
184void t_close(void *c, int cid)
185{
186 (void)c;
187 pc_client_close(cid);
188}
189
190#if PC_ENABLE_EDGE_ORIGIN_TLS
191// --- TLS transport seam (pc_tls_csess layered over pc_client) -----------------------------------
192// The client TLS session is a singleton, so the underlying cid + peer-closed latch live in the owned Ctx
193// (one TLS fetch at a time, enforced by pc_tls_client_session_active() in start_fetch). The BIO callbacks move
194// ciphertext over pc_client's wire ring for that cid - the same bridge the MQTT/WS clients use.
195int edge_tls_bio_send(void *ctx, const unsigned char *buf, size_t len)
196{
197 (void)ctx;
198 size_t cap = len > 0xFFFF ? 0xFFFF : len;
199 return pc_client_send(s_ctx.tls_cid, buf, cap) ? (int)cap : MBEDTLS_ERR_SSL_WANT_WRITE;
200}
201int edge_tls_bio_recv(void *ctx, unsigned char *buf, size_t len)
202{
203 (void)ctx;
204 size_t n = pc_client_read(s_ctx.tls_cid, buf, len);
205 if (n == 0)
206 {
207 return pc_client_is_closed(s_ctx.tls_cid) ? 0 : MBEDTLS_ERR_SSL_WANT_READ;
208 }
209 return (int)n;
210}
211
212int t_tls_open(void *c, const char *host, uint16_t port, uint32_t timeout)
213{
214 (void)c;
215 s_ctx.tls_cid = pc_client_open(host, port, timeout);
216 if (s_ctx.tls_cid < 0)
217 {
218 return -1;
219 }
220 s_ctx.tls_peer_closed = false;
221 if (!pc_tls_client_session_begin(host, edge_tls_bio_send, edge_tls_bio_recv))
222 {
223 pc_client_close(s_ctx.tls_cid);
224 s_ctx.tls_cid = -1;
225 return -1;
226 }
227 // Block through the handshake at connect - the same brief block the MQTT/WS clients take (and that the
228 // plaintext pc_client_open already takes on DNS+connect). edge_fetch_begin sends the request
229 // synchronously right after open returns, so the session must be established first. pcdelay() yields to
230 // the network stack between handshake flights so the peer's records arrive.
231 uint32_t deadline = pc_millis() + timeout;
232 int h = 0;
233 while ((h = pc_tls_client_session_handshake()) == 0 && !pc_client_is_closed(s_ctx.tls_cid) &&
234 (int32_t)(deadline - pc_millis()) > 0)
235 {
236 pcdelay(5);
237 }
238 if (h != 1)
239 {
240 pc_tls_client_session_end();
241 pc_client_close(s_ctx.tls_cid);
242 s_ctx.tls_cid = -1;
243 return -1;
244 }
245 return s_ctx.tls_cid;
246}
247bool t_tls_send(void *c, int cid, const void *d, size_t l)
248{
249 (void)c;
250 (void)cid;
251 return pc_tls_client_session_write((const uint8_t *)d, l) == (int)l;
252}
253size_t t_tls_read(void *c, int cid, uint8_t *b, size_t cap)
254{
255 (void)c;
256 (void)cid;
257 int n = pc_tls_client_session_read(b, cap);
258 if (n < 0)
259 {
260 s_ctx.tls_peer_closed = true; // close_notify / decrypt error -> report closed via t_tls_closed
261 }
262 return n > 0 ? (size_t)n : 0;
263}
264bool t_tls_closed(void *c, int cid)
265{
266 (void)c;
267 return s_ctx.tls_peer_closed || pc_client_is_closed(cid);
268}
269void t_tls_close(void *c, int cid)
270{
271 (void)c;
272 pc_tls_client_session_end();
273 pc_client_close(cid);
274 s_ctx.tls_cid = -1;
275}
276#endif // PC_ENABLE_EDGE_ORIGIN_TLS
277
278// Request-header lookup used to (re)serialize the Vary secondary key; ctx is the client HttpReq.
279const char *req_lookup(void *ctx, const char *name)
280{
281 return http_get_header((const HttpReq *)ctx, name);
282}
283
284EdgeRouteMap *map_match(const char *path)
285{
286 for (int i = 0; i < PC_EDGE_MAP_MAX; i++)
287 {
288 if (!s_ctx.maps[i].used)
289 {
290 continue;
291 }
292 size_t pl = strnlen(s_ctx.maps[i].prefix, sizeof(s_ctx.maps[i].prefix));
293 if (strncmp(path, s_ctx.maps[i].prefix, pl) == 0)
294 {
295 return &s_ctx.maps[i];
296 }
297 }
298 return nullptr;
299}
300
301int alloc_fetch()
302{
303 for (int i = 0; i < PC_EDGE_FETCH_SLOTS; i++)
304 {
305 if (!s_ctx.fetches[i].used)
306 {
307 return i;
308 }
309 }
310 return -1;
311}
312
313// The ChunkSource: page the cached body to the client. On completion (or exhaustion) release a
314// transient passthrough entry (key ""). A dropped connection leaves the transient LRU-reclaimable.
315size_t edge_chunk_source(uint8_t *buf, size_t cap, void *ctx)
316{
317 EdgeServeCursor *c = (EdgeServeCursor *)ctx;
318 if (!c->active || !c->entry)
319 {
320 return 0;
321 }
322 size_t remaining = c->end - c->off;
323 if (remaining == 0)
324 {
325 c->active = false;
326 if (c->entry->key[0] == '\0') // transient passthrough entry -> free its slot
327 {
328 edge_store_free_entry(&s_ctx.store, c->entry);
329 }
330 c->entry = nullptr;
331 return 0;
332 }
333 size_t n = remaining < cap ? remaining : cap;
334 memcpy(buf, c->entry->body + c->off, n);
335 c->off += n;
336 return n;
337}
338
339// Serve a cache entry, replaying its validators + Age, tagged with @p xcache. A client `Range` request
340// (PC_ENABLE_RANGE) is answered with a 206 window (or 416 if unsatisfiable); otherwise a full 200.
341void serve_hit(uint8_t slot, EdgeEntry *e, uint32_t now, const char *xcache)
342{
343 EdgeServeCursor *c = &s_ctx.serve[slot];
344 c->active = true;
345 c->entry = e;
346 c->off = 0;
347 c->end = e->body_len;
348 int status = 200;
349
350#if PC_ENABLE_RANGE
351 const char *range = s_ctx.range_hdr[slot]; // captured at mw time (http_pool[slot] is stale post-fetch)
352 if (range[0])
353 {
354 size_t rs = 0;
355 size_t re = 0;
356 int rr = http_parse_byte_range(range, e->body_len, &rs, &re);
357 if (rr < 0) // syntactically valid but unsatisfiable -> 416, no body window served
358 {
359 char cr[48];
360 pc_sb sb_cr = {cr, sizeof(cr), 0, true};
361 pc_sb_put(&sb_cr, "bytes */");
362 pc_sb_u32(&sb_cr, (uint32_t)((unsigned)e->body_len));
363 if (pc_sb_finish(&sb_cr) == 0)
364 {
365 cr[0] = '\0';
366 }
367 s_ctx.server->add_response_header(slot, "Content-Range", cr);
368 c->active = false;
369 c->entry = nullptr;
370 s_ctx.server->send(slot, 416, PC_MIME_TEXT_PLAIN, "Range Not Satisfiable");
371 return;
372 }
373 if (rr > 0) // satisfiable -> 206 with just the requested window [rs, re]
374 {
375 status = 206;
376 c->off = (uint32_t)rs;
377 c->end = (uint32_t)re + 1;
378 char cr[48];
379 pc_sb sb_cr2 = {cr, sizeof(cr), 0, true};
380 pc_sb_put(&sb_cr2, "bytes ");
381 pc_sb_u32(&sb_cr2, (uint32_t)((unsigned)rs));
382 pc_sb_put(&sb_cr2, "-");
383 pc_sb_u32(&sb_cr2, (uint32_t)((unsigned)re));
384 pc_sb_put(&sb_cr2, "/");
385 pc_sb_u32(&sb_cr2, (uint32_t)((unsigned)e->body_len));
386 if (pc_sb_finish(&sb_cr2) == 0)
387 {
388 cr[0] = '\0';
389 }
390 s_ctx.server->add_response_header(slot, "Content-Range", cr);
391 }
392 }
393 s_ctx.server->add_response_header(slot, "Accept-Ranges", "bytes"); // advertise range support
394#endif
395
396 s_ctx.server->add_response_header(slot, "X-Cache", xcache);
397 if (e->etag[0])
398 {
399 s_ctx.server->add_response_header(slot, "ETag", e->etag);
400 }
401 if (e->last_modified[0])
402 {
403 s_ctx.server->add_response_header(slot, "Last-Modified", e->last_modified);
404 }
405 if (e->content_encoding[0])
406 {
407 s_ctx.server->add_response_header(slot, "Content-Encoding", e->content_encoding);
408 }
409 long age = edge_current_age(e->initial_age, e->insert_ms, now);
410 if (age < 0)
411 {
412 age = 0;
413 }
414 char agebuf[12];
415 pc_sb sb_agebuf = {agebuf, sizeof(agebuf), 0, true};
416 pc_sb_i64(&sb_agebuf, (int64_t)(age));
417 if (pc_sb_finish(&sb_agebuf) == 0)
418 {
419 agebuf[0] = '\0';
420 }
421 s_ctx.server->add_response_header(slot, "Age", agebuf);
422 const char *ct = e->content_type[0] ? e->content_type : "application/octet-stream";
423 s_ctx.server->send_chunked(slot, status, ct, edge_chunk_source, c);
424}
425
426// Serve a non-cacheable / non-200 origin response through a transient unindexed store slot, so the
427// serve source outlives the fetch (which the caller frees) and no-store content is never re-served.
428void serve_passthrough(uint8_t slot, EdgeFetch *f)
429{
430 EdgeEntry *e = edge_store_alloc(&s_ctx.store, "", ""); // key "" -> never matched by a lookup
431 if (!e)
432 {
433 s_ctx.server->send(slot, 502, PC_MIME_TEXT_PLAIN, "Bad Gateway");
434 return;
435 }
436 s_ctx.store.stats.stores--; // a transient is not a cache store
437 e->status = f->status;
438 if (!edge_header_value((const char *)f->buf, f->head_len, "Content-Type", e->content_type, sizeof(e->content_type)))
439 {
440 strncpy(e->content_type, "application/octet-stream", sizeof(e->content_type) - 1);
441 }
442 edge_header_value((const char *)f->buf, f->head_len, "Content-Encoding", e->content_encoding,
443 sizeof(e->content_encoding));
444 size_t bl = f->body_len;
445 if (bl > PC_EDGE_BODY_MAX)
446 {
447 bl = PC_EDGE_BODY_MAX;
448 }
449 memcpy(e->body, f->buf + f->body_off, bl);
450 e->body_len = (uint16_t)bl;
451
452 EdgeServeCursor *c = &s_ctx.serve[slot];
453 c->active = true;
454 c->entry = e;
455 c->off = 0;
456 c->end = e->body_len;
457 s_ctx.server->add_response_header(slot, "X-Cache", "MISS");
458 if (e->content_encoding[0])
459 {
460 s_ctx.server->add_response_header(slot, "Content-Encoding", e->content_encoding);
461 }
462 const char *ct = e->content_type[0] ? e->content_type : "application/octet-stream";
463 s_ctx.server->send_chunked(slot, e->status ? e->status : 200, ct, edge_chunk_source, c);
464}
465
466// Store a cacheable 200 response into a fresh entry and serve it.
467void store_response(uint8_t slot, EdgeFetchSlot *fs, HttpReq *req, const pc_cache_control *cc, const char *vary_hdr,
468 uint32_t now)
469{
470 EdgeFetch *f = &fs->f;
471 const char *head = (const char *)f->buf;
472 size_t head_len = f->head_len;
473
474 char vary_vals[PC_EDGE_VARY_MAX];
475 edge_vary_serialize(vary_hdr[0] ? vary_hdr : nullptr, req_lookup, req, vary_vals, sizeof(vary_vals));
476
477 EdgeEntry *e = edge_store_alloc(&s_ctx.store, fs->canon, vary_vals);
478 if (!e)
479 {
480 serve_passthrough(slot, f);
481 return;
482 }
483 e->status = 200;
484 edge_header_value(head, head_len, "Content-Type", e->content_type, sizeof(e->content_type));
485 edge_header_value(head, head_len, "Content-Encoding", e->content_encoding, sizeof(e->content_encoding));
486 edge_header_value(head, head_len, "ETag", e->etag, sizeof(e->etag));
487 edge_header_value(head, head_len, "Last-Modified", e->last_modified, sizeof(e->last_modified));
488 size_t vhl = strnlen(vary_hdr, sizeof(e->vary_names));
489 if (vary_hdr[0] && vhl < sizeof(e->vary_names))
490 {
491 memcpy(e->vary_names, vary_hdr, vhl + 1);
492 }
493
494 size_t bl = f->body_len;
495 if (bl > PC_EDGE_BODY_MAX)
496 {
497 bl = PC_EDGE_BODY_MAX;
498 }
499 memcpy(e->body, f->buf + f->body_off, bl);
500 e->body_len = (uint16_t)bl;
501 s_ctx.store.stats.bytes_stored += bl;
502
503 int64_t date = -1;
504 int64_t expires = -1;
505 int64_t last_mod = -1;
506 int32_t age = 0;
507 char v[64];
508 if (edge_header_value(head, head_len, "Date", v, sizeof(v)))
509 {
510 date = edge_parse_http_date(v, strnlen(v, sizeof(v)));
511 }
512 if (edge_header_value(head, head_len, "Expires", v, sizeof(v)))
513 {
514 expires = edge_parse_http_date(v, strnlen(v, sizeof(v)));
515 }
516 if (e->last_modified[0])
517 {
518 last_mod = edge_parse_http_date(e->last_modified, strnlen(e->last_modified, sizeof(e->last_modified)));
519 }
520 if (edge_header_value(head, head_len, "Age", v, sizeof(v)))
521 {
522 long a = 0;
523 for (const char *p = v; *p >= '0' && *p <= '9'; p++)
524 {
525 a = a * 10 + (*p - '0');
526 }
527 age = (int32_t)a;
528 }
529 edge_entry_set_freshness(e, cc, /*shared=*/true, date, expires, last_mod, age, /*response_time=*/-1, now);
530 serve_hit(slot, e, now, "MISS");
531}
532
533// A completed origin fetch: revalidation 304 / store 200 / pass through anything else.
534void on_fetch_done(uint8_t slot, EdgeFetchSlot *fs, uint32_t now)
535{
536 EdgeFetch *f = &fs->f;
537 const char *head = (const char *)f->buf;
538 size_t head_len = f->head_len;
539 HttpReq *req = &http_pool[slot];
540
541 if (fs->revalidate && f->status == 304 && fs->reval_entry)
542 {
543 edge_apply_304(fs->reval_entry, head, head_len, -1, now);
544 s_ctx.store.stats.revalidations_304++;
545 serve_hit(slot, fs->reval_entry, now, "REVALIDATED");
546 return;
547 }
548 if (f->status == 200)
549 {
550 pc_cache_control cc;
551 cache_control_init(&cc);
552 char v[128];
553 if (edge_header_value(head, head_len, "Cache-Control", v, sizeof(v)))
554 {
555 cache_control_parse(v, strnlen(v, sizeof(v)), &cc);
556 }
557 char vary_hdr[PC_EDGE_VARY_MAX];
558 vary_hdr[0] = '\0';
559 edge_header_value(head, head_len, "Vary", vary_hdr, sizeof(vary_hdr));
560 if (edge_is_storeable(200, "GET", &cc, vary_hdr[0] ? vary_hdr : nullptr, f->body_len))
561 {
562 if (fs->revalidate && fs->reval_entry) // 200 on a revalidation replaces the stale entry
563 {
564 edge_store_free_entry(&s_ctx.store, fs->reval_entry);
565 s_ctx.store.stats.replaces_200++;
566 }
567 store_response(slot, fs, req, &cc, vary_hdr, now);
568 return;
569 }
570 serve_passthrough(slot, f); // 200 but not storeable
571 return;
572 }
573 serve_passthrough(slot, f); // non-200 status
574}
575
576// Forward decls for the seam functions installed by pc_edge_cache_enable().
577MwResult edge_cache_mw(uint8_t slot, HttpReq *req);
578bool edge_cache_poll(uint8_t slot);
579
580// Build + begin the origin fetch for @p fs from its captured route/path/query (so it can begin either
581// immediately at mw time or later, after the mesh phase exhausts its peers). Picks the plaintext or TLS
582// transport; a revalidation adds the conditional headers. @return false if no fetch could start (fail open).
583bool begin_origin_fetch(EdgeFetchSlot *fs, uint32_t now)
584{
585 EdgeRouteMap *m = fs->route;
586 const EdgeFetchTransport *tport = &s_ctx.transport;
587#if PC_ENABLE_EDGE_ORIGIN_TLS
588 if (m->https)
589 {
590 if (pc_tls_client_session_active())
591 {
592 return false; // the shared client-TLS session is busy -> fail open (never tear down a live one)
593 }
594 tport = &s_ctx.transport_tls;
595 }
596#endif
597 char cond[192];
598 cond[0] = '\0';
599 if (fs->reval_entry)
600 {
601 edge_build_conditional(fs->reval_entry, cond, sizeof(cond));
602 }
603 pc_sb sb_reqbuf = {s_ctx.reqbuf, sizeof(s_ctx.reqbuf), 0, true};
604 pc_sb_put(&sb_reqbuf, "GET ");
605 pc_sb_put(&sb_reqbuf, fs->path);
606 pc_sb_put(&sb_reqbuf, fs->query[0] ? "?" : "");
607 pc_sb_put(&sb_reqbuf, fs->query);
608 pc_sb_put(&sb_reqbuf, " HTTP/1.1\r\nHost: ");
609 pc_sb_put(&sb_reqbuf, m->origin_host);
610 pc_sb_put(&sb_reqbuf, "\r\nUser-Agent: PC-EdgeCache\r\nConnection: close\r\n");
611 pc_sb_put(&sb_reqbuf, cond);
612 pc_sb_put(&sb_reqbuf, "\r\n");
613 int rl = (int)pc_sb_finish(&sb_reqbuf);
614 if (rl <= 0 || (size_t)rl >= sizeof(s_ctx.reqbuf))
615 {
616 return false;
617 }
618 edge_fetch_begin(&fs->f, tport, m->origin_host, m->origin_port, s_ctx.reqbuf, (size_t)rl, now);
619 if (fs->f.st == EdgeFetchStatus::FAILED)
620 {
621 edge_fetch_end(&fs->f, tport);
622 return false;
623 }
624 fs->transport = tport;
625 return true;
626}
627
628#if PC_ENABLE_EDGE_MESH
629int mesh_peer_count()
630{
631 int n = 0;
632 for (int i = 0; i < PC_MESH_MAX_PEERS; i++)
633 {
634 if (s_ctx.peers[i].used)
635 {
636 n++;
637 }
638 }
639 return n;
640}
641
642// The @p n-th used peer in slot order, or nullptr.
643MeshPeer *mesh_peer_nth(int n)
644{
645 for (int i = 0; i < PC_MESH_MAX_PEERS; i++)
646 {
647 if (s_ctx.peers[i].used && n-- == 0)
648 {
649 return &s_ctx.peers[i];
650 }
651 }
652 return nullptr;
653}
654
655// Snapshot the request headers as `name RS value US ...` so a peer can re-run the Vary matcher. Headers past
656// the cap are dropped (at worst a safe mesh miss, never wrong content).
657void mesh_snapshot_headers(const HttpReq *req, char *out, size_t cap)
658{
659 size_t pos = 0;
660 out[0] = '\0';
661 for (uint8_t i = 0; i < req->header_count; i++)
662 {
663 const char *k = req->headers[i].key;
664 const char *v = req->headers[i].val;
665 size_t kl = strnlen(k, MAX_KEY_LEN);
666 size_t vl = strnlen(v, MAX_VAL_LEN);
667 if (pos + kl + 1 + vl + 1 >= cap)
668 {
669 break;
670 }
671 memcpy(out + pos, k, kl);
672 pos += kl;
673 out[pos++] = '\x1e';
674 memcpy(out + pos, v, vl);
675 pos += vl;
676 out[pos++] = '\x1f';
677 }
678 out[pos] = '\0';
679}
680
681// The peer query reuses the slot's origin response buffer (the mesh and origin phases never run together).
682#if PC_EDGE_FETCH_BUF < PC_EDGE_MESH_RESP_MAX
683#error "PC_EDGE_FETCH_BUF must hold a mesh response (>= PC_EDGE_MESH_RESP_MAX). It defaults to that "\
684 "floor, so this only fires if you pinned it lower; raise PC_EDGE_FETCH_BUF or lower PC_EDGE_BODY_MAX."
685#endif
686
687// Begin the mesh query against the peer at fs->peer_idx. @return false if there is no such peer.
688bool mesh_begin_peer(EdgeFetchSlot *fs, uint32_t now)
689{
690 MeshPeer *p = mesh_peer_nth(fs->peer_idx);
691 if (!p)
692 {
693 return false;
694 }
695 edge_mesh_fetch_begin(&fs->mf, &s_ctx.transport, p->host, p->port, fs->mreq, fs->mreq_len, fs->f.buf,
696 sizeof(fs->f.buf), now);
697 return true;
698}
699
700// A peer HIT: rehydrate the entry into a fresh L1 slot, verify it matches the request, and serve it as fresh
701// (age propagated). @return true if it was served; false (freeing the slot) if corrupt / wrong / already stale.
702bool mesh_store_and_serve(uint8_t slot, EdgeFetchSlot *fs, uint32_t now)
703{
704 EdgeEntry *e = edge_store_alloc(&s_ctx.store, fs->canon, "");
705 if (!e)
706 {
707 return false;
708 }
709 if (!edge_mesh_deserialize_entry(fs->mf.buf + fs->mf.entry_off, fs->mf.entry_len, e, now) ||
710 strcmp(e->key, fs->canon) != 0 || !edge_entry_fresh(e, now))
711 {
712 edge_store_free_entry(&s_ctx.store, e);
713 return false;
714 }
715 s_ctx.store.stats.bytes_stored += e->body_len;
716 serve_hit(slot, e, now, "MESH");
717 return true;
718}
719
720// The current peer query ended without a served hit: try the next sibling, else begin the origin fetch.
721// @return true if the slot still owns work (mesh continues or origin began); false = give up.
722bool mesh_advance_or_origin(EdgeFetchSlot *fs, uint32_t now)
723{
724 fs->peer_idx++;
725 if (mesh_begin_peer(fs, now))
726 {
727 return true; // querying the next sibling (still MESH phase)
728 }
729 s_ctx.store.stats.mesh_misses++;
730 if (begin_origin_fetch(fs, now))
731 {
732 fs->phase = EdgeFetchPhase::ORIGIN;
733 return true;
734 }
735 return false;
736}
737#endif // PC_ENABLE_EDGE_MESH
738
739bool start_fetch(uint8_t slot, HttpReq *req, EdgeRouteMap *m, const char *canon, EdgeEntry *reval, uint32_t now)
740{
741 int fi = alloc_fetch();
742 if (fi < 0)
743 {
744 return false;
745 }
746 EdgeFetchSlot *fs = &s_ctx.fetches[fi];
747 fs->client_slot = slot;
748 fs->revalidate = (reval != nullptr);
749 fs->reval_entry = reval;
750 fs->route = m;
751 memcpy(fs->canon, canon, strnlen(canon, sizeof(fs->canon) - 1) + 1);
752 strncpy(fs->path, req->path, sizeof(fs->path) - 1);
753 fs->path[sizeof(fs->path) - 1] = '\0';
754 strncpy(fs->query, req->query, sizeof(fs->query) - 1);
755 fs->query[sizeof(fs->query) - 1] = '\0';
756
757#if PC_ENABLE_EDGE_MESH
758 // On a full miss (not a revalidation) with >= 1 sibling, query the mesh before the origin.
759 if (!reval && mesh_peer_count() > 0)
760 {
761 uint8_t digest[32];
762 edge_key_digest(canon, strnlen(canon, PC_EDGE_KEY_MAX), digest);
763 mesh_snapshot_headers(req, s_ctx.mesh_hdrs, sizeof(s_ctx.mesh_hdrs));
764 fs->mreq_len = edge_mesh_build_request(digest, canon, s_ctx.mesh_hdrs, fs->mreq, sizeof(fs->mreq));
765 fs->peer_idx = 0;
766 if (fs->mreq_len > 0 && mesh_begin_peer(fs, now))
767 {
768 fs->phase = EdgeFetchPhase::MESH;
769 fs->used = true;
770 s_ctx.pending[slot].active = true;
771 s_ctx.pending[slot].fetch_idx = (uint8_t)fi;
772 return true;
773 }
774 }
775 fs->phase = EdgeFetchPhase::ORIGIN;
776#endif
777 if (!begin_origin_fetch(fs, now))
778 {
779 return false; // fs->used stays false -> the slot is reclaimed
780 }
781 fs->used = true;
782 s_ctx.pending[slot].active = true;
783 s_ctx.pending[slot].fetch_idx = (uint8_t)fi;
784 return true;
785}
786
787#if PC_ENABLE_DBM
788// Promote a reboot-surviving entry from L2 into a fresh L1 slot, forced stale so the caller revalidates it
789// (the monotonic insert time is meaningless across a reboot). @return the promoted entry, or nullptr.
790EdgeEntry *try_promote_l2(const char *canon, uint32_t now)
791{
792 uint8_t digest[32];
793 edge_key_digest(canon, strnlen(canon, PC_EDGE_KEY_MAX), digest);
794 EdgeEntry *e = edge_store_alloc(&s_ctx.store, canon, ""); // may evict + write-back an L1 victim first
795 if (!e)
796 {
797 return nullptr;
798 }
799 if (!edge_sd_get(s_ctx.l2, digest, e, s_ctx.sd_buf, sizeof(s_ctx.sd_buf)) || strcmp(e->key, canon) != 0)
800 {
801 edge_store_free_entry(&s_ctx.store, e); // L2 miss or digest collision -> not promoted
802 return nullptr;
803 }
804 e->lifetime_s = 0; // force stale: freshness is untrustworthy across a reboot -> caller revalidates
805 e->initial_age = 0;
806 e->date_epoch = e->expires_epoch = -1;
807 e->age_hdr = 0;
808 e->insert_ms = now;
809 e->last_used_ms = now;
810 s_ctx.store.stats.l2_promotes++;
811 return e;
812}
813#endif
814
815// The cache middleware: fresh hit -> serve; stale/miss -> start an async origin fetch (suspend); else
816// fall through (fail open).
817MwResult edge_cache_mw(uint8_t slot, HttpReq *req)
818{
819 if (!s_ctx.registered || !s_ctx.server || slot >= MAX_CONNS)
820 {
821 return MwResult::MW_NEXT;
822 }
823 bool is_get = strcmp(req->method, "GET") == 0;
824 bool is_head = strcmp(req->method, "HEAD") == 0;
825 if (!is_get && !is_head)
826 {
827 return MwResult::MW_NEXT; // only cache safe methods
828 }
829 if (http_get_header(req, "Authorization"))
830 {
831 return MwResult::MW_NEXT; // never cache authorized/private requests
832 }
833 EdgeRouteMap *m = map_match(req->path);
834 if (!m)
835 {
836 return MwResult::MW_NEXT; // not a mapped origin
837 }
838
839 const char *host = http_get_header(req, "Host");
840 if (!host)
841 {
842 host = "";
843 }
844 char canon[PC_EDGE_KEY_MAX];
845 if (edge_key_canon("GET", host, req->path, req->query, /*include_query=*/true, canon, sizeof(canon)) == 0)
846 {
847 return MwResult::MW_NEXT; // key too long -> uncacheable, fail open
848 }
849
850#if PC_ENABLE_RANGE
851 // Capture the Range header now, while http_pool[slot] is the client request: a miss serves from the
852 // poll after the async fetch has reused that buffer, so serve_hit resolves the window against this copy.
853 const char *rh = http_get_header(req, "Range");
854 strncpy(s_ctx.range_hdr[slot], rh ? rh : "", sizeof(s_ctx.range_hdr[slot]) - 1);
855 s_ctx.range_hdr[slot][sizeof(s_ctx.range_hdr[slot]) - 1] = '\0';
856#endif
857
858 uint32_t now = pc_millis();
859 EdgeEntry *e = edge_store_find(&s_ctx.store, canon, req_lookup, req, now);
860 if (e && edge_entry_fresh(e, now))
861 {
862 s_ctx.store.stats.hits++;
863 serve_hit(slot, e, now, "HIT");
864 return MwResult::MW_HALT;
865 }
866#if PC_ENABLE_DBM
867 if (!e && s_ctx.l2) // L1 miss: try promoting a reboot-surviving entry from L2 (force-stale -> revalidate)
868 {
869 e = try_promote_l2(canon, now);
870 }
871#endif
872 s_ctx.store.stats.misses++;
873 EdgeEntry *reval = (e && edge_entry_has_validator(e)) ? e : nullptr;
874 if (!start_fetch(slot, req, m, canon, reval, now))
875 {
876 return MwResult::MW_NEXT; // no fetch slot / origin open failed -> fail open to normal dispatch
877 }
878 return MwResult::MW_HALT; // client request suspended until the fetch completes
879}
880
881// Per-slot poll hook: drive an in-flight sibling query then origin fetch, then serve. Returns true while it
882// owns the slot.
883bool edge_cache_poll(uint8_t slot)
884{
885 if (slot >= MAX_CONNS || !s_ctx.pending[slot].active)
886 {
887 return false;
888 }
889 uint8_t fi = s_ctx.pending[slot].fetch_idx;
890 EdgeFetchSlot *fs = &s_ctx.fetches[fi];
891 uint32_t now = pc_millis();
892
893#if PC_ENABLE_EDGE_MESH
894 if (fs->phase == EdgeFetchPhase::MESH)
895 {
896 if (!pc_conn_active(slot)) // client vanished mid-query: abort
897 {
898 edge_mesh_fetch_end(&fs->mf, &s_ctx.transport);
899 fs->used = false;
900 s_ctx.pending[slot].active = false;
901 return true;
902 }
903 EdgeMeshStatus ms = edge_mesh_fetch_pump(&fs->mf, &s_ctx.transport, now);
904 if (ms == EdgeMeshStatus::PENDING)
905 {
906 return true; // still querying this sibling
907 }
908 bool served = (ms == EdgeMeshStatus::HIT) && mesh_store_and_serve(slot, fs, now);
909 edge_mesh_fetch_end(&fs->mf, &s_ctx.transport);
910 if (served)
911 {
912 s_ctx.store.stats.mesh_hits++;
913 fs->used = false;
914 s_ctx.pending[slot].active = false;
915 return true;
916 }
917 if (mesh_advance_or_origin(fs, now)) // try the next sibling, else begin the origin fetch
918 {
919 return true;
920 }
921 s_ctx.server->send(slot, 502, PC_MIME_TEXT_PLAIN, "Bad Gateway"); // no sibling + origin start failed
922 fs->used = false;
923 s_ctx.pending[slot].active = false;
924 return true;
925 }
926#endif
927
928 const EdgeFetchTransport *tport = fs->transport; // the transport chosen for this fetch (plaintext or TLS)
929 if (!pc_conn_active(slot)) // client vanished mid-fetch: abort
930 {
931 edge_fetch_end(&fs->f, tport);
932 fs->used = false;
933 s_ctx.pending[slot].active = false;
934 return true;
935 }
936
937 EdgeFetchStatus st = edge_fetch_pump(&fs->f, tport, now);
938 if (st == EdgeFetchStatus::PENDING)
939 {
940 return true; // still receiving; owns the slot
941 }
942
943 if (st == EdgeFetchStatus::DONE)
944 {
945 on_fetch_done(slot, fs, now);
946 }
947 else if (st == EdgeFetchStatus::FAILED && fs->revalidate && fs->reval_entry)
948 {
949 serve_hit(slot, fs->reval_entry, now, "STALE"); // stale-if-error: serve the last good copy
950 }
951 else // FAILED miss / OVERSIZE
952 {
953 s_ctx.server->send(slot, 502, PC_MIME_TEXT_PLAIN, "Bad Gateway");
954 }
955 edge_fetch_end(&fs->f, tport);
956 fs->used = false;
957 s_ctx.pending[slot].active = false;
958 return true;
959}
960
961#if PC_ENABLE_EDGE_MESH
962// --- PROTO_MESH serving side: answer a sibling's query from the LOCAL cache only (one hop, never recurses to
963// this node's own origin or peers, so the fleet cannot loop) -------------------------------------------
964
965// Case-insensitive compare of the first @p n bytes (header names).
966bool mesh_name_eq(const char *a, const char *b, size_t n)
967{
968 for (size_t i = 0; i < n; i++)
969 {
970 char ca = a[i];
971 char cb = b[i];
972 if (ca >= 'A' && ca <= 'Z')
973 {
974 ca = (char)(ca - 'A' + 'a');
975 }
976 if (cb >= 'A' && cb <= 'Z')
977 {
978 cb = (char)(cb - 'A' + 'a');
979 }
980 if (ca != cb)
981 {
982 return false;
983 }
984 }
985 return true;
986}
987
988// EdgeHdrLookup over a request-header snapshot blob (`name RS value US ...`); ctx is a MeshLookupCtx. The
989// returned pointer is valid until the next call (edge_vary_serialize copies each value before re-looking up).
990struct MeshLookupCtx
991{
992 const char *blob;
993 char valbuf[MAX_VAL_LEN];
994};
995const char *mesh_hdr_lookup(void *ctx, const char *name)
996{
997 MeshLookupCtx *lc = (MeshLookupCtx *)ctx;
998 size_t nl = strnlen(name, MAX_KEY_LEN);
999 const char *p = lc->blob;
1000 while (*p)
1001 {
1002 const char *rs = strchr(p, '\x1e');
1003 if (!rs)
1004 {
1005 break;
1006 }
1007 const char *us = strchr(rs + 1, '\x1f');
1008 if (!us)
1009 {
1010 break;
1011 }
1012 if ((size_t)(rs - p) == nl && mesh_name_eq(p, name, nl))
1013 {
1014 size_t vl = (size_t)(us - (rs + 1));
1015 if (vl >= sizeof(lc->valbuf))
1016 {
1017 vl = sizeof(lc->valbuf) - 1;
1018 }
1019 memcpy(lc->valbuf, rs + 1, vl);
1020 lc->valbuf[vl] = '\0';
1021 return lc->valbuf;
1022 }
1023 p = us + 1;
1024 }
1025 return nullptr;
1026}
1027
1028MeshConn *mesh_conn_by_slot(uint8_t slot)
1029{
1030 for (int i = 0; i < PC_MESH_MAX_CONNS; i++)
1031 {
1032 if (s_ctx.mesh_conns[i].active && s_ctx.mesh_conns[i].conn_slot == slot)
1033 {
1034 return &s_ctx.mesh_conns[i];
1035 }
1036 }
1037 return nullptr;
1038}
1039
1040// Build the response for a parsed request into mc->outbuf: a HIT carrying a fresh local variant, else a MISS.
1041void mesh_answer(MeshConn *mc, const uint8_t digest[32], const char *canon, uint32_t now)
1042{
1043 bool hit = false;
1044 uint8_t verify[32];
1045 edge_key_digest(canon, strnlen(canon, PC_EDGE_KEY_MAX), verify);
1046 if (memcmp(verify, digest, 32) == 0) // integrity: the canonical key must hash to the advertised digest
1047 {
1048 MeshLookupCtx lc;
1049 lc.blob = s_ctx.mesh_hdrs;
1050 EdgeEntry *e = edge_store_find(&s_ctx.store, canon, mesh_hdr_lookup, &lc, now);
1051 if (e && edge_entry_fresh(e, now))
1052 {
1053 long age = edge_current_age(e->initial_age, e->insert_ms, now);
1054 if (age < 0)
1055 {
1056 age = 0;
1057 }
1058 // Serialize the entry directly after the 6-byte response header to avoid a large stack temp.
1059 size_t fn = edge_mesh_serialize_entry(e, age, mc->outbuf + 6, sizeof(mc->outbuf) - 6);
1060 if (fn > 0 && fn <= 0xFFFFu)
1061 {
1062 mc->outbuf[0] = PC_EDGE_MESH_MAGIC0;
1063 mc->outbuf[1] = PC_EDGE_MESH_MAGIC1;
1064 mc->outbuf[2] = PC_EDGE_MESH_VERSION;
1065 mc->outbuf[3] = 1; // HIT
1066 mc->outbuf[4] = (uint8_t)(fn & 0xFF);
1067 mc->outbuf[5] = (uint8_t)(fn >> 8);
1068 mc->out_len = (uint16_t)(6 + fn);
1069 hit = true;
1070 }
1071 }
1072 }
1073 if (!hit)
1074 {
1075 mc->out_len = (uint16_t)edge_mesh_build_response(false, nullptr, 0, mc->outbuf, sizeof(mc->outbuf));
1076 }
1077 mc->out_off = 0;
1078 mc->responded = true;
1079}
1080
1081void mesh_serve_end(MeshConn *mc)
1082{
1083 mc->active = false;
1084 pc_conn_close(mc->conn_slot);
1085}
1086
1087// Drive one serve connection: accumulate the request, answer it, then page the response out with backpressure.
1088void mesh_serve_pump(MeshConn *mc)
1089{
1090 uint8_t slot = mc->conn_slot;
1091 if (!mc->responded)
1092 {
1093 if (pc_conn_available(slot) && mc->req_len < sizeof(mc->reqbuf))
1094 {
1095 mc->req_len += (uint16_t)pc_conn_read(slot, mc->reqbuf + mc->req_len, sizeof(mc->reqbuf) - mc->req_len);
1096 }
1097 uint8_t digest[32];
1098 char canon[PC_EDGE_KEY_MAX];
1099 EdgeMeshParse p = edge_mesh_parse_request(mc->reqbuf, mc->req_len, digest, canon, sizeof(canon),
1100 s_ctx.mesh_hdrs, sizeof(s_ctx.mesh_hdrs));
1101 if (p == EdgeMeshParse::INCOMPLETE)
1102 {
1103 if (mc->req_len >= sizeof(mc->reqbuf))
1104 {
1105 mesh_serve_end(mc); // full buffer, still short -> junk, drop
1106 }
1107 return; // otherwise wait for more
1108 }
1109 if (p != EdgeMeshParse::HIT)
1110 {
1111 mesh_serve_end(mc); // malformed
1112 return;
1113 }
1114 mesh_answer(mc, digest, canon, pc_millis());
1115 }
1116 while (mc->out_off < mc->out_len)
1117 {
1118 u16_t room = pc_conn_sndbuf(slot);
1119 if (room == 0)
1120 {
1121 return; // backpressure; retry next poll
1122 }
1123 uint16_t remaining = (uint16_t)(mc->out_len - mc->out_off);
1124 u16_t n = remaining < room ? remaining : room;
1125 if (!pc_conn_send(slot, mc->outbuf + mc->out_off, n))
1126 {
1127 return; // retry next poll
1128 }
1129 mc->out_off = (uint16_t)(mc->out_off + n);
1130 }
1131 // Whole response queued: flush it out, then dwell in CONN_CLOSING until the peer ACKs (a plain
1132 // pc_conn_close would RST and discard the response the peer has not read yet). pc_conn_send already
1133 // COPY'd the bytes into the TCP buffer and the graceful finalize does not call on_close, so free the
1134 // MeshConn now - the transport owns the drain from here.
1135 pc_conn_flush(slot);
1136 pc_conn_begin_close(slot);
1137 mc->active = false;
1138}
1139
1140void mesh_on_accept(uint8_t slot)
1141{
1142 for (int i = 0; i < PC_MESH_MAX_CONNS; i++)
1143 {
1144 if (!s_ctx.mesh_conns[i].active)
1145 {
1146 MeshConn *mc = &s_ctx.mesh_conns[i];
1147 mc->active = true;
1148 mc->conn_slot = slot;
1149 mc->req_len = 0;
1150 mc->responded = false;
1151 mc->out_off = 0;
1152 mc->out_len = 0;
1153 return;
1154 }
1155 }
1156 pc_conn_close(slot); // no free serve slot
1157}
1158
1159void mesh_on_data(uint8_t slot)
1160{
1161 MeshConn *mc = mesh_conn_by_slot(slot);
1162 if (mc)
1163 {
1164 mesh_serve_pump(mc);
1165 }
1166}
1167
1168void mesh_on_poll(uint8_t slot)
1169{
1170 if (!pc_conn_active(slot))
1171 {
1172 return;
1173 }
1174 MeshConn *mc = mesh_conn_by_slot(slot);
1175 if (mc)
1176 {
1177 mesh_serve_pump(mc);
1178 }
1179}
1180
1181void mesh_on_close(uint8_t slot)
1182{
1183 MeshConn *mc = mesh_conn_by_slot(slot);
1184 if (mc)
1185 {
1186 mc->active = false; // the transport owns the closing slot
1187 }
1188}
1189
1190const ProtoHandler s_mesh_handler = {mesh_on_accept, mesh_on_data, mesh_on_close, mesh_on_poll};
1191#endif // PC_ENABLE_EDGE_MESH
1192} // namespace
1193
1194// --- public API ----------------------------------------------------------------------------------
1195
1196void pc_edge_cache_enable(PC &server)
1197{
1198 s_ctx.server = &server;
1199 edge_store_init(&s_ctx.store);
1200 for (int i = 0; i < PC_EDGE_FETCH_SLOTS; i++)
1201 {
1202 s_ctx.fetches[i].used = false;
1203 s_ctx.fetches[i].f.cid = -1;
1204 }
1205 for (int i = 0; i < MAX_CONNS; i++)
1206 {
1207 s_ctx.pending[i].active = false;
1208 s_ctx.serve[i].active = false;
1209 s_ctx.serve[i].entry = nullptr;
1210#if PC_ENABLE_RANGE
1211 s_ctx.range_hdr[i][0] = '\0';
1212#endif
1213 }
1214 s_ctx.transport.open = t_open;
1215 s_ctx.transport.send = t_send;
1216 s_ctx.transport.read = t_read;
1217 s_ctx.transport.closed = t_closed;
1218 s_ctx.transport.close = t_close;
1219 s_ctx.transport.ctx = nullptr;
1220#if PC_ENABLE_EDGE_ORIGIN_TLS
1221 s_ctx.transport_tls.open = t_tls_open;
1222 s_ctx.transport_tls.send = t_tls_send;
1223 s_ctx.transport_tls.read = t_tls_read;
1224 s_ctx.transport_tls.closed = t_tls_closed;
1225 s_ctx.transport_tls.close = t_tls_close;
1226 s_ctx.transport_tls.ctx = nullptr;
1227 s_ctx.tls_cid = -1;
1228 s_ctx.tls_peer_closed = false;
1229#endif
1230#if PC_ENABLE_DBM
1231 s_ctx.store.on_evict = s_ctx.l2 ? edge_on_evict : nullptr; // re-arm write-back after edge_store_init
1232 s_ctx.store.evict_ctx = nullptr;
1233#endif
1234#if PC_ENABLE_EDGE_MESH
1235 for (int i = 0; i < PC_MESH_MAX_CONNS; i++)
1236 {
1237 s_ctx.mesh_conns[i].active = false;
1238 }
1239#endif
1240 if (!s_ctx.registered)
1241 {
1242 server.use(edge_cache_mw);
1243 pc_http_set_edge_poll(edge_cache_poll);
1244 s_ctx.registered = true;
1245 }
1246}
1247
1248#if PC_ENABLE_DBM
1249void pc_edge_cache_bind_sd(pc_dbm *dbm)
1250{
1251 s_ctx.l2 = dbm;
1252 s_ctx.store.on_evict = dbm ? edge_on_evict : nullptr;
1253 s_ctx.store.evict_ctx = nullptr;
1254}
1255#endif
1256
1257bool pc_edge_cache_map(const char *path_prefix, const char *origin_base_url)
1258{
1259 if (!path_prefix || !origin_base_url)
1260 {
1261 return false;
1262 }
1263 if (strnlen(path_prefix, sizeof(s_ctx.maps[0].prefix)) >= sizeof(s_ctx.maps[0].prefix))
1264 {
1265 return false;
1266 }
1267 bool https = false;
1268 char host[PC_EDGE_ORIGIN_URL_MAX];
1269 uint16_t port = 80;
1270 char ignore_path[256];
1271 if (!http_client_parse_url(origin_base_url, &https, host, sizeof(host), &port, ignore_path, sizeof(ignore_path)))
1272 {
1273 return false;
1274 }
1275#if !PC_ENABLE_EDGE_ORIGIN_TLS
1276 if (https)
1277 {
1278 return false; // plaintext origins only unless PC_ENABLE_EDGE_ORIGIN_TLS is set
1279 }
1280#endif
1281 for (int i = 0; i < PC_EDGE_MAP_MAX; i++)
1282 {
1283 if (s_ctx.maps[i].used)
1284 {
1285 continue;
1286 }
1287 strncpy(s_ctx.maps[i].prefix, path_prefix, sizeof(s_ctx.maps[i].prefix) - 1);
1288 s_ctx.maps[i].prefix[sizeof(s_ctx.maps[i].prefix) - 1] = '\0';
1289 strncpy(s_ctx.maps[i].origin_host, host, sizeof(s_ctx.maps[i].origin_host) - 1);
1290 s_ctx.maps[i].origin_host[sizeof(s_ctx.maps[i].origin_host) - 1] = '\0';
1291 s_ctx.maps[i].origin_port = port;
1292 s_ctx.maps[i].https = https;
1293 s_ctx.maps[i].used = true;
1294 return true;
1295 }
1296 return false; // map table full
1297}
1298
1299#if PC_ENABLE_EDGE_ORIGIN_TLS
1300void pc_edge_cache_set_origin_ca(const uint8_t *ca_pem, size_t len)
1301{
1302 pc_tls_client_set_ca(ca_pem, len); // shared client-TLS trust store (also used by MQTTS/wss/HTTP client)
1303}
1304void pc_edge_cache_set_origin_pin(const uint8_t sha256[32])
1305{
1306 pc_tls_client_set_pin(sha256);
1307}
1308#endif
1309
1310#if PC_ENABLE_EDGE_MESH
1311bool pc_edge_cache_add_peer(const char *host, uint16_t port)
1312{
1313 if (!host)
1314 {
1315 return false;
1316 }
1317 size_t hl = strnlen(host, PC_MESH_HOST_MAX + 1);
1318 if (hl == 0 || hl >= PC_MESH_HOST_MAX)
1319 {
1320 return false;
1321 }
1322 for (int i = 0; i < PC_MESH_MAX_PEERS; i++)
1323 {
1324 if (!s_ctx.peers[i].used)
1325 {
1326 memcpy(s_ctx.peers[i].host, host, hl + 1);
1327 s_ctx.peers[i].port = port;
1328 s_ctx.peers[i].used = true;
1329 return true;
1330 }
1331 }
1332 return false; // peer table full
1333}
1334
1335void pc_edge_cache_mesh_serve(void)
1336{
1337 if (!s_ctx.mesh_registered)
1338 {
1339 proto_register(ConnProto::PROTO_MESH, &s_mesh_handler);
1340 s_ctx.mesh_registered = true;
1341 }
1342}
1343#endif // PC_ENABLE_EDGE_MESH
1344
1345void pc_edge_cache_reset(void)
1346{
1347 edge_store_init(&s_ctx.store);
1348#if PC_ENABLE_DBM
1349 if (s_ctx.l2)
1350 {
1351 edge_sd_purge_all(s_ctx.l2);
1352 s_ctx.store.on_evict = edge_on_evict; // edge_store_init cleared it - re-arm the write-back hook
1353 }
1354#endif
1355 for (int i = 0; i < PC_EDGE_MAP_MAX; i++)
1356 {
1357 s_ctx.maps[i].used = false;
1358 }
1359#if PC_ENABLE_EDGE_MESH
1360 for (int i = 0; i < PC_MESH_MAX_PEERS; i++)
1361 {
1362 s_ctx.peers[i].used = false;
1363 }
1364#endif
1365}
1366
1367bool pc_edge_cache_purge(const char *canonical_key)
1368{
1369 if (!canonical_key)
1370 {
1371 return false;
1372 }
1373 bool purged = edge_store_purge(&s_ctx.store, canonical_key) > 0;
1374#if PC_ENABLE_DBM
1375 if (s_ctx.l2)
1376 {
1377 uint8_t digest[32];
1378 edge_key_digest(canonical_key, strnlen(canonical_key, PC_EDGE_KEY_MAX), digest);
1379 if (edge_sd_del(s_ctx.l2, digest))
1380 {
1381 purged = true;
1382 }
1383 }
1384#endif
1385 return purged;
1386}
1387
1388uint32_t pc_edge_cache_purge_prefix(const char *path_prefix)
1389{
1390 if (!path_prefix)
1391 {
1392 return 0;
1393 }
1394 uint32_t n = edge_store_purge_prefix(&s_ctx.store, path_prefix);
1395#if PC_ENABLE_DBM
1396 if (s_ctx.l2)
1397 {
1398 n += edge_sd_purge_prefix(s_ctx.l2, path_prefix, s_ctx.sd_buf, sizeof(s_ctx.sd_buf));
1399 }
1400#endif
1401 return n;
1402}
1403
1404void pc_edge_cache_stats(EdgeCacheStats *out)
1405{
1406 if (out)
1407 {
1408 *out = s_ctx.store.stats;
1409 }
1410}
1411
1412#endif // PC_ENABLE_EDGE_CACHE
#define MAX_CONNS
Definition c2_defaults.h:47
Single-port HTTP server with deterministic, zero-allocation execution.
Definition protocore.h:348
void use(Middleware mw)
Register a middleware to run before every request is dispatched.
#define PC_EDGE_FETCH_SLOTS
#define PC_EDGE_BODY_MAX
#define PC_MESH_MAX_CONNS
#define PC_MESH_MAX_PEERS
bool pc_client_send(int, const void *, size_t)
Queue len wire bytes for transmission (marshaled tcp_write + output).
Definition client.cpp:366
size_t pc_client_read(int, uint8_t *, size_t)
Drain up to cap buffered wire bytes into buf; returns the count.
Definition client.cpp:374
bool pc_client_is_closed(int)
True once the peer closed (FIN) or the connection errored.
Definition client.cpp:362
int pc_client_open(const char *, uint16_t, uint32_t)
Resolve host (dotted-quad fast path, else DNS) and connect to host : port, blocking up to timeout_ms.
Definition client.cpp:354
void pc_client_close(int)
Tear down the connection (marshaled) and return the slot to the pool.
Definition client.cpp:378
Layer 4 outbound TCP client transport - the client-side peer of the (server) transport in tcp....
Pluggable monotonic clock for all library timing.
void pcdelay(uint32_t ms)
Block for at least ms milliseconds - the library's single delay primitive.
Definition clock.h:89
uint32_t pc_millis(void)
The library's monotonic time at 1000 Hz (milliseconds).
Definition clock.h:71
CDN edge-cache tier - server glue (PC_ENABLE_EDGE_CACHE).
CDN edge-cache tier - L2 SD persistence (PC_ENABLE_EDGE_CACHE && PC_ENABLE_DBM).
CDN edge-cache tier - async origin-fetch engine (PC_ENABLE_EDGE_CACHE).
CDN edge-cache tier - mesh (sibling-cache) wire codec + async peer-query engine (PC_ENABLE_EDGE_MESH)...
Zero-heap outbound HTTP(S) client over raw lwIP (PC_ENABLE_HTTP_CLIENT).
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).
Standalone HTTP/1.1 request parser - no transport dependency.
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).
Layer 6 (Presentation) - wires the transport ring buffer to the HTTP parser.
Layer 5 (Session) - per-protocol connection handler dispatch table.
void proto_register(ConnProto proto, const ProtoHandler *h)
Register h for protocol proto (replaces any previous handler).
Definition session.cpp:38
Layer 7 (Application) - public HTTP routing API.
MwResult
Outcome of a middleware function (see Middleware).
Definition protocore.h:138
@ MW_HALT
Stop dispatch; the middleware already sent a response.
@ MW_NEXT
Continue to the next middleware / the route handler.
#define MAX_VAL_LEN
Maximum header field-value length.
@ PROTO_MESH
Edge-cache sibling link (PC_ENABLE_EDGE_MESH): answers a peer's content-addressed query.
#define MAX_QUERY_LEN
Maximum raw query-string length (everything after ?).
#define PC_MESH_HOST_MAX
#define PC_EDGE_MESH_RESP_MAX
Worst-case mesh response frame (header + entry on a HIT).
#define PC_EDGE_ORIGIN_URL_MAX
#define PC_EDGE_KEY_MAX
#define PC_EDGE_VARY_MAX
#define MAX_PATH_LEN
Maximum URL path length (including leading /).
#define PC_EDGE_SD_VALUE_MAX
Worst-case serialized L2 entry (edge_sd_serialize).
#define MAX_KEY_LEN
Maximum header field-name length (e.g. "Content-Type").
#define PC_EDGE_MAP_MAX
#define PC_MESH_HDRS_MAX
#define PC_EDGE_MESH_REQ_MAX
Worst-case mesh request frame (bounded request-header snapshot for Vary matching).
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_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
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.
Header headers[MAX_HEADERS]
Captured header fields.
char query[MAX_QUERY_LEN]
Raw query string (after ?).
uint8_t header_count
Valid entries in headers[].
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).
Per-protocol connection event/poll callbacks (Layer 5 dispatch vtable).
Bump-append target; ok latches false once an append would overflow cap.
Definition strbuf.h:30
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
void pc_conn_close(uint8_t slot)
Close connection slot gracefully (tcp_close), aborting if the FIN cannot be queued....
Definition tcp.cpp:645
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
void pc_conn_begin_close(uint8_t slot_id)
Begin a graceful close that dwells in ConnState::CONN_CLOSING until the peer ACKs.
Definition tcp.cpp:778
Layer 4 (Transport) - TCP connection pool, ring buffers, and lwIP integration.
Deterministic TLS engine: mbedTLS over a static memory pool (PC_ENABLE_TLS).