DeterministicESPAsyncWebServer v6.28.0
Zero-allocation, bounded-execution async HTTP server for ESP32
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
11#if DETWS_ENABLE_EDGE_CACHE
12
13#include "dwserver.h" // DetWebServer, Middleware, MwResult, ChunkSource
14#include "network_drivers/presentation/http_parser/http_parser.h" // HttpReq, http_get_header, http_pool
15#include "network_drivers/presentation/presentation.h" // detws_http_set_edge_poll
16#include "network_drivers/transport/client.h" // det_client_*
17#include "network_drivers/transport/tcp.h" // det_conn_active
18#include "services/clock.h" // detws_millis
20#if DETWS_ENABLE_DBM
21#include "services/edge_cache/edge_cache_sd.h" // L2 SD tier
22#endif
23#include "server/http_range.h" // http_parse_byte_range (Range/206 support)
24#include "services/http_client/http_client.h" // http_client_parse_url
25#include "shared_primitives/mime.h" // DET_MIME_TEXT_PLAIN
26#if DETWS_ENABLE_EDGE_ORIGIN_TLS
27#include "network_drivers/tls/tls.h" // det_tls_csess_* (TLS upstream origin fetch)
28#include <mbedtls/ssl.h> // MBEDTLS_ERR_SSL_WANT_READ / WANT_WRITE
29#endif
30#include <stdio.h>
31#include <string.h>
32
33namespace
34{
35struct EdgeRouteMap
36{
37 bool used;
38 char prefix[MAX_PATH_LEN];
39 char origin_host[DETWS_EDGE_ORIGIN_URL_MAX];
40 uint16_t origin_port;
41 bool https; ///< fetch this origin over TLS (DETWS_ENABLE_EDGE_ORIGIN_TLS)
42};
43
44struct EdgeFetchSlot
45{
46 bool used;
47 EdgeFetch f;
48 uint8_t client_slot;
49 bool revalidate;
50 EdgeEntry *reval_entry; // the stale entry being revalidated (nullptr for a plain miss)
51 const EdgeFetchTransport *transport; // plaintext or TLS transport, chosen per route at start_fetch
52 char canon[DETWS_EDGE_KEY_MAX];
53};
54
55struct EdgePending
56{
57 bool active;
58 uint8_t fetch_idx;
59};
60
61// The ChunkSource ctx for a paged send; must outlive the response, so it lives in the owned Ctx.
62struct EdgeServeCursor
63{
64 bool active;
65 EdgeEntry *entry;
66 uint32_t off, end;
67};
68
69// The single owned file-static: all of this subsystem's mutable state.
70struct EdgeCacheProxyCtx
71{
72 DetWebServer *server;
73 bool registered;
74 EdgeCacheStore store;
75 EdgeRouteMap maps[DETWS_EDGE_MAP_MAX];
76 EdgeFetchSlot fetches[DETWS_EDGE_FETCH_SLOTS];
77 EdgePending pending[MAX_CONNS];
78 EdgeServeCursor serve[MAX_CONNS];
79 EdgeFetchTransport transport;
80#if DETWS_ENABLE_EDGE_ORIGIN_TLS
81 EdgeFetchTransport transport_tls; // TLS binding over det_tls_csess, used for https routes
82 int tls_cid; // underlying det_client cid of the in-flight TLS fetch (singleton session)
83 bool tls_peer_closed; // latched when the TLS session reports closed / errored
84#endif
85 char reqbuf[MAX_PATH_LEN + MAX_QUERY_LEN + 256]; // scratch for one origin request line (freed by send)
86#if DETWS_ENABLE_RANGE
87 // The client's Range header, captured per slot at middleware time. serve_hit runs for a miss/stale
88 // entry from the poll loop *after* the async fetch has reused http_pool[slot], so the original request
89 // is no longer readable there - the window must be resolved against this captured copy.
90 char range_hdr[MAX_CONNS][48];
91#endif
92#if DETWS_ENABLE_DBM
93 DetwsDbm *l2; // the persistent L2 tier (nullptr = L1-only)
94 uint8_t sd_buf[EDGE_SD_VALUE_MAX]; // serialize/deserialize scratch for one L2 value
95#endif
96};
97EdgeCacheProxyCtx s_ctx;
98
99#if DETWS_ENABLE_DBM
100// L1 write-back hook: spill an evicted victim to L2 (edge_sd_put skips no-validator / oversize entries).
101void edge_on_evict(void *ctx, const EdgeEntry *victim)
102{
103 (void)ctx;
104 if (s_ctx.l2 && edge_sd_put(s_ctx.l2, victim, s_ctx.sd_buf, sizeof(s_ctx.sd_buf)))
105 s_ctx.store.stats.l2_spills++;
106}
107#endif
108
109// --- det_client transport seam -------------------------------------------------------------------
110int t_open(void *c, const char *host, uint16_t port, uint32_t timeout)
111{
112 (void)c;
113 return det_client_open(host, port, timeout);
114}
115bool t_send(void *c, int cid, const void *d, size_t l)
116{
117 (void)c;
118 return det_client_send(cid, d, l);
119}
120size_t t_read(void *c, int cid, uint8_t *b, size_t cap)
121{
122 (void)c;
123 return det_client_read(cid, b, cap);
124}
125bool t_closed(void *c, int cid)
126{
127 (void)c;
128 return det_client_is_closed(cid);
129}
130void t_close(void *c, int cid)
131{
132 (void)c;
133 det_client_close(cid);
134}
135
136#if DETWS_ENABLE_EDGE_ORIGIN_TLS
137// --- TLS transport seam (det_tls_csess layered over det_client) -----------------------------------
138// The client TLS session is a singleton, so the underlying cid + peer-closed latch live in the owned Ctx
139// (one TLS fetch at a time, enforced by det_tls_csess_active() in start_fetch). The BIO callbacks move
140// ciphertext over det_client's wire ring for that cid - the same bridge the MQTT/WS clients use.
141int edge_tls_bio_send(void *ctx, const unsigned char *buf, size_t len)
142{
143 (void)ctx;
144 size_t cap = len > 0xFFFF ? 0xFFFF : len;
145 return det_client_send(s_ctx.tls_cid, buf, cap) ? (int)cap : MBEDTLS_ERR_SSL_WANT_WRITE;
146}
147int edge_tls_bio_recv(void *ctx, unsigned char *buf, size_t len)
148{
149 (void)ctx;
150 size_t n = det_client_read(s_ctx.tls_cid, buf, len);
151 if (n == 0)
152 return det_client_is_closed(s_ctx.tls_cid) ? 0 : MBEDTLS_ERR_SSL_WANT_READ;
153 return (int)n;
154}
155
156int t_tls_open(void *c, const char *host, uint16_t port, uint32_t timeout)
157{
158 (void)c;
159 s_ctx.tls_cid = det_client_open(host, port, timeout);
160 if (s_ctx.tls_cid < 0)
161 return -1;
162 s_ctx.tls_peer_closed = false;
163 if (!det_tls_csess_begin(host, edge_tls_bio_send, edge_tls_bio_recv))
164 {
165 det_client_close(s_ctx.tls_cid);
166 s_ctx.tls_cid = -1;
167 return -1;
168 }
169 // Block through the handshake at connect - the same brief block the MQTT/WS clients take (and that the
170 // plaintext det_client_open already takes on DNS+connect). edge_fetch_begin sends the request
171 // synchronously right after open returns, so the session must be established first. dwsdelay() yields to
172 // the network stack between handshake flights so the peer's records arrive.
173 uint32_t deadline = detws_millis() + timeout;
174 int h = 0;
175 while ((h = det_tls_csess_handshake()) == 0 && !det_client_is_closed(s_ctx.tls_cid) &&
176 (int32_t)(deadline - detws_millis()) > 0)
177 dwsdelay(5);
178 if (h != 1)
179 {
180 det_tls_csess_end();
181 det_client_close(s_ctx.tls_cid);
182 s_ctx.tls_cid = -1;
183 return -1;
184 }
185 return s_ctx.tls_cid;
186}
187bool t_tls_send(void *c, int cid, const void *d, size_t l)
188{
189 (void)c;
190 (void)cid;
191 return det_tls_csess_write((const uint8_t *)d, l) == (int)l;
192}
193size_t t_tls_read(void *c, int cid, uint8_t *b, size_t cap)
194{
195 (void)c;
196 (void)cid;
197 int n = det_tls_csess_read(b, cap);
198 if (n < 0)
199 s_ctx.tls_peer_closed = true; // close_notify / decrypt error -> report closed via t_tls_closed
200 return n > 0 ? (size_t)n : 0;
201}
202bool t_tls_closed(void *c, int cid)
203{
204 (void)c;
205 return s_ctx.tls_peer_closed || det_client_is_closed(cid);
206}
207void t_tls_close(void *c, int cid)
208{
209 (void)c;
210 det_tls_csess_end();
211 det_client_close(cid);
212 s_ctx.tls_cid = -1;
213}
214#endif // DETWS_ENABLE_EDGE_ORIGIN_TLS
215
216// Request-header lookup used to (re)serialize the Vary secondary key; ctx is the client HttpReq.
217const char *req_lookup(void *ctx, const char *name)
218{
219 return http_get_header((const HttpReq *)ctx, name);
220}
221
222EdgeRouteMap *map_match(const char *path)
223{
224 for (int i = 0; i < DETWS_EDGE_MAP_MAX; i++)
225 {
226 if (!s_ctx.maps[i].used)
227 continue;
228 size_t pl = strlen(s_ctx.maps[i].prefix);
229 if (strncmp(path, s_ctx.maps[i].prefix, pl) == 0)
230 return &s_ctx.maps[i];
231 }
232 return nullptr;
233}
234
235int alloc_fetch()
236{
237 for (int i = 0; i < DETWS_EDGE_FETCH_SLOTS; i++)
238 if (!s_ctx.fetches[i].used)
239 return i;
240 return -1;
241}
242
243// The ChunkSource: page the cached body to the client. On completion (or exhaustion) release a
244// transient passthrough entry (key ""). A dropped connection leaves the transient LRU-reclaimable.
245size_t edge_chunk_source(uint8_t *buf, size_t cap, void *ctx)
246{
247 EdgeServeCursor *c = (EdgeServeCursor *)ctx;
248 if (!c->active || !c->entry)
249 return 0;
250 size_t remaining = c->end - c->off;
251 if (remaining == 0)
252 {
253 c->active = false;
254 if (c->entry->key[0] == '\0') // transient passthrough entry -> free its slot
255 edge_store_free_entry(&s_ctx.store, c->entry);
256 c->entry = nullptr;
257 return 0;
258 }
259 size_t n = remaining < cap ? remaining : cap;
260 memcpy(buf, c->entry->body + c->off, n);
261 c->off += n;
262 return n;
263}
264
265// Serve a cache entry, replaying its validators + Age, tagged with @p xcache. A client `Range` request
266// (DETWS_ENABLE_RANGE) is answered with a 206 window (or 416 if unsatisfiable); otherwise a full 200.
267void serve_hit(uint8_t slot, EdgeEntry *e, uint32_t now, const char *xcache)
268{
269 EdgeServeCursor *c = &s_ctx.serve[slot];
270 c->active = true;
271 c->entry = e;
272 c->off = 0;
273 c->end = e->body_len;
274 int status = 200;
275
276#if DETWS_ENABLE_RANGE
277 const char *range = s_ctx.range_hdr[slot]; // captured at mw time (http_pool[slot] is stale post-fetch)
278 if (range[0])
279 {
280 size_t rs = 0;
281 size_t re = 0;
282 int rr = http_parse_byte_range(range, e->body_len, &rs, &re);
283 if (rr < 0) // syntactically valid but unsatisfiable -> 416, no body window served
284 {
285 char cr[48];
286 snprintf(cr, sizeof(cr), "bytes */%u", (unsigned)e->body_len);
287 s_ctx.server->add_response_header(slot, "Content-Range", cr);
288 c->active = false;
289 c->entry = nullptr;
290 s_ctx.server->send(slot, 416, DET_MIME_TEXT_PLAIN, "Range Not Satisfiable");
291 return;
292 }
293 if (rr > 0) // satisfiable -> 206 with just the requested window [rs, re]
294 {
295 status = 206;
296 c->off = (uint32_t)rs;
297 c->end = (uint32_t)re + 1;
298 char cr[48];
299 snprintf(cr, sizeof(cr), "bytes %u-%u/%u", (unsigned)rs, (unsigned)re, (unsigned)e->body_len);
300 s_ctx.server->add_response_header(slot, "Content-Range", cr);
301 }
302 }
303 s_ctx.server->add_response_header(slot, "Accept-Ranges", "bytes"); // advertise range support
304#endif
305
306 s_ctx.server->add_response_header(slot, "X-Cache", xcache);
307 if (e->etag[0])
308 s_ctx.server->add_response_header(slot, "ETag", e->etag);
309 if (e->last_modified[0])
310 s_ctx.server->add_response_header(slot, "Last-Modified", e->last_modified);
311 if (e->content_encoding[0])
312 s_ctx.server->add_response_header(slot, "Content-Encoding", e->content_encoding);
313 long age = edge_current_age(e->initial_age, e->insert_ms, now);
314 if (age < 0)
315 age = 0;
316 char agebuf[12];
317 snprintf(agebuf, sizeof(agebuf), "%ld", age);
318 s_ctx.server->add_response_header(slot, "Age", agebuf);
319 const char *ct = e->content_type[0] ? e->content_type : "application/octet-stream";
320 s_ctx.server->send_chunked(slot, status, ct, edge_chunk_source, c);
321}
322
323// Serve a non-cacheable / non-200 origin response through a transient unindexed store slot, so the
324// serve source outlives the fetch (which the caller frees) and no-store content is never re-served.
325void serve_passthrough(uint8_t slot, EdgeFetch *f)
326{
327 EdgeEntry *e = edge_store_alloc(&s_ctx.store, "", ""); // key "" -> never matched by a lookup
328 if (!e)
329 {
330 s_ctx.server->send(slot, 502, DET_MIME_TEXT_PLAIN, "Bad Gateway");
331 return;
332 }
333 s_ctx.store.stats.stores--; // a transient is not a cache store
334 e->status = f->status;
335 if (!edge_header_value((const char *)f->buf, f->head_len, "Content-Type", e->content_type, sizeof(e->content_type)))
336 strncpy(e->content_type, "application/octet-stream", sizeof(e->content_type) - 1);
337 edge_header_value((const char *)f->buf, f->head_len, "Content-Encoding", e->content_encoding,
338 sizeof(e->content_encoding));
339 size_t bl = f->body_len;
340 if (bl > DETWS_EDGE_BODY_MAX)
342 memcpy(e->body, f->buf + f->body_off, bl);
343 e->body_len = (uint16_t)bl;
344
345 EdgeServeCursor *c = &s_ctx.serve[slot];
346 c->active = true;
347 c->entry = e;
348 c->off = 0;
349 c->end = e->body_len;
350 s_ctx.server->add_response_header(slot, "X-Cache", "MISS");
351 if (e->content_encoding[0])
352 s_ctx.server->add_response_header(slot, "Content-Encoding", e->content_encoding);
353 const char *ct = e->content_type[0] ? e->content_type : "application/octet-stream";
354 s_ctx.server->send_chunked(slot, e->status ? e->status : 200, ct, edge_chunk_source, c);
355}
356
357// Store a cacheable 200 response into a fresh entry and serve it.
358void store_response(uint8_t slot, EdgeFetchSlot *fs, HttpReq *req, const DetwsCacheControl *cc, const char *vary_hdr,
359 uint32_t now)
360{
361 EdgeFetch *f = &fs->f;
362 const char *head = (const char *)f->buf;
363 size_t head_len = f->head_len;
364
365 char vary_vals[DETWS_EDGE_VARY_MAX];
366 edge_vary_serialize(vary_hdr[0] ? vary_hdr : nullptr, req_lookup, req, vary_vals, sizeof(vary_vals));
367
368 EdgeEntry *e = edge_store_alloc(&s_ctx.store, fs->canon, vary_vals);
369 if (!e)
370 {
371 serve_passthrough(slot, f);
372 return;
373 }
374 e->status = 200;
375 edge_header_value(head, head_len, "Content-Type", e->content_type, sizeof(e->content_type));
376 edge_header_value(head, head_len, "Content-Encoding", e->content_encoding, sizeof(e->content_encoding));
377 edge_header_value(head, head_len, "ETag", e->etag, sizeof(e->etag));
378 edge_header_value(head, head_len, "Last-Modified", e->last_modified, sizeof(e->last_modified));
379 if (vary_hdr[0] && strlen(vary_hdr) < sizeof(e->vary_names))
380 memcpy(e->vary_names, vary_hdr, strlen(vary_hdr) + 1);
381
382 size_t bl = f->body_len;
383 if (bl > DETWS_EDGE_BODY_MAX)
385 memcpy(e->body, f->buf + f->body_off, bl);
386 e->body_len = (uint16_t)bl;
387 s_ctx.store.stats.bytes_stored += bl;
388
389 int64_t date = -1;
390 int64_t expires = -1;
391 int64_t last_mod = -1;
392 int32_t age = 0;
393 char v[64];
394 if (edge_header_value(head, head_len, "Date", v, sizeof(v)))
395 date = edge_parse_http_date(v, strlen(v));
396 if (edge_header_value(head, head_len, "Expires", v, sizeof(v)))
397 expires = edge_parse_http_date(v, strlen(v));
398 if (e->last_modified[0])
399 last_mod = edge_parse_http_date(e->last_modified, strlen(e->last_modified));
400 if (edge_header_value(head, head_len, "Age", v, sizeof(v)))
401 {
402 long a = 0;
403 for (const char *p = v; *p >= '0' && *p <= '9'; p++)
404 a = a * 10 + (*p - '0');
405 age = (int32_t)a;
406 }
407 edge_entry_set_freshness(e, cc, /*shared=*/true, date, expires, last_mod, age, /*response_time=*/-1, now);
408 serve_hit(slot, e, now, "MISS");
409}
410
411// A completed origin fetch: revalidation 304 / store 200 / pass through anything else.
412void on_fetch_done(uint8_t slot, EdgeFetchSlot *fs, uint32_t now)
413{
414 EdgeFetch *f = &fs->f;
415 const char *head = (const char *)f->buf;
416 size_t head_len = f->head_len;
417 HttpReq *req = &http_pool[slot];
418
419 if (fs->revalidate && f->status == 304 && fs->reval_entry)
420 {
421 edge_apply_304(fs->reval_entry, head, head_len, -1, now);
422 s_ctx.store.stats.revalidations_304++;
423 serve_hit(slot, fs->reval_entry, now, "REVALIDATED");
424 return;
425 }
426 if (f->status == 200)
427 {
428 DetwsCacheControl cc;
429 cache_control_init(&cc);
430 char v[128];
431 if (edge_header_value(head, head_len, "Cache-Control", v, sizeof(v)))
432 cache_control_parse(v, strlen(v), &cc);
433 char vary_hdr[DETWS_EDGE_VARY_MAX];
434 vary_hdr[0] = '\0';
435 edge_header_value(head, head_len, "Vary", vary_hdr, sizeof(vary_hdr));
436 if (edge_is_storeable(200, "GET", &cc, vary_hdr[0] ? vary_hdr : nullptr, f->body_len))
437 {
438 if (fs->revalidate && fs->reval_entry) // 200 on a revalidation replaces the stale entry
439 {
440 edge_store_free_entry(&s_ctx.store, fs->reval_entry);
441 s_ctx.store.stats.replaces_200++;
442 }
443 store_response(slot, fs, req, &cc, vary_hdr, now);
444 return;
445 }
446 serve_passthrough(slot, f); // 200 but not storeable
447 return;
448 }
449 serve_passthrough(slot, f); // non-200 status
450}
451
452// Forward decls for the seam functions installed by det_edge_cache_enable().
453MwResult edge_cache_mw(uint8_t slot, HttpReq *req);
454bool edge_cache_poll(uint8_t slot);
455
456bool start_fetch(uint8_t slot, HttpReq *req, EdgeRouteMap *m, const char *canon, EdgeEntry *reval, uint32_t now)
457{
458 const EdgeFetchTransport *tport = &s_ctx.transport;
459#if DETWS_ENABLE_EDGE_ORIGIN_TLS
460 if (m->https)
461 {
462 if (det_tls_csess_active())
463 return false; // the shared client-TLS session is busy -> fail open (never tear down a live one)
464 tport = &s_ctx.transport_tls;
465 }
466#endif
467 int fi = alloc_fetch();
468 if (fi < 0)
469 return false;
470 EdgeFetchSlot *fs = &s_ctx.fetches[fi];
471 char cond[192];
472 cond[0] = '\0';
473 if (reval)
474 edge_build_conditional(reval, cond, sizeof(cond));
475 int rl =
476 snprintf(s_ctx.reqbuf, sizeof(s_ctx.reqbuf),
477 "GET %s%s%s HTTP/1.1\r\nHost: %s\r\nUser-Agent: DetWebServer-EdgeCache\r\nConnection: close\r\n%s\r\n",
478 req->path, req->query[0] ? "?" : "", req->query, m->origin_host, cond);
479 if (rl <= 0 || (size_t)rl >= sizeof(s_ctx.reqbuf))
480 return false;
481 edge_fetch_begin(&fs->f, tport, m->origin_host, m->origin_port, s_ctx.reqbuf, (size_t)rl, now);
482 if (fs->f.st == EdgeFetchStatus::FAILED)
483 {
484 edge_fetch_end(&fs->f, tport);
485 return false;
486 }
487 fs->used = true;
488 fs->client_slot = slot;
489 fs->revalidate = (reval != nullptr);
490 fs->reval_entry = reval;
491 fs->transport = tport;
492 memcpy(fs->canon, canon, strlen(canon) + 1);
493 s_ctx.pending[slot].active = true;
494 s_ctx.pending[slot].fetch_idx = (uint8_t)fi;
495 return true;
496}
497
498#if DETWS_ENABLE_DBM
499// Promote a reboot-surviving entry from L2 into a fresh L1 slot, forced stale so the caller revalidates it
500// (the monotonic insert time is meaningless across a reboot). @return the promoted entry, or nullptr.
501EdgeEntry *try_promote_l2(const char *canon, uint32_t now)
502{
503 uint8_t digest[32];
504 edge_key_digest(canon, strlen(canon), digest);
505 EdgeEntry *e = edge_store_alloc(&s_ctx.store, canon, ""); // may evict + write-back an L1 victim first
506 if (!e)
507 return nullptr;
508 if (!edge_sd_get(s_ctx.l2, digest, e, s_ctx.sd_buf, sizeof(s_ctx.sd_buf)) || strcmp(e->key, canon) != 0)
509 {
510 edge_store_free_entry(&s_ctx.store, e); // L2 miss or digest collision -> not promoted
511 return nullptr;
512 }
513 e->lifetime_s = 0; // force stale: freshness is untrustworthy across a reboot -> caller revalidates
514 e->initial_age = 0;
515 e->date_epoch = e->expires_epoch = -1;
516 e->age_hdr = 0;
517 e->insert_ms = now;
518 e->last_used_ms = now;
519 s_ctx.store.stats.l2_promotes++;
520 return e;
521}
522#endif
523
524// The cache middleware: fresh hit -> serve; stale/miss -> start an async origin fetch (suspend); else
525// fall through (fail open).
526MwResult edge_cache_mw(uint8_t slot, HttpReq *req)
527{
528 if (!s_ctx.registered || !s_ctx.server || slot >= MAX_CONNS)
529 return MwResult::MW_NEXT;
530 bool is_get = strcmp(req->method, "GET") == 0;
531 bool is_head = strcmp(req->method, "HEAD") == 0;
532 if (!is_get && !is_head)
533 return MwResult::MW_NEXT; // only cache safe methods
534 if (http_get_header(req, "Authorization"))
535 return MwResult::MW_NEXT; // never cache authorized/private requests
536 EdgeRouteMap *m = map_match(req->path);
537 if (!m)
538 return MwResult::MW_NEXT; // not a mapped origin
539
540 const char *host = http_get_header(req, "Host");
541 if (!host)
542 host = "";
543 char canon[DETWS_EDGE_KEY_MAX];
544 if (edge_key_canon("GET", host, req->path, req->query, /*include_query=*/true, canon, sizeof(canon)) == 0)
545 return MwResult::MW_NEXT; // key too long -> uncacheable, fail open
546
547#if DETWS_ENABLE_RANGE
548 // Capture the Range header now, while http_pool[slot] is the client request: a miss serves from the
549 // poll after the async fetch has reused that buffer, so serve_hit resolves the window against this copy.
550 const char *rh = http_get_header(req, "Range");
551 strncpy(s_ctx.range_hdr[slot], rh ? rh : "", sizeof(s_ctx.range_hdr[slot]) - 1);
552 s_ctx.range_hdr[slot][sizeof(s_ctx.range_hdr[slot]) - 1] = '\0';
553#endif
554
555 uint32_t now = detws_millis();
556 EdgeEntry *e = edge_store_find(&s_ctx.store, canon, req_lookup, req, now);
557 if (e && edge_entry_fresh(e, now))
558 {
559 s_ctx.store.stats.hits++;
560 serve_hit(slot, e, now, "HIT");
561 return MwResult::MW_HALT;
562 }
563#if DETWS_ENABLE_DBM
564 if (!e && s_ctx.l2) // L1 miss: try promoting a reboot-surviving entry from L2 (force-stale -> revalidate)
565 e = try_promote_l2(canon, now);
566#endif
567 s_ctx.store.stats.misses++;
568 EdgeEntry *reval = (e && edge_entry_has_validator(e)) ? e : nullptr;
569 if (!start_fetch(slot, req, m, canon, reval, now))
570 return MwResult::MW_NEXT; // no fetch slot / origin open failed -> fail open to normal dispatch
571 return MwResult::MW_HALT; // client request suspended until the fetch completes
572}
573
574// Per-slot poll hook: drive an in-flight origin fetch, then serve. Returns true while it owns the slot.
575bool edge_cache_poll(uint8_t slot)
576{
577 if (slot >= MAX_CONNS || !s_ctx.pending[slot].active)
578 return false;
579 uint8_t fi = s_ctx.pending[slot].fetch_idx;
580 EdgeFetchSlot *fs = &s_ctx.fetches[fi];
581 const EdgeFetchTransport *tport = fs->transport; // the transport chosen for this fetch (plaintext or TLS)
582 uint32_t now = detws_millis();
583
584 if (!det_conn_active(slot)) // client vanished mid-fetch: abort
585 {
586 edge_fetch_end(&fs->f, tport);
587 fs->used = false;
588 s_ctx.pending[slot].active = false;
589 return true;
590 }
591
592 EdgeFetchStatus st = edge_fetch_pump(&fs->f, tport, now);
593 if (st == EdgeFetchStatus::PENDING)
594 return true; // still receiving; owns the slot
595
596 if (st == EdgeFetchStatus::DONE)
597 {
598 on_fetch_done(slot, fs, now);
599 }
600 else if (st == EdgeFetchStatus::FAILED && fs->revalidate && fs->reval_entry)
601 {
602 serve_hit(slot, fs->reval_entry, now, "STALE"); // stale-if-error: serve the last good copy
603 }
604 else // FAILED miss / OVERSIZE
605 {
606 s_ctx.server->send(slot, 502, DET_MIME_TEXT_PLAIN, "Bad Gateway");
607 }
608 edge_fetch_end(&fs->f, tport);
609 fs->used = false;
610 s_ctx.pending[slot].active = false;
611 return true;
612}
613} // namespace
614
615// --- public API ----------------------------------------------------------------------------------
616
617void det_edge_cache_enable(DetWebServer &server)
618{
619 s_ctx.server = &server;
620 edge_store_init(&s_ctx.store);
621 for (int i = 0; i < DETWS_EDGE_FETCH_SLOTS; i++)
622 {
623 s_ctx.fetches[i].used = false;
624 s_ctx.fetches[i].f.cid = -1;
625 }
626 for (int i = 0; i < MAX_CONNS; i++)
627 {
628 s_ctx.pending[i].active = false;
629 s_ctx.serve[i].active = false;
630 s_ctx.serve[i].entry = nullptr;
631#if DETWS_ENABLE_RANGE
632 s_ctx.range_hdr[i][0] = '\0';
633#endif
634 }
635 s_ctx.transport.open = t_open;
636 s_ctx.transport.send = t_send;
637 s_ctx.transport.read = t_read;
638 s_ctx.transport.closed = t_closed;
639 s_ctx.transport.close = t_close;
640 s_ctx.transport.ctx = nullptr;
641#if DETWS_ENABLE_EDGE_ORIGIN_TLS
642 s_ctx.transport_tls.open = t_tls_open;
643 s_ctx.transport_tls.send = t_tls_send;
644 s_ctx.transport_tls.read = t_tls_read;
645 s_ctx.transport_tls.closed = t_tls_closed;
646 s_ctx.transport_tls.close = t_tls_close;
647 s_ctx.transport_tls.ctx = nullptr;
648 s_ctx.tls_cid = -1;
649 s_ctx.tls_peer_closed = false;
650#endif
651#if DETWS_ENABLE_DBM
652 s_ctx.store.on_evict = s_ctx.l2 ? edge_on_evict : nullptr; // re-arm write-back after edge_store_init
653 s_ctx.store.evict_ctx = nullptr;
654#endif
655 if (!s_ctx.registered)
656 {
657 server.use(edge_cache_mw);
658 detws_http_set_edge_poll(edge_cache_poll);
659 s_ctx.registered = true;
660 }
661}
662
663#if DETWS_ENABLE_DBM
664void det_edge_cache_bind_sd(DetwsDbm *dbm)
665{
666 s_ctx.l2 = dbm;
667 s_ctx.store.on_evict = dbm ? edge_on_evict : nullptr;
668 s_ctx.store.evict_ctx = nullptr;
669}
670#endif
671
672bool det_edge_cache_map(const char *path_prefix, const char *origin_base_url)
673{
674 if (!path_prefix || !origin_base_url)
675 return false;
676 if (strlen(path_prefix) >= sizeof(s_ctx.maps[0].prefix))
677 return false;
678 bool https = false;
679 char host[DETWS_EDGE_ORIGIN_URL_MAX];
680 uint16_t port = 80;
681 char ignore_path[256];
682 if (!http_client_parse_url(origin_base_url, &https, host, sizeof(host), &port, ignore_path, sizeof(ignore_path)))
683 return false;
684#if !DETWS_ENABLE_EDGE_ORIGIN_TLS
685 if (https)
686 return false; // plaintext origins only unless DETWS_ENABLE_EDGE_ORIGIN_TLS is set
687#endif
688 for (int i = 0; i < DETWS_EDGE_MAP_MAX; i++)
689 {
690 if (s_ctx.maps[i].used)
691 continue;
692 strncpy(s_ctx.maps[i].prefix, path_prefix, sizeof(s_ctx.maps[i].prefix) - 1);
693 s_ctx.maps[i].prefix[sizeof(s_ctx.maps[i].prefix) - 1] = '\0';
694 strncpy(s_ctx.maps[i].origin_host, host, sizeof(s_ctx.maps[i].origin_host) - 1);
695 s_ctx.maps[i].origin_host[sizeof(s_ctx.maps[i].origin_host) - 1] = '\0';
696 s_ctx.maps[i].origin_port = port;
697 s_ctx.maps[i].https = https;
698 s_ctx.maps[i].used = true;
699 return true;
700 }
701 return false; // map table full
702}
703
704#if DETWS_ENABLE_EDGE_ORIGIN_TLS
705void det_edge_cache_set_origin_ca(const uint8_t *ca_pem, size_t len)
706{
707 det_tls_client_set_ca(ca_pem, len); // shared client-TLS trust store (also used by MQTTS/wss/HTTP client)
708}
709void det_edge_cache_set_origin_pin(const uint8_t sha256[32])
710{
711 det_tls_client_set_pin(sha256);
712}
713#endif
714
715void det_edge_cache_reset(void)
716{
717 edge_store_init(&s_ctx.store);
718#if DETWS_ENABLE_DBM
719 if (s_ctx.l2)
720 {
721 edge_sd_purge_all(s_ctx.l2);
722 s_ctx.store.on_evict = edge_on_evict; // edge_store_init cleared it - re-arm the write-back hook
723 }
724#endif
725 for (int i = 0; i < DETWS_EDGE_MAP_MAX; i++)
726 s_ctx.maps[i].used = false;
727}
728
729bool det_edge_cache_purge(const char *canonical_key)
730{
731 if (!canonical_key)
732 return false;
733 bool purged = edge_store_purge(&s_ctx.store, canonical_key) > 0;
734#if DETWS_ENABLE_DBM
735 if (s_ctx.l2)
736 {
737 uint8_t digest[32];
738 edge_key_digest(canonical_key, strlen(canonical_key), digest);
739 if (edge_sd_del(s_ctx.l2, digest))
740 purged = true;
741 }
742#endif
743 return purged;
744}
745
746uint32_t det_edge_cache_purge_prefix(const char *path_prefix)
747{
748 if (!path_prefix)
749 return 0;
750 uint32_t n = edge_store_purge_prefix(&s_ctx.store, path_prefix);
751#if DETWS_ENABLE_DBM
752 if (s_ctx.l2)
753 n += edge_sd_purge_prefix(s_ctx.l2, path_prefix, s_ctx.sd_buf, sizeof(s_ctx.sd_buf));
754#endif
755 return n;
756}
757
758void det_edge_cache_stats(EdgeCacheStats *out)
759{
760 if (out)
761 *out = s_ctx.store.stats;
762}
763
764#endif // DETWS_ENABLE_EDGE_CACHE
#define DETWS_EDGE_BODY_MAX
#define DETWS_EDGE_FETCH_SLOTS
#define MAX_QUERY_LEN
Maximum raw query-string length (everything after ?).
#define DETWS_EDGE_ORIGIN_URL_MAX
#define DETWS_EDGE_VARY_MAX
#define DETWS_EDGE_MAP_MAX
#define MAX_PATH_LEN
Maximum URL path length (including leading /).
#define MAX_CONNS
Maximum simultaneous TCP connections (fixed static pool; ~3.95 KB of internal RAM per slot).
#define DETWS_EDGE_KEY_MAX
Single-port HTTP server with deterministic, zero-allocation execution.
Definition dwserver.h:348
void use(Middleware mw)
Register a middleware to run before every request is dispatched.
bool det_client_is_closed(int)
True once the peer closed (FIN) or the connection errored.
Definition client.cpp:318
int det_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:310
size_t det_client_read(int, uint8_t *, size_t)
Drain up to cap buffered wire bytes into buf; returns the count.
Definition client.cpp:330
void det_client_close(int)
Tear down the connection (marshaled) and return the slot to the pool.
Definition client.cpp:334
bool det_client_send(int, const void *, size_t)
Queue len wire bytes for transmission (marshaled tcp_write + output).
Definition client.cpp:322
Layer 4 outbound TCP client transport - the client-side peer of the (server) transport in tcp....
Pluggable monotonic clock for all library timing.
uint32_t detws_millis(void)
The library's monotonic time at 1000 Hz (milliseconds).
Definition clock.h:71
void dwsdelay(uint32_t ms)
Block for at least ms milliseconds - the library's single delay primitive.
Definition clock.h:87
Layer 7 (Application) - public HTTP routing API.
MwResult
Outcome of a middleware function (see Middleware).
Definition dwserver.h:138
@ MW_HALT
Stop dispatch; the middleware already sent a response.
@ MW_NEXT
Continue to the next middleware / the route handler.
CDN edge-cache tier - server glue (DETWS_ENABLE_EDGE_CACHE).
CDN edge-cache tier - L2 SD persistence (DETWS_ENABLE_EDGE_CACHE && DETWS_ENABLE_DBM).
CDN edge-cache tier - async origin-fetch engine (DETWS_ENABLE_EDGE_CACHE).
Zero-heap outbound HTTP(S) client over raw lwIP (DETWS_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.
Fully-parsed HTTP/1.1 request.
char query[MAX_QUERY_LEN]
Raw query string (after ?).
char method[DETWS_METHOD_BUF_SIZE]
HTTP method, null-terminated (OPTIONS, or WebDAV methods when enabled).
char path[MAX_PATH_LEN]
URL path, null-terminated; no query string.
Layer 4 (Transport) - TCP connection pool, ring buffers, and lwIP integration.
Deterministic TLS engine: mbedTLS over a static memory pool (DETWS_ENABLE_TLS).