code wiki / (root) / nx_http_redirect.nx

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.)