ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
edge_fetch.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_fetch.cpp
6 * @brief CDN edge-cache tier - async origin-fetch engine. See edge_fetch.h.
7 */
8
10
11#if PC_ENABLE_EDGE_CACHE
12
13#include "services/net/http_client/http_client.h" // http_client_parse_response
14#include "services/web/edge_cache/edge_cache.h" // edge_header_value
15#include <string.h>
16
17namespace
18{
19// Offset just past the CRLFCRLF header terminator, or 0 if the header block is not complete.
20size_t head_end(const uint8_t *b, size_t n)
21{
22 for (size_t i = 0; i + 3 < n; i++)
23 {
24 if (b[i] == '\r' && b[i + 1] == '\n' && b[i + 2] == '\r' && b[i + 3] == '\n')
25 {
26 return i + 4;
27 }
28 }
29 return 0;
30}
31
32bool hex_val(uint8_t c, int *v)
33{
34 if (c >= '0' && c <= '9')
35 {
36 *v = c - '0';
37 return true;
38 }
39 c |= 0x20;
40 if (c >= 'a' && c <= 'f')
41 {
42 *v = c - 'a' + 10;
43 return true;
44 }
45 return false;
46}
47
48// After the final zero-length chunk's size line (offset j), true once the trailer section reaches the
49// empty line (CRLF) that ends the message; false while the buffer is still short.
50bool chunked_trailer_complete(const uint8_t *b, size_t n, size_t j)
51{
52 for (;;)
53 {
54 if (j + 1 < n && b[j] == '\r' && b[j + 1] == '\n')
55 {
56 return true;
57 }
58 size_t k = j;
59 while (k < n && b[k] != '\n')
60 {
61 k++;
62 }
63 if (k >= n)
64 {
65 return false;
66 }
67 j = k + 1;
68 }
69}
70
71// True if the chunked body @p b[0..n) reaches its terminating zero-length chunk + trailer CRLF.
72bool chunked_complete(const uint8_t *b, size_t n)
73{
74 size_t i = 0;
75 for (;;)
76 {
77 size_t sz = 0;
78 size_t j = i;
79 bool any = false;
80 int v = 0;
81 while (j < n && hex_val(b[j], &v))
82 {
83 sz = sz * 16 + (size_t)v;
84 j++;
85 any = true;
86 }
87 if (!any)
88 {
89 return false;
90 }
91 while (j < n && b[j] != '\n') // skip chunk extensions to the size-line LF
92 {
93 j++;
94 }
95 if (j >= n)
96 {
97 return false;
98 }
99 j++; // past LF
100 if (sz == 0)
101 {
102 return chunked_trailer_complete(b, n, j);
103 }
104 size_t next = j + sz + 2; // chunk data + trailing CRLF
105 if (next > n)
106 {
107 return false;
108 }
109 i = next;
110 }
111}
112
113// Case-insensitive "does @p s contain 'chunked'".
114bool has_chunked(const char *s)
115{
116 char low[40];
117 size_t i = 0;
118 // The `i + 1 < sizeof(low)` bound has no false arm to reach: has_chunked has internal linkage
119 // and its one caller passes `char te[40]`, which edge_header_value NUL-terminates inside its
120 // own capacity - so s is at most 39 characters and s[i] always short-circuits first. The bound
121 // stays as the guard that keeps that true if the caller's buffer ever grows.
122 for (; s[i] && i + 1 < sizeof(low); i++) // GCOVR_EXCL_LINE - bound unreachable, see above
123 {
124 low[i] = (s[i] >= 'A' && s[i] <= 'Z') ? (char)(s[i] + 32) : s[i];
125 }
126 low[i] = '\0';
127 return strstr(low, "chunked") != nullptr;
128}
129} // namespace
130
131bool edge_resp_complete(const uint8_t *buf, size_t len, bool conn_closed, size_t *head_len)
132{
133 size_t h = head_end(buf, len);
134 *head_len = h;
135 if (h == 0)
136 {
137 return conn_closed; // no whole header block yet (a closed peer ends the wait)
138 }
139 char v[24];
140 if (edge_header_value((const char *)buf, h, "Content-Length", v, sizeof(v)))
141 {
142 size_t cl = 0;
143 bool any = false;
144 for (const char *p = v; *p >= '0' && *p <= '9'; p++)
145 {
146 cl = cl * 10 + (size_t)(*p - '0');
147 any = true;
148 }
149 if (any)
150 {
151 return len >= h + cl;
152 }
153 }
154 char te[40];
155 if (edge_header_value((const char *)buf, h, "Transfer-Encoding", te, sizeof(te)) && has_chunked(te))
156 {
157 return chunked_complete(buf + h, len - h);
158 }
159 return conn_closed; // close-delimited body
160}
161
162void edge_fetch_begin(EdgeFetch *f, const EdgeFetchTransport *t, const char *host, uint16_t port, const void *request,
163 size_t req_len, uint32_t now_ms)
164{
165 memset(f, 0, sizeof(*f));
166 f->cid = -1;
167 f->start_ms = now_ms;
168 f->st = EdgeFetchStatus::PENDING;
169 f->cid = t->open(t->ctx, host, port, PC_EDGE_FETCH_TIMEOUT_MS);
170 if (f->cid < 0)
171 {
172 f->st = EdgeFetchStatus::FAILED;
173 return;
174 }
175 if (!t->send(t->ctx, f->cid, request, req_len))
176 {
177 f->st = EdgeFetchStatus::FAILED;
178 }
179}
180
181EdgeFetchStatus edge_fetch_pump(EdgeFetch *f, const EdgeFetchTransport *t, uint32_t now_ms)
182{
183 if (f->st != EdgeFetchStatus::PENDING)
184 {
185 return f->st;
186 }
187
188 while (f->got < sizeof(f->buf))
189 {
190 size_t n = t->read(t->ctx, f->cid, f->buf + f->got, sizeof(f->buf) - f->got);
191 if (n == 0)
192 {
193 break;
194 }
195 f->got += n;
196 }
197 bool closed = t->closed(t->ctx, f->cid);
198
199 size_t hl = 0;
200 if (edge_resp_complete(f->buf, f->got, closed, &hl))
201 {
202 size_t bo = 0;
203 size_t bl = 0;
204 int status = http_client_parse_response(f->buf, f->got, &bo, &bl);
205 if (status < 0)
206 {
207 f->st = EdgeFetchStatus::FAILED;
208 return f->st;
209 }
210 f->status = status;
211 f->head_len = hl;
212 f->body_off = bo;
213 f->body_len = bl;
214 f->st = EdgeFetchStatus::DONE;
215 return f->st;
216 }
217 if (f->got >= sizeof(f->buf)) // full but not complete -> too big to cache
218 {
219 f->st = EdgeFetchStatus::OVERSIZE;
220 return f->st;
221 }
222 if (closed) // origin closed before a complete response
223 {
224 f->st = EdgeFetchStatus::FAILED;
225 return f->st;
226 }
227 if (now_ms - f->start_ms >= PC_EDGE_FETCH_TIMEOUT_MS)
228 {
229 f->st = EdgeFetchStatus::FAILED;
230 return f->st;
231 }
232 return EdgeFetchStatus::PENDING;
233}
234
235void edge_fetch_end(EdgeFetch *f, const EdgeFetchTransport *t)
236{
237 if (f->cid >= 0)
238 {
239 t->close(t->ctx, f->cid);
240 f->cid = -1;
241 }
242}
243
244#endif // PC_ENABLE_EDGE_CACHE
CDN edge-cache tier - pure engine (PC_ENABLE_EDGE_CACHE).
CDN edge-cache tier - async origin-fetch engine (PC_ENABLE_EDGE_CACHE).
Zero-heap outbound HTTP(S) client over raw lwIP (PC_ENABLE_HTTP_CLIENT).
#define PC_EDGE_FETCH_TIMEOUT_MS