DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
httpcache.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 httpcache.cpp
6 * @brief Cache-Control builder / parser / freshness implementation (see httpcache.h).
7 */
8
9#include "httpcache.h"
10
11#if DETWS_ENABLE_HTTP_CACHE
12
13#include <string.h>
14
15static const size_t CC_SENT = (size_t)-1; // overflow sentinel threaded through the emitters
16
17void cache_control_init(DetwsCacheControl *cc)
18{
19 cc->cc_public = false;
20 cc->cc_private = false;
21 cc->no_store = false;
22 cc->no_cache = false;
23 cc->no_transform = false;
24 cc->must_revalidate = false;
25 cc->proxy_revalidate = false;
26 cc->must_understand = false;
27 cc->cc_immutable = false;
28 cc->only_if_cached = false;
29 cc->max_age = -1;
30 cc->s_maxage = -1;
31 cc->stale_while_revalidate = -1;
32 cc->stale_if_error = -1;
33 cc->max_stale = -1;
34 cc->min_fresh = -1;
35}
36
37// --- build -----------------------------------------------------------------
38
39static size_t cc_emit_uint(char *buf, size_t cap, size_t n, unsigned v)
40{
41 char rev[10];
42 int ri = 0;
43 if (v == 0)
44 rev[ri++] = '0';
45 else
46 while (v)
47 {
48 rev[ri++] = (char)('0' + (v % 10));
49 v /= 10;
50 }
51 if (n + (size_t)ri > cap)
52 return CC_SENT;
53 for (int k = 0; k < ri; k++)
54 buf[n + k] = rev[ri - 1 - k];
55 return n + (size_t)ri;
56}
57
58// Emit one bare token (with the ", " separator before all but the first).
59static size_t cc_tok(char *buf, size_t cap, size_t n, bool *first, const char *tok)
60{
61 if (n == CC_SENT)
62 return CC_SENT;
63 size_t tlen = strnlen(tok, cap);
64 size_t need = (*first ? 0 : 2) + tlen;
65 if (n + need > cap)
66 return CC_SENT;
67 if (!*first)
68 {
69 buf[n++] = ',';
70 buf[n++] = ' ';
71 }
72 memcpy(buf + n, tok, tlen);
73 *first = false;
74 return n + tlen;
75}
76
77// Emit "key=value".
78static size_t cc_kv(char *buf, size_t cap, size_t n, bool *first, const char *key, long v)
79{
80 n = cc_tok(buf, cap, n, first, key);
81 if (n == CC_SENT || n >= cap)
82 return CC_SENT;
83 buf[n++] = '=';
84 return cc_emit_uint(buf, cap, n, (unsigned)v);
85}
86
87size_t cache_control_build(char *buf, size_t cap, const DetwsCacheControl *cc)
88{
89 if (!buf || !cc || cap == 0)
90 return 0;
91 size_t n = 0;
92 bool first = true;
93
94 if (cc->cc_public)
95 n = cc_tok(buf, cap, n, &first, "public");
96 if (cc->cc_private)
97 n = cc_tok(buf, cap, n, &first, "private");
98 if (cc->no_store)
99 n = cc_tok(buf, cap, n, &first, "no-store");
100 if (cc->no_cache)
101 n = cc_tok(buf, cap, n, &first, "no-cache");
102 if (cc->max_age >= 0)
103 n = cc_kv(buf, cap, n, &first, "max-age", cc->max_age);
104 if (cc->s_maxage >= 0)
105 n = cc_kv(buf, cap, n, &first, "s-maxage", cc->s_maxage);
106 if (cc->must_revalidate)
107 n = cc_tok(buf, cap, n, &first, "must-revalidate");
108 if (cc->proxy_revalidate)
109 n = cc_tok(buf, cap, n, &first, "proxy-revalidate");
110 if (cc->no_transform)
111 n = cc_tok(buf, cap, n, &first, "no-transform");
112 if (cc->must_understand)
113 n = cc_tok(buf, cap, n, &first, "must-understand");
114 if (cc->cc_immutable)
115 n = cc_tok(buf, cap, n, &first, "immutable");
116 if (cc->stale_while_revalidate >= 0)
117 n = cc_kv(buf, cap, n, &first, "stale-while-revalidate", cc->stale_while_revalidate);
118 if (cc->stale_if_error >= 0)
119 n = cc_kv(buf, cap, n, &first, "stale-if-error", cc->stale_if_error);
120 if (cc->only_if_cached)
121 n = cc_tok(buf, cap, n, &first, "only-if-cached");
122 if (cc->max_stale == -2)
123 n = cc_tok(buf, cap, n, &first, "max-stale");
124 else if (cc->max_stale >= 0)
125 n = cc_kv(buf, cap, n, &first, "max-stale", cc->max_stale);
126 if (cc->min_fresh >= 0)
127 n = cc_kv(buf, cap, n, &first, "min-fresh", cc->min_fresh);
128
129 if (n == CC_SENT || first || n + 1 > cap)
130 return 0; // overflow, or nothing was emitted, or no room for the NUL
131 buf[n] = 0;
132 return n;
133}
134
135// --- parse -----------------------------------------------------------------
136
137// Case-insensitive compare of [s,s+len) to the (lowercase) NUL-terminated @p target.
138static bool cc_ci_eq(const char *s, size_t len, const char *target)
139{
140 size_t i = 0;
141 for (; i < len && target[i]; i++)
142 {
143 char c = s[i];
144 if (c >= 'A' && c <= 'Z')
145 c = (char)(c + 32);
146 if (c != target[i])
147 return false;
148 }
149 return i == len && target[i] == 0;
150}
151
152// Parse a non-negative delta-seconds from [v,v+vlen) (tolerates surrounding quotes / spaces).
153// Returns the value clamped to INT32_MAX, or -1 if no digits are present.
154static int32_t cc_parse_delta(const char *v, size_t vlen)
155{
156 if (!v)
157 return -1;
158 size_t i = 0;
159 while (i < vlen && (v[i] == ' ' || v[i] == '\t' || v[i] == '"'))
160 i++;
161 long val = -1;
162 bool any = false;
163 while (i < vlen && v[i] >= '0' && v[i] <= '9')
164 {
165 if (!any)
166 {
167 val = 0;
168 any = true;
169 }
170 val = val * 10 + (v[i] - '0');
171 if (val > 2147483647L)
172 val = 2147483647L;
173 i++;
174 }
175 return any ? (int32_t)val : -1;
176}
177
178static bool cc_match(DetwsCacheControl *cc, const char *name, size_t nlen, const char *val, size_t vlen)
179{
180 if (cc_ci_eq(name, nlen, "public"))
181 cc->cc_public = true;
182 else if (cc_ci_eq(name, nlen, "private"))
183 cc->cc_private = true;
184 else if (cc_ci_eq(name, nlen, "no-store"))
185 cc->no_store = true;
186 else if (cc_ci_eq(name, nlen, "no-cache"))
187 cc->no_cache = true;
188 else if (cc_ci_eq(name, nlen, "no-transform"))
189 cc->no_transform = true;
190 else if (cc_ci_eq(name, nlen, "must-revalidate"))
191 cc->must_revalidate = true;
192 else if (cc_ci_eq(name, nlen, "proxy-revalidate"))
193 cc->proxy_revalidate = true;
194 else if (cc_ci_eq(name, nlen, "must-understand"))
195 cc->must_understand = true;
196 else if (cc_ci_eq(name, nlen, "immutable"))
197 cc->cc_immutable = true;
198 else if (cc_ci_eq(name, nlen, "only-if-cached"))
199 cc->only_if_cached = true;
200 else if (cc_ci_eq(name, nlen, "max-age"))
201 cc->max_age = cc_parse_delta(val, vlen);
202 else if (cc_ci_eq(name, nlen, "s-maxage"))
203 cc->s_maxage = cc_parse_delta(val, vlen);
204 else if (cc_ci_eq(name, nlen, "stale-while-revalidate"))
205 cc->stale_while_revalidate = cc_parse_delta(val, vlen);
206 else if (cc_ci_eq(name, nlen, "stale-if-error"))
207 cc->stale_if_error = cc_parse_delta(val, vlen);
208 else if (cc_ci_eq(name, nlen, "max-stale"))
209 cc->max_stale = val ? cc_parse_delta(val, vlen) : -2; // present with no value = "any"
210 else if (cc_ci_eq(name, nlen, "min-fresh"))
211 cc->min_fresh = cc_parse_delta(val, vlen);
212 else
213 return false; // unknown directive - ignored
214 return true;
215}
216
217// Parse one comma-separated directive starting at *i (advancing past it) and apply it; returns true
218// if a known directive matched.
219static bool cache_parse_one_directive(const char *s, size_t len, size_t *i, DetwsCacheControl *cc)
220{
221 while (*i < len && (s[*i] == ',' || s[*i] == ' ' || s[*i] == '\t'))
222 (*i)++; // skip separators / OWS
223 if (*i >= len)
224 return false;
225 size_t start = *i;
226 while (*i < len && s[*i] != ',')
227 (*i)++; // to the next comma
228 size_t end = *i;
229 while (end > start && (s[end - 1] == ' ' || s[end - 1] == '\t'))
230 end--; // trim trailing OWS
231 size_t eq = start;
232 while (eq < end && s[eq] != '=')
233 eq++;
234 size_t nlen = (eq < end ? eq : end) - start;
235 while (nlen > 0 && (s[start + nlen - 1] == ' ' || s[start + nlen - 1] == '\t'))
236 nlen--; // trim trailing OWS from name
237 const char *val = (eq < end) ? s + eq + 1 : nullptr;
238 size_t vlen = (eq < end) ? end - (eq + 1) : 0;
239 return nlen && cc_match(cc, s + start, nlen, val, vlen);
240}
241
242bool cache_control_parse(const char *s, size_t len, DetwsCacheControl *cc)
243{
244 cache_control_init(cc);
245 if (!s)
246 return false;
247 bool found = false;
248 size_t i = 0;
249 while (i < len)
250 {
251 if (cache_parse_one_directive(s, len, &i, cc))
252 found = true;
253 }
254 return found;
255}
256
257// --- presets + freshness ---------------------------------------------------
258
259void cache_immutable_asset(DetwsCacheControl *cc, uint32_t max_age)
260{
261 cache_control_init(cc);
262 cc->cc_public = true;
263 cc->max_age = (int32_t)(max_age > 2147483647u ? 2147483647u : max_age);
264 cc->cc_immutable = true;
265}
266
267void cache_revalidatable(DetwsCacheControl *cc, uint32_t max_age, int32_t stale_while_revalidate)
268{
269 cache_control_init(cc);
270 cc->cc_public = true;
271 cc->max_age = (int32_t)(max_age > 2147483647u ? 2147483647u : max_age);
272 if (stale_while_revalidate >= 0)
273 cc->stale_while_revalidate = stale_while_revalidate;
274}
275
276void cache_no_store(DetwsCacheControl *cc)
277{
278 cache_control_init(cc);
279 cc->no_store = true;
280}
281
282void cache_shared(DetwsCacheControl *cc, uint32_t max_age, uint32_t s_maxage)
283{
284 cache_control_init(cc);
285 cc->cc_public = true;
286 cc->max_age = (int32_t)(max_age > 2147483647u ? 2147483647u : max_age);
287 cc->s_maxage = (int32_t)(s_maxage > 2147483647u ? 2147483647u : s_maxage);
288}
289
290long cache_freshness_lifetime(const DetwsCacheControl *cc, bool shared, long expires_minus_date)
291{
292 if (shared && cc->s_maxage >= 0)
293 return cc->s_maxage;
294 if (cc->max_age >= 0)
295 return cc->max_age;
296 if (expires_minus_date >= 0)
297 return expires_minus_date;
298 return -1; // no explicit expiration - the caller applies a heuristic
299}
300
301#endif // DETWS_ENABLE_HTTP_CACHE
HTTP Cache-Control directive builder + parser + freshness helper (RFC 9111), DETWS_ENABLE_HTTP_CACHE.