nx_http_header_find.nx source
↩ module page · 180 lines · 6510 B
1// nx_http_header_find.nx -- case-insensitive HTTP header search.
2//
3// Per RFC 7230 §3.2: header field names are case-insensitive. This
4// primitive scans a raw response header block + finds a named header
5// + returns its value start offset + length via out params.
6//
7// Used by:
8// - ACME (RFC 8555) Replay-Nonce extraction
9// - HTTP/HTTPS clients reading Location / Content-Type / etc.
10// - Audit-log header parsing
11// - Substrate browser cookie + content-disposition extraction
12//
13// Header block format (RFC 7230 §3):
14// <status-line> CRLF
15// <name>: <value> CRLF
16// <name>: <value> CRLF
17// ...
18// CRLF
19//
20// (We accept LF-only too for tolerance per RFC 7230 §3.5.)
21//
22// Per cardinal feedback-defensive-at-boundaries-trusting-internally:
23// search is bounded by the caller-supplied header_block_len; never
24// walks past.
25//
26// nx_capability_claims:
27// needs: [sealed_enum, byte_ops]
28// provides: [http_header_case_insensitive_search]
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: L2 (transform: bytes -> offset+length)
35
36// ---- Sealed enum: header-find verdict ----------------------------
37
38const NXHF_FOUND: i64 = 0
39const NXHF_NOT_FOUND: i64 = 1
40const NXHF_MALFORMED: i64 = 2
41const NXHF_BAD_ARG: i64 = 3
42const NXHF_VERDICT_N: i64 = 4
43
44func nxhf_verdict_is_valid(v: i64) -> i64 {
45 if v < 0 { return 0 }
46 if v >= NXHF_VERDICT_N { return 0 }
47 return 1
48}
49
50func nxhf_verdict_name(v: i64) -> *u8 {
51 if v == NXHF_FOUND { return "FOUND" as *u8 }
52 if v == NXHF_NOT_FOUND { return "NOT_FOUND" as *u8 }
53 if v == NXHF_MALFORMED { return "MALFORMED" as *u8 }
54 if v == NXHF_BAD_ARG { return "BAD_ARG" as *u8 }
55 return "INVALID" as *u8
56}
57
58// ---- ASCII case-folding ------------------------------------------
59
60// Lowercase an ASCII byte (A-Z -> a-z). Non-letter bytes pass through.
61func nxhf_tolower(b: i64) -> i64 {
62 if b >= 0x41 && b <= 0x5a { return b + 0x20 }
63 return b
64}
65
66// Compare two byte sequences case-insensitively (ASCII). Returns 1
67// on match, 0 on mismatch. Both must be exactly n bytes.
68func nxhf_eq_ci(a: *u8, b: *u8, n: i64) -> i64 {
69 var i: i64 = 0
70 while i < n {
71 let ca: i64 = nxhf_tolower(a[i] as i64)
72 let cb: i64 = nxhf_tolower(b[i] as i64)
73 if ca != cb { return 0 }
74 i = i + 1
75 }
76 return 1
77}
78
79// ---- Line iteration ----------------------------------------------
80
81// Return offset of the first byte after the next CRLF (or LF) at or
82// after `off`, or `len` if no line-ending found.
83func nxhf_next_line(buf: *u8, len: i64, off: i64) -> i64 {
84 var p: i64 = off
85 while p < len {
86 if buf[p] == 0x0a { return p + 1 } // LF
87 p = p + 1
88 }
89 return len
90}
91
92// ---- Entry point -------------------------------------------------
93//
94// Search the header block for `name` (case-insensitive). Returns
95// NXHF_FOUND with out_val_off + out_val_len set to the value
96// substring (without leading SP/HTAB), or NXHF_NOT_FOUND.
97//
98// Skips the first line (the status line "HTTP/1.1 200 OK").
99//
100// Header value:
101// - leading whitespace (SP / HTAB) is trimmed
102// - trailing whitespace before CRLF is trimmed
103// - inner whitespace preserved
104// - CRLF or LF line ending is NOT included in the value length
105
106func nx_http_header_find(
107 headers: *u8, headers_n: i64,
108 name: *u8, name_n: i64,
109 out_val_off: *i64, out_val_len: *i64) -> i64 {
110 if headers == (0 as *u8) { return NXHF_BAD_ARG }
111 if name == (0 as *u8) { return NXHF_BAD_ARG }
112 if out_val_off == (0 as *i64) { return NXHF_BAD_ARG }
113 if out_val_len == (0 as *i64) { return NXHF_BAD_ARG }
114 if headers_n <= 0 { return NXHF_BAD_ARG }
115 if name_n <= 0 { return NXHF_BAD_ARG }
116
117 // Skip status line (first line).
118 var line_start: i64 = nxhf_next_line(headers, headers_n, 0)
119
120 while line_start < headers_n {
121 let line_end: i64 = nxhf_next_line(headers, headers_n, line_start)
122 // The line bytes are [line_start, line_end-1) -- line_end points
123 // PAST the LF. Compute the content-only end (excluding CRLF/LF).
124 var content_end: i64 = line_end - 1 // past the LF
125 if content_end > line_start && headers[content_end - 1] == 0x0d as u8 {
126 content_end = content_end - 1 // also strip CR
127 }
128
129 // Empty line = end of headers.
130 if content_end == line_start { return NXHF_NOT_FOUND }
131
132 // Find the colon.
133 var colon: i64 = -1
134 var p: i64 = line_start
135 while p < content_end {
136 if headers[p] == 0x3a as u8 { colon = p; p = content_end } else {
137 p = p + 1
138 }
139 }
140 if colon < 0 {
141 // Malformed header line (no colon). Per RFC 7230 §3.2,
142 // we could be strict; substrate is lenient + skips.
143 line_start = line_end
144 } else {
145 let nm_len: i64 = colon - line_start
146 if nm_len == name_n {
147 let nm_ptr: *u8 = ((headers as i64) + line_start) as *u8
148 if nxhf_eq_ci(nm_ptr, name, nm_len) == 1 {
149 // Found. Trim leading SP/HTAB after colon.
150 var v_start: i64 = colon + 1
151 var done_l: i64 = 0
152 while done_l == 0 && v_start < content_end {
153 let bL: i64 = headers[v_start] as i64
154 if bL == 0x20 || bL == 0x09 {
155 v_start = v_start + 1
156 } else {
157 done_l = 1
158 }
159 }
160 // Trim trailing SP/HTAB before content_end.
161 var v_end: i64 = content_end
162 var done_t: i64 = 0
163 while done_t == 0 && v_end > v_start {
164 let bT: i64 = headers[v_end - 1] as i64
165 if bT == 0x20 || bT == 0x09 {
166 v_end = v_end - 1
167 } else {
168 done_t = 1
169 }
170 }
171 *out_val_off = v_start
172 *out_val_len = v_end - v_start
173 return NXHF_FOUND
174 }
175 }
176 line_start = line_end
177 }
178 }
179 return NXHF_NOT_FOUND
180}