11#if DETWS_ENABLE_EDGE_CACHE
26#if DETWS_ENABLE_EDGE_ORIGIN_TLS
28#include <mbedtls/ssl.h>
50 EdgeEntry *reval_entry;
51 const EdgeFetchTransport *transport;
70struct EdgeCacheProxyCtx
79 EdgeFetchTransport transport;
80#if DETWS_ENABLE_EDGE_ORIGIN_TLS
81 EdgeFetchTransport transport_tls;
94 uint8_t sd_buf[EDGE_SD_VALUE_MAX];
97EdgeCacheProxyCtx s_ctx;
101void edge_on_evict(
void *ctx,
const EdgeEntry *victim)
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++;
110int t_open(
void *c,
const char *host, uint16_t port, uint32_t timeout)
115bool t_send(
void *c,
int cid,
const void *d,
size_t l)
120size_t t_read(
void *c,
int cid, uint8_t *b,
size_t cap)
125bool t_closed(
void *c,
int cid)
130void t_close(
void *c,
int cid)
136#if DETWS_ENABLE_EDGE_ORIGIN_TLS
141int edge_tls_bio_send(
void *ctx,
const unsigned char *buf,
size_t len)
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;
147int edge_tls_bio_recv(
void *ctx,
unsigned char *buf,
size_t len)
156int t_tls_open(
void *c,
const char *host, uint16_t port, uint32_t timeout)
160 if (s_ctx.tls_cid < 0)
162 s_ctx.tls_peer_closed =
false;
163 if (!det_tls_csess_begin(host, edge_tls_bio_send, edge_tls_bio_recv))
185 return s_ctx.tls_cid;
187bool t_tls_send(
void *c,
int cid,
const void *d,
size_t l)
191 return det_tls_csess_write((
const uint8_t *)d, l) == (int)l;
193size_t t_tls_read(
void *c,
int cid, uint8_t *b,
size_t cap)
197 int n = det_tls_csess_read(b, cap);
199 s_ctx.tls_peer_closed =
true;
200 return n > 0 ? (size_t)n : 0;
202bool t_tls_closed(
void *c,
int cid)
207void t_tls_close(
void *c,
int cid)
217const char *req_lookup(
void *ctx,
const char *name)
222EdgeRouteMap *map_match(
const char *path)
226 if (!s_ctx.maps[i].used)
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];
238 if (!s_ctx.fetches[i].used)
245size_t edge_chunk_source(uint8_t *buf,
size_t cap,
void *ctx)
247 EdgeServeCursor *c = (EdgeServeCursor *)ctx;
248 if (!c->active || !c->entry)
250 size_t remaining = c->end - c->off;
254 if (c->entry->key[0] ==
'\0')
255 edge_store_free_entry(&s_ctx.store, c->entry);
259 size_t n = remaining < cap ? remaining : cap;
260 memcpy(buf, c->entry->body + c->off, n);
267void serve_hit(uint8_t slot, EdgeEntry *e, uint32_t now,
const char *xcache)
269 EdgeServeCursor *c = &s_ctx.serve[slot];
273 c->end = e->body_len;
276#if DETWS_ENABLE_RANGE
277 const char *range = s_ctx.range_hdr[slot];
282 int rr = http_parse_byte_range(range, e->body_len, &rs, &re);
286 snprintf(cr,
sizeof(cr),
"bytes */%u", (
unsigned)e->body_len);
287 s_ctx.server->add_response_header(slot,
"Content-Range", cr);
290 s_ctx.server->send(slot, 416, DET_MIME_TEXT_PLAIN,
"Range Not Satisfiable");
296 c->off = (uint32_t)rs;
297 c->end = (uint32_t)re + 1;
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);
303 s_ctx.server->add_response_header(slot,
"Accept-Ranges",
"bytes");
306 s_ctx.server->add_response_header(slot,
"X-Cache", xcache);
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);
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);
325void serve_passthrough(uint8_t slot, EdgeFetch *f)
327 EdgeEntry *e = edge_store_alloc(&s_ctx.store,
"",
"");
330 s_ctx.server->send(slot, 502, DET_MIME_TEXT_PLAIN,
"Bad Gateway");
333 s_ctx.store.stats.stores--;
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;
342 memcpy(e->body, f->buf + f->body_off, bl);
343 e->body_len = (uint16_t)bl;
345 EdgeServeCursor *c = &s_ctx.serve[slot];
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);
358void store_response(uint8_t slot, EdgeFetchSlot *fs,
HttpReq *req,
const DetwsCacheControl *cc,
const char *vary_hdr,
361 EdgeFetch *f = &fs->f;
362 const char *head = (
const char *)f->buf;
363 size_t head_len = f->head_len;
366 edge_vary_serialize(vary_hdr[0] ? vary_hdr : nullptr, req_lookup, req, vary_vals, sizeof(vary_vals));
368 EdgeEntry *e = edge_store_alloc(&s_ctx.store, fs->canon, vary_vals);
371 serve_passthrough(slot, f);
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);
382 size_t bl = f->body_len;
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;
390 int64_t expires = -1;
391 int64_t last_mod = -1;
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)))
403 for (
const char *p = v; *p >=
'0' && *p <=
'9'; p++)
404 a = a * 10 + (*p -
'0');
407 edge_entry_set_freshness(e, cc,
true, date, expires, last_mod, age, -1, now);
408 serve_hit(slot, e, now,
"MISS");
412void on_fetch_done(uint8_t slot, EdgeFetchSlot *fs, uint32_t now)
414 EdgeFetch *f = &fs->f;
415 const char *head = (
const char *)f->buf;
416 size_t head_len = f->head_len;
419 if (fs->revalidate && f->status == 304 && fs->reval_entry)
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");
426 if (f->status == 200)
428 DetwsCacheControl cc;
429 cache_control_init(&cc);
431 if (edge_header_value(head, head_len,
"Cache-Control", v,
sizeof(v)))
432 cache_control_parse(v, strlen(v), &cc);
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))
438 if (fs->revalidate && fs->reval_entry)
440 edge_store_free_entry(&s_ctx.store, fs->reval_entry);
441 s_ctx.store.stats.replaces_200++;
443 store_response(slot, fs, req, &cc, vary_hdr, now);
446 serve_passthrough(slot, f);
449 serve_passthrough(slot, f);
454bool edge_cache_poll(uint8_t slot);
456bool start_fetch(uint8_t slot,
HttpReq *req, EdgeRouteMap *m,
const char *canon, EdgeEntry *reval, uint32_t now)
458 const EdgeFetchTransport *tport = &s_ctx.transport;
459#if DETWS_ENABLE_EDGE_ORIGIN_TLS
462 if (det_tls_csess_active())
464 tport = &s_ctx.transport_tls;
467 int fi = alloc_fetch();
470 EdgeFetchSlot *fs = &s_ctx.fetches[fi];
474 edge_build_conditional(reval, cond,
sizeof(cond));
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))
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)
484 edge_fetch_end(&fs->f, tport);
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;
501EdgeEntry *try_promote_l2(
const char *canon, uint32_t now)
504 edge_key_digest(canon, strlen(canon), digest);
505 EdgeEntry *e = edge_store_alloc(&s_ctx.store, canon,
"");
508 if (!edge_sd_get(s_ctx.l2, digest, e, s_ctx.sd_buf,
sizeof(s_ctx.sd_buf)) || strcmp(e->key, canon) != 0)
510 edge_store_free_entry(&s_ctx.store, e);
515 e->date_epoch = e->expires_epoch = -1;
518 e->last_used_ms = now;
519 s_ctx.store.stats.l2_promotes++;
528 if (!s_ctx.registered || !s_ctx.server || slot >=
MAX_CONNS)
530 bool is_get = strcmp(req->
method,
"GET") == 0;
531 bool is_head = strcmp(req->
method,
"HEAD") == 0;
532 if (!is_get && !is_head)
536 EdgeRouteMap *m = map_match(req->
path);
544 if (edge_key_canon(
"GET", host, req->
path, req->
query,
true, canon,
sizeof(canon)) == 0)
547#if DETWS_ENABLE_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';
556 EdgeEntry *e = edge_store_find(&s_ctx.store, canon, req_lookup, req, now);
557 if (e && edge_entry_fresh(e, now))
559 s_ctx.store.stats.hits++;
560 serve_hit(slot, e, now,
"HIT");
565 e = try_promote_l2(canon, now);
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))
575bool edge_cache_poll(uint8_t slot)
577 if (slot >=
MAX_CONNS || !s_ctx.pending[slot].active)
579 uint8_t fi = s_ctx.pending[slot].fetch_idx;
580 EdgeFetchSlot *fs = &s_ctx.fetches[fi];
581 const EdgeFetchTransport *tport = fs->transport;
584 if (!det_conn_active(slot))
586 edge_fetch_end(&fs->f, tport);
588 s_ctx.pending[slot].active =
false;
592 EdgeFetchStatus st = edge_fetch_pump(&fs->f, tport, now);
593 if (st == EdgeFetchStatus::PENDING)
596 if (st == EdgeFetchStatus::DONE)
598 on_fetch_done(slot, fs, now);
600 else if (st == EdgeFetchStatus::FAILED && fs->revalidate && fs->reval_entry)
602 serve_hit(slot, fs->reval_entry, now,
"STALE");
606 s_ctx.server->send(slot, 502, DET_MIME_TEXT_PLAIN,
"Bad Gateway");
608 edge_fetch_end(&fs->f, tport);
610 s_ctx.pending[slot].active =
false;
619 s_ctx.server = &server;
620 edge_store_init(&s_ctx.store);
623 s_ctx.fetches[i].used =
false;
624 s_ctx.fetches[i].f.cid = -1;
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';
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;
649 s_ctx.tls_peer_closed =
false;
652 s_ctx.store.on_evict = s_ctx.l2 ? edge_on_evict :
nullptr;
653 s_ctx.store.evict_ctx =
nullptr;
655 if (!s_ctx.registered)
657 server.
use(edge_cache_mw);
658 detws_http_set_edge_poll(edge_cache_poll);
659 s_ctx.registered =
true;
664void det_edge_cache_bind_sd(DetwsDbm *dbm)
667 s_ctx.store.on_evict = dbm ? edge_on_evict :
nullptr;
668 s_ctx.store.evict_ctx =
nullptr;
672bool det_edge_cache_map(
const char *path_prefix,
const char *origin_base_url)
674 if (!path_prefix || !origin_base_url)
676 if (strlen(path_prefix) >=
sizeof(s_ctx.maps[0].prefix))
681 char ignore_path[256];
682 if (!http_client_parse_url(origin_base_url, &https, host,
sizeof(host), &port, ignore_path,
sizeof(ignore_path)))
684#if !DETWS_ENABLE_EDGE_ORIGIN_TLS
690 if (s_ctx.maps[i].used)
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;
704#if DETWS_ENABLE_EDGE_ORIGIN_TLS
705void det_edge_cache_set_origin_ca(
const uint8_t *ca_pem,
size_t len)
707 det_tls_client_set_ca(ca_pem, len);
709void det_edge_cache_set_origin_pin(
const uint8_t sha256[32])
711 det_tls_client_set_pin(sha256);
715void det_edge_cache_reset(
void)
717 edge_store_init(&s_ctx.store);
721 edge_sd_purge_all(s_ctx.l2);
722 s_ctx.store.on_evict = edge_on_evict;
726 s_ctx.maps[i].used =
false;
729bool det_edge_cache_purge(
const char *canonical_key)
733 bool purged = edge_store_purge(&s_ctx.store, canonical_key) > 0;
738 edge_key_digest(canonical_key, strlen(canonical_key), digest);
739 if (edge_sd_del(s_ctx.l2, digest))
746uint32_t det_edge_cache_purge_prefix(
const char *path_prefix)
750 uint32_t n = edge_store_purge_prefix(&s_ctx.store, path_prefix);
753 n += edge_sd_purge_prefix(s_ctx.l2, path_prefix, s_ctx.sd_buf,
sizeof(s_ctx.sd_buf));
758void det_edge_cache_stats(EdgeCacheStats *out)
761 *out = s_ctx.store.stats;
#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.
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.
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.
size_t det_client_read(int, uint8_t *, size_t)
Drain up to cap buffered wire bytes into buf; returns the count.
void det_client_close(int)
Tear down the connection (marshaled) and return the slot to the pool.
bool det_client_send(int, const void *, size_t)
Queue len wire bytes for transmission (marshaled tcp_write + output).
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).
void dwsdelay(uint32_t ms)
Block for at least ms milliseconds - the library's single delay primitive.
Layer 7 (Application) - public HTTP routing API.
MwResult
Outcome of a middleware function (see Middleware).
@ 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).