nx_http_redirect.nx source
↩ module page · 290 lines · 10281 B
1// nx_http_redirect.nx -- HTTP 3xx redirect emitter.
2//
3// Five sealed-enum redirect types per RFC 7231 §6.4:
4//
5// 301 Moved Permanently -- old URL is permanently gone; UA + cache
6// both update. Method MAY change to GET.
7// 302 Found -- temporary; UA does not cache; some UAs
8// historically change POST->GET.
9// 303 See Other -- POST-then-GET pattern. UA always
10// switches to GET. Standard for after-
11// form-submit.
12// 307 Temporary Redirect -- like 302 but PRESERVES method (POST
13// stays POST). Per RFC 7231 §6.4.7.
14// 308 Permanent Redirect -- like 301 but PRESERVES method. Per
15// RFC 7538.
16//
17// Choice matrix:
18// permanent? method-preserving? -> 308
19// permanent? method-may-change? -> 301
20// temporary? method-preserving? -> 307
21// temporary? method-may-change? -> 302
22// POST-form-submit? -> 303
23//
24// Substrate's structural safety:
25// - Location value is validated: REJECTS bytes that would inject
26// CR or LF into the response header (preventing CRLF injection
27// vulnerability class).
28// - Location value should ALSO be passed through scheme allowlist
29// to prevent open-redirect attacks; that's a CALLER decision
30// (substrate doesn't impose a policy).
31//
32// Per cardinal feedback-no-third-party-trust-native-or-nothing:
33// substrate's own redirect emitter; no res.redirect() framework,
34// no nginx return-directive translation.
35//
36// nx_capability_claims:
37// needs: [sealed_enum, byte_ops]
38// provides: [http_redirect_emit, crlf_injection_prevention]
39// safety: [no_unchecked_deref, no_floating_point, no_syscall,
40// bounded_iteration, crlf_injection_rejected]
41// verdict: [sealed_enum_5_kind + sealed_enum_4_verdict]
42// license: ORIGINAL
43// kind: racing_crew_specialist
44// layer: L3 (algorithm: structured response emit)
45
46import "nx_syscalls_x86_64.nx"
47const NXR_MAGIC_1024: i64 = 1024
48
49// ---- Sealed enum: redirect kind ----------------------------------
50
51const NXR_PERMANENT_301: i64 = 0
52const NXR_FOUND_302: i64 = 1
53const NXR_SEE_OTHER_303: i64 = 2
54const NXR_TEMP_307: i64 = 3
55const NXR_PERM_308: i64 = 4
56const NXR_KIND_N: i64 = 5
57
58func nxr_kind_is_valid(k: i64) -> i64 {
59 if k < 0 { return 0 }
60 if k >= NXR_KIND_N { return 0 }
61 return 1
62}
63
64func nxr_kind_status_code(k: i64) -> i64 {
65 if k == NXR_PERMANENT_301 { return 301 }
66 if k == NXR_FOUND_302 { return 302 }
67 if k == NXR_SEE_OTHER_303 { return 303 }
68 if k == NXR_TEMP_307 { return 307 }
69 if k == NXR_PERM_308 { return 308 }
70 return 0
71}
72
73func nxr_kind_reason(k: i64) -> *u8 {
74 if k == NXR_PERMANENT_301 { return "Moved Permanently" as *u8 }
75 if k == NXR_FOUND_302 { return "Found" as *u8 }
76 if k == NXR_SEE_OTHER_303 { return "See Other" as *u8 }
77 if k == NXR_TEMP_307 { return "Temporary Redirect" as *u8 }
78 if k == NXR_PERM_308 { return "Permanent Redirect" as *u8 }
79 return "INVALID" as *u8
80}
81
82func nxr_kind_reason_len(k: i64) -> i64 {
83 if k == NXR_PERMANENT_301 { return 17 }
84 if k == NXR_FOUND_302 { return 5 }
85 if k == NXR_SEE_OTHER_303 { return 9 }
86 if k == NXR_TEMP_307 { return 18 }
87 if k == NXR_PERM_308 { return 18 }
88 return 7
89}
90
91// Predicate: is this a PERMANENT redirect (cacheable by default)?
92// Used to emit Cache-Control: max-age on 301/308.
93func nxr_kind_is_permanent(k: i64) -> i64 {
94 if k == NXR_PERMANENT_301 { return 1 }
95 if k == NXR_PERM_308 { return 1 }
96 return 0
97}
98
99// ---- Sealed enum: verdict ----------------------------------------
100
101const NXR_OK: i64 = 0
102const NXR_OOM_BUFFER: i64 = 1
103const NXR_BAD_LOCATION: i64 = 2 // CRLF or other unsafe byte
104const NXR_BAD_ARG: i64 = 3
105const NXR_VERDICT_N: i64 = 4
106
107func nxr_verdict_is_valid(v: i64) -> i64 {
108 if v < 0 { return 0 }
109 if v >= NXR_VERDICT_N { return 0 }
110 return 1
111}
112
113func nxr_verdict_name(v: i64) -> *u8 {
114 if v == NXR_OK { return "OK" as *u8 }
115 if v == NXR_OOM_BUFFER { return "OOM_BUFFER" as *u8 }
116 if v == NXR_BAD_LOCATION { return "BAD_LOCATION" as *u8 }
117 if v == NXR_BAD_ARG { return "BAD_ARG" as *u8 }
118 return "INVALID" as *u8
119}
120
121// ---- Location validator ------------------------------------------
122//
123// Per RFC 7230 §3.2.4, header field values must NOT contain CR or LF
124// outside of obs-fold (which we don't support). Substrate REJECTS
125// Location values containing CR/LF (header injection prevention).
126//
127// Other bytes: NUL is rejected (terminator confusion). Otherwise
128// any byte allowed (URI percent-encoding is caller's responsibility;
129// we don't re-encode here since the caller may have already encoded).
130
131func nxr_location_is_safe(loc: *u8, loc_n: i64) -> i64 {
132 if loc == (0 as *u8) { return 0 }
133 if loc_n <= 0 { return 0 }
134 var i: i64 = 0
135 while i < loc_n {
136 let b: i64 = loc[i] as i64
137 if b == 0x0d { return 0 } // CR
138 if b == 0x0a { return 0 } // LF
139 if b == 0x00 { return 0 } // NUL
140 i = i + 1
141 }
142 return 1
143}
144
145// ---- Byte emit helpers -------------------------------------------
146
147func nxr_put(out: *u8, off: *i64, cap: i64, b: i64) -> i64 {
148 if *off >= cap { return NXR_OOM_BUFFER }
149 out[*off] = b as u8
150 *off = *off + 1
151 return NXR_OK
152}
153
154func nxr_put_cstr(out: *u8, off: *i64, cap: i64, s: *u8) -> i64 {
155 var i: i64 = 0
156 while s[i] != 0 {
157 let rc: i64 = nxr_put(out, off, cap, s[i] as i64)
158 if rc != NXR_OK { return rc }
159 i = i + 1
160 }
161 return NXR_OK
162}
163
164func nxr_put_bytes(out: *u8, off: *i64, cap: i64, src: *u8, n: i64) -> i64 {
165 var i: i64 = 0
166 while i < n {
167 let rc: i64 = nxr_put(out, off, cap, src[i] as i64)
168 if rc != NXR_OK { return rc }
169 i = i + 1
170 }
171 return NXR_OK
172}
173
174// i64 -> decimal ASCII (small ints; status codes max 3 digits).
175func nxr_put_int_3(out: *u8, off: *i64, cap: i64, v: i64) -> i64 {
176 if v < 0 || v > 999 { return NXR_BAD_ARG }
177 if v >= 100 {
178 let rc: i64 = nxr_put(out, off, cap, 0x30 + (v / 100))
179 if rc != NXR_OK { return rc }
180 }
181 if v >= 10 {
182 let rc: i64 = nxr_put(out, off, cap, 0x30 + ((v / 10) - (v / 100) * 10))
183 if rc != NXR_OK { return rc }
184 }
185 return nxr_put(out, off, cap, 0x30 + (v - (v / 10) * 10))
186}
187
188// ---- Top-level redirect emitter ----------------------------------
189//
190// Output shape:
191//
192// HTTP/1.1 <code> <reason>\r\n
193// Location: <validated_loc>\r\n
194// Cache-Control: <directive>\r\n (only for 301 / 308)
195// Content-Length: 0\r\n
196// \r\n
197
198func nx_http_emit_redirect(
199 out: *u8, off: *i64, cap: i64,
200 kind: i64,
201 location: *u8, location_n: i64) -> i64 {
202 if out == (0 as *u8) { return NXR_BAD_ARG }
203 if off == (0 as *i64) { return NXR_BAD_ARG }
204 if cap <= 0 { return NXR_BAD_ARG }
205 if nxr_kind_is_valid(kind) != 1 { return NXR_BAD_ARG }
206 if nxr_location_is_safe(location, location_n) != 1 { return NXR_BAD_LOCATION }
207
208 // Status line.
209 let r1: i64 = nxr_put_cstr(out, off, cap, "HTTP/1.1 " as *u8)
210 if r1 != NXR_OK { return r1 }
211 let r2: i64 = nxr_put_int_3(out, off, cap, nxr_kind_status_code(kind))
212 if r2 != NXR_OK { return r2 }
213 let r3: i64 = nxr_put(out, off, cap, 0x20)
214 if r3 != NXR_OK { return r3 }
215 let r4: i64 = nxr_put_bytes(out, off, cap,
216 nxr_kind_reason(kind),
217 nxr_kind_reason_len(kind))
218 if r4 != NXR_OK { return r4 }
219 let r5: i64 = nxr_put_cstr(out, off, cap, "\r\n" as *u8)
220 if r5 != NXR_OK { return r5 }
221
222 // Location.
223 let r6: i64 = nxr_put_cstr(out, off, cap, "Location: " as *u8)
224 if r6 != NXR_OK { return r6 }
225 let r7: i64 = nxr_put_bytes(out, off, cap, location, location_n)
226 if r7 != NXR_OK { return r7 }
227 let r8: i64 = nxr_put_cstr(out, off, cap, "\r\n" as *u8)
228 if r8 != NXR_OK { return r8 }
229
230 // Cache-Control for permanent redirects (recommended).
231 if nxr_kind_is_permanent(kind) == 1 {
232 let r9: i64 = nxr_put_cstr(out, off, cap,
233 "Cache-Control: public, max-age=86400\r\n" as *u8)
234 if r9 != NXR_OK { return r9 }
235 }
236
237 return nxr_put_cstr(out, off, cap,
238 "Content-Length: 0\r\n\r\n" as *u8)
239}
240
241// ---- Convenience: HTTP -> HTTPS canonical redirect --------------
242//
243// Used by an HTTP daemon to redirect every plain-HTTP request to
244// the HTTPS equivalent. Standard production behavior once a TLS
245// daemon is bound. Emits 308 (permanent, method-preserving).
246//
247// Caller passes the request's Host header value + request URL path.
248// Substrate emits: Location: https://<host><path>
249
250func nx_http_emit_redirect_to_https(
251 out: *u8, off: *i64, cap: i64,
252 host: *u8, host_n: i64,
253 path: *u8, path_n: i64) -> i64 {
254 if host == (0 as *u8) { return NXR_BAD_ARG }
255 if path == (0 as *u8) { return NXR_BAD_ARG }
256 if host_n <= 0 { return NXR_BAD_ARG }
257 if path_n <= 0 { return NXR_BAD_ARG }
258 if nxr_location_is_safe(host, host_n) != 1 { return NXR_BAD_LOCATION }
259 if nxr_location_is_safe(path, path_n) != 1 { return NXR_BAD_LOCATION }
260
261 // Build Location: https://<host><path> into a scratch buf.
262 let loc_buf: *u8 = sys_mmap(NXR_MAGIC_1024)
263 var loc_off: i64 = 0
264 let pfx: *u8 = "https://" as *u8
265 var i: i64 = 0
266 while i < 8 {
267 if loc_off >= NXR_MAGIC_1024 { return NXR_OOM_BUFFER }
268 loc_buf[loc_off] = pfx[i]
269 loc_off = loc_off + 1
270 i = i + 1
271 }
272 var j: i64 = 0
273 while j < host_n {
274 if loc_off >= NXR_MAGIC_1024 { return NXR_OOM_BUFFER }
275 loc_buf[loc_off] = host[j]
276 loc_off = loc_off + 1
277 j = j + 1
278 }
279 var k: i64 = 0
280 while k < path_n {
281 if loc_off >= NXR_MAGIC_1024 { return NXR_OOM_BUFFER }
282 loc_buf[loc_off] = path[k]
283 loc_off = loc_off + 1
284 k = k + 1
285 }
286 return nx_http_emit_redirect(out, off, cap, NXR_PERM_308,
287 loc_buf, loc_off)
288}
289
290// (sys_mmap import at top of file; used by https-redirect helper.)