code wiki / (root) / nx_http_cache.nx

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}