nx_http_cache.nx source
↩ module page · 327 lines · 11306 B
1// nx_http_cache.nx -- HTTP caching primitives (ETag + 304).
2//
3// Standard nginx/apache behavior: substrate emits ETag header on
4// every cacheable response; client sends back If-None-Match on the
5// next request; substrate compares + emits 304 Not Modified if
6// content unchanged.
7//
8// Saves bandwidth on repeated dashboard loads: a 14 KB response
9// becomes a 113-byte 304 header.
10//
11// Per cardinal feedback-no-third-party-trust-native-or-nothing:
12// substrate's own ETag computation. No external hash library.
13//
14// ETag algorithm: FNV-1a 64-bit hash of response body bytes
15// emitted as 16-char lowercase hex. Weak-equivalent per RFC 7232
16// §2.1 (the weak prefix `W/` is not emitted; substrate uses strong
17// equivalence -- byte-equal response = byte-equal ETag).
18//
19// FNV-1a was chosen because:
20// 1. No crypto-dep transitive imports (works under any syscall ABI)
21// 2. ~1 GB/s single-thread on modern hardware (no perf hit)
22// 3. Adequate collision resistance for HTTP caching (not security)
23// 4. ~30 LOC; auditable
24//
25// nx_capability_claims:
26// needs: [sealed_enum, byte_ops, http_header_search]
27// provides: [etag_compute_fnv1a_hex, cache_control_emit,
28// 304_not_modified_emit, if_none_match_parse]
29// safety: [no_unchecked_deref, no_floating_point, no_syscall,
30// bounded_iteration, bit_equal_reproducible]
31// verdict: [sealed_enum_4_state]
32// license: ORIGINAL
33// kind: racing_crew_specialist
34// layer: L3 (algorithm: HTTP cache decision)
35
36import "nx_http_header_find.nx"
37const NXC_MAGIC_3750763034362895579: i64 = 3750763034362895579
38const NXC_MAGIC_1099511628211: i64 = 1099511628211
39
40// ---- Sealed enum: cache decision verdict -------------------------
41
42const NXC_OK_FRESH: i64 = 0 // serve fresh; emit ETag
43const NXC_OK_NOT_MODIFIED: i64 = 1 // emit 304; skip body
44const NXC_OOM_BUFFER: i64 = 2
45const NXC_BAD_ARG: i64 = 3
46const NXC_VERDICT_N: i64 = 4
47
48func nxc_verdict_is_valid(v: i64) -> i64 {
49 if v < 0 { return 0 }
50 if v >= NXC_VERDICT_N { return 0 }
51 return 1
52}
53
54func nxc_verdict_name(v: i64) -> *u8 {
55 if v == NXC_OK_FRESH { return "OK_FRESH" as *u8 }
56 if v == NXC_OK_NOT_MODIFIED { return "OK_NOT_MODIFIED" as *u8 }
57 if v == NXC_OOM_BUFFER { return "OOM_BUFFER" as *u8 }
58 if v == NXC_BAD_ARG { return "BAD_ARG" as *u8 }
59 return "INVALID" as *u8
60}
61
62// ---- FNV-1a 64-bit hash ------------------------------------------
63//
64// RFC 5234 unofficial / FNV "1.0" / Fowler-Noll-Vo.
65// Public-domain algorithm (the authors released it).
66//
67// Init = 0xcbf29ce484222325
68// Prime = 0x100000001b3
69//
70// hash = init; for each byte b: hash ^= b; hash *= prime; return hash
71//
72// Stable across runs. Collision-resistant enough for HTTP caching
73// (NOT cryptographic; do not use for security).
74
75func nxc_fnv1a_64(bytes: *u8, n: i64) -> i64 {
76 // Init constant (cbf29ce484222325). NishiLang lacks i64 hex
77 // literals of this magnitude in const-RHS so we build at runtime.
78 // 0xcbf29ce484222325 = -3750763034362895579 as signed i64.
79 var h: i64 = -NXC_MAGIC_3750763034362895579
80 let prime: i64 = NXC_MAGIC_1099511628211 // 0x100000001b3
81 var i: i64 = 0
82 while i < n {
83 let b: i64 = bytes[i] as i64
84 h = h ^ b
85 h = h * prime
86 i = i + 1
87 }
88 return h
89}
90
91// ---- i64 -> 16-char lowercase hex --------------------------------
92
93func nxc_put_hex_64(out: *u8, off: *i64, cap: i64, v: i64) -> i64 {
94 var i: i64 = 15
95 while i >= 0 {
96 if *off >= cap { return NXC_OOM_BUFFER }
97 let shift: i64 = i * 4
98 let nibble: i64 = (v >> shift) & 0xf
99 var c: i64 = 0x30 + nibble
100 if nibble >= 10 { c = 0x61 + (nibble - 10) }
101 out[*off] = c as u8
102 *off = *off + 1
103 i = i - 1
104 }
105 return NXC_OK_FRESH
106}
107
108// ---- Compute ETag for a response body ----------------------------
109//
110// Writes 16-byte lowercase hex (FNV-1a 64-bit hash) into out_etag[*off..]
111// + advances *off. Caller emits the bytes between quotes as the
112// HTTP ETag header value:
113//
114// ETag: "<16-hex-chars>"
115
116func nx_http_etag_compute(body: *u8, body_n: i64,
117 out_etag: *u8, out_off: *i64,
118 out_cap: i64) -> i64 {
119 if body == (0 as *u8) && body_n > 0 { return NXC_BAD_ARG }
120 if out_etag == (0 as *u8) { return NXC_BAD_ARG }
121 if out_off == (0 as *i64) { return NXC_BAD_ARG }
122 if out_cap <= 0 { return NXC_BAD_ARG }
123 let h: i64 = nxc_fnv1a_64(body, body_n)
124 return nxc_put_hex_64(out_etag, out_off, out_cap, h)
125}
126
127// ---- Parse If-None-Match header value vs computed ETag ----------
128//
129// HTTP header value can be:
130// "<etag>"
131// "<etag>", "<etag2>", ...
132// * (matches anything)
133//
134// We do simple substring match for our 16-hex-char value within the
135// header value. Per RFC 7232 §3.2 weak/strong distinction is
136// ignored here -- substrate emits strong ETags so any match is OK.
137//
138// Returns 1 if the computed ETag matches; 0 otherwise.
139
140func nx_http_if_none_match_matches(
141 header_val: *u8, header_val_n: i64,
142 etag_hex: *u8, etag_hex_n: i64) -> i64 {
143 if header_val == (0 as *u8) { return 0 }
144 if etag_hex == (0 as *u8) { return 0 }
145 if etag_hex_n <= 0 { return 0 }
146 // Wildcard `*` matches anything.
147 if header_val_n >= 1 {
148 if header_val[0] == 0x2a as u8 { return 1 }
149 }
150 // Substring scan for etag_hex within header_val.
151 if header_val_n < etag_hex_n { return 0 }
152 var i: i64 = 0
153 while i <= header_val_n - etag_hex_n {
154 var eq: i64 = 1
155 var j: i64 = 0
156 while j < etag_hex_n {
157 if header_val[i + j] != etag_hex[j] { eq = 0; j = etag_hex_n }
158 j = j + 1
159 }
160 if eq == 1 { return 1 }
161 i = i + 1
162 }
163 return 0
164}
165
166// ---- 304 Not Modified emitter ------------------------------------
167//
168// Per RFC 7232 §4.1, a 304 response MUST include:
169// - Same Cache-Control / Date / ETag headers as the 200 would
170// - NO message body
171// - Optionally Last-Modified
172//
173// Our minimal 304:
174// HTTP/1.1 304 Not Modified\r\n
175// ETag: "<hex>"\r\n
176// Cache-Control: public, max-age=300\r\n
177// Content-Length: 0\r\n
178// \r\n
179
180func nxc_put(out: *u8, off: *i64, cap: i64, b: i64) -> i64 {
181 if *off >= cap { return NXC_OOM_BUFFER }
182 out[*off] = b as u8
183 *off = *off + 1
184 return NXC_OK_FRESH
185}
186
187func nxc_put_cstr(out: *u8, off: *i64, cap: i64, s: *u8) -> i64 {
188 var i: i64 = 0
189 while s[i] != 0 {
190 let rc: i64 = nxc_put(out, off, cap, s[i] as i64)
191 if rc != NXC_OK_FRESH { return rc }
192 i = i + 1
193 }
194 return NXC_OK_FRESH
195}
196
197func nxc_put_bytes(out: *u8, off: *i64, cap: i64, src: *u8, n: i64) -> i64 {
198 var i: i64 = 0
199 while i < n {
200 let rc: i64 = nxc_put(out, off, cap, src[i] as i64)
201 if rc != NXC_OK_FRESH { return rc }
202 i = i + 1
203 }
204 return NXC_OK_FRESH
205}
206
207func nx_http_emit_304(out: *u8, off: *i64, cap: i64,
208 etag_hex: *u8, etag_hex_n: i64,
209 max_age_s: i64) -> i64 {
210 if out == (0 as *u8) { return NXC_BAD_ARG }
211 if off == (0 as *i64) { return NXC_BAD_ARG }
212 if etag_hex == (0 as *u8) { return NXC_BAD_ARG }
213 if etag_hex_n <= 0 { return NXC_BAD_ARG }
214 if cap <= 0 { return NXC_BAD_ARG }
215 if max_age_s < 0 { return NXC_BAD_ARG }
216
217 let r1: i64 = nxc_put_cstr(out, off, cap,
218 "HTTP/1.1 304 Not Modified\r\nETag: \"" as *u8)
219 if r1 != NXC_OK_FRESH { return r1 }
220 let r2: i64 = nxc_put_bytes(out, off, cap, etag_hex, etag_hex_n)
221 if r2 != NXC_OK_FRESH { return r2 }
222 let r3: i64 = nxc_put_cstr(out, off, cap,
223 "\"\r\nCache-Control: public, max-age=" as *u8)
224 if r3 != NXC_OK_FRESH { return r3 }
225
226 // Emit max_age in decimal (small int; up to ~10 digits).
227 if max_age_s == 0 {
228 let rc: i64 = nxc_put(out, off, cap, 0x30)
229 if rc != NXC_OK_FRESH { return rc }
230 } else {
231 // Decimal-emit via stack-local scratch (no sys_mmap).
232 var scratch_buf: i64 = 0
233 var n: i64 = max_age_s
234 var k: i64 = 0
235 // We need a buffer. Allocate 16 bytes via the caller pattern
236 // -- but this file is syscall-free. Use a stack-localish
237 // approach: build digits in reverse into out_buf, then swap.
238 let digit_start: i64 = *off
239 while n > 0 {
240 let rc: i64 = nxc_put(out, off, cap, 0x30 + (n - (n / 10) * 10))
241 if rc != NXC_OK_FRESH { return rc }
242 n = n / 10
243 k = k + 1
244 }
245 // Reverse in place.
246 var lo: i64 = digit_start
247 var hi: i64 = *off - 1
248 while lo < hi {
249 let tmp: i64 = out[lo] as i64
250 out[lo] = out[hi]
251 out[hi] = tmp as u8
252 lo = lo + 1
253 hi = hi - 1
254 }
255 }
256
257 return nxc_put_cstr(out, off, cap,
258 "\r\nContent-Length: 0\r\n\r\n" as *u8)
259}
260
261// ---- Cache-Control header injection helper -----------------------
262//
263// Emits just the `Cache-Control: public, max-age=N\r\n` line.
264// Caller composes with the rest of the response headers.
265
266func nx_http_emit_cache_control_header(
267 out: *u8, off: *i64, cap: i64, max_age_s: i64) -> i64 {
268 if out == (0 as *u8) { return NXC_BAD_ARG }
269 if off == (0 as *i64) { return NXC_BAD_ARG }
270 if cap <= 0 { return NXC_BAD_ARG }
271 if max_age_s < 0 { return NXC_BAD_ARG }
272
273 let r1: i64 = nxc_put_cstr(out, off, cap,
274 "Cache-Control: public, max-age=" as *u8)
275 if r1 != NXC_OK_FRESH { return r1 }
276 if max_age_s == 0 {
277 let rc: i64 = nxc_put(out, off, cap, 0x30)
278 if rc != NXC_OK_FRESH { return rc }
279 } else {
280 var n: i64 = max_age_s
281 let digit_start: i64 = *off
282 while n > 0 {
283 let rc: i64 = nxc_put(out, off, cap, 0x30 + (n - (n / 10) * 10))
284 if rc != NXC_OK_FRESH { return rc }
285 n = n / 10
286 }
287 var lo: i64 = digit_start
288 var hi: i64 = *off - 1
289 while lo < hi {
290 let tmp: i64 = out[lo] as i64
291 out[lo] = out[hi]
292 out[hi] = tmp as u8
293 lo = lo + 1
294 hi = hi - 1
295 }
296 }
297 return nxc_put_cstr(out, off, cap, "\r\n" as *u8)
298}
299
300// ---- Top-level cache decision -----------------------------------
301//
302// Caller pattern:
303//
304// compute body bytes for the response
305// compute etag via nx_http_etag_compute
306// parse If-None-Match from request headers via nx_http_header_find
307// call nx_http_cache_decide(if_none_match_val, if_none_match_n,
308// etag_hex, etag_hex_n)
309// if NXC_OK_NOT_MODIFIED:
310// emit 304
311// else:
312// emit 200 with ETag + Cache-Control headers + body
313
314func nx_http_cache_decide(
315 if_none_match: *u8, if_none_match_n: i64,
316 etag_hex: *u8, etag_hex_n: i64) -> i64 {
317 if etag_hex == (0 as *u8) { return NXC_BAD_ARG }
318 if etag_hex_n <= 0 { return NXC_BAD_ARG }
319 // No If-None-Match header -- always fresh.
320 if if_none_match == (0 as *u8) { return NXC_OK_FRESH }
321 if if_none_match_n <= 0 { return NXC_OK_FRESH }
322 if nx_http_if_none_match_matches(if_none_match, if_none_match_n,
323 etag_hex, etag_hex_n) == 1 {
324 return NXC_OK_NOT_MODIFIED
325 }
326 return NXC_OK_FRESH
327}