nx_http_resolve_redirect.nx source
↩ module page · 132 lines · 5645 B
1// nx_http_resolve_redirect.nx -- client-side 3xx redirect Location
2// follow. Given a raw HTTP response (status line + headers block,
3// possibly trailing body) and the URL we fetched it from, this
4// primitive:
5//
6// 1. detects whether the status is a follow-worthy 3xx
7// (301/302/303/307/308 per RFC 7231 §6.4)
8// 2. extracts the Location header value case-insensitively
9// 3. resolves Location against the base URL (RFC 3986 §5.2) so
10// both absolute Location and relative Location work
11// 4. writes the resolved absolute URL to a caller-supplied buffer
12//
13// Composes:
14// - nx_http_header_find (RFC 7230 §3.2.4 case-insensitive)
15// - nx_url_resolve (RFC 3986 §5.2)
16//
17// Out of scope (the orchestrator that loops nx_https_get + this
18// primitive is `nx_https_get_following` in a follow-up arc):
19// - looping with hop limit
20// - cookie + auth header carry-forward across hops
21// - method preservation (301 vs 307 vs 308 semantics) -- caller
22// decides via the returned `permanent` + `method_preserving`
23// verdict bits
24//
25// nx_safety_envelope:
26// intended_use: "HTTP redirect follow for the bits-up browser."
27// sil_target: SIL1
28// evidence: [rfc7231_section_6_4_canonical_basis,
29// composes_url_resolver,
30// scheme_allowlist_caller_responsibility]
31// hazard_register: [bug-tape-open-redirect-to-evil-site,
32// bug-tape-redirect-to-javascript-url,
33// bug-tape-redirect-loop-uncapped]
34// residual_risk: "Caller MUST cap redirect count + audit
35// scheme of resolved URL (substrate emits
36// the resolved URL verbatim)."
37// verdict: NOT_YET_EVALUATED
38
39import "nx_syscalls.nx"
40import "nx_http_header_find.nx"
41import "nx_url_resolve.nx"
42
43// Return values for nx_http_resolve_redirect.
44const NX_REDIRECT_RESOLVED: i64 = 0 // out buffer holds an absolute URL
45const NX_REDIRECT_NOT_3XX: i64 = 1 // status is not a follow-worthy 3xx
46const NX_REDIRECT_NO_LOCATION: i64 = 2 // 3xx but no Location header
47const NX_REDIRECT_BAD_LOCATION: i64 = 3 // Location contained CR/LF/NUL
48const NX_REDIRECT_BAD_BASE: i64 = 4 // base URL had no scheme
49const NX_REDIRECT_TRUNC: i64 = 5 // out buffer too small
50
51// Status-code predicates (RFC 7231 §6.4).
52// Returns 1 if status is one of the follow-worthy 3xx codes the
53// substrate handles.
54func nx_redirect_status_is_followable(status: i64) -> i64 {
55 if status == 301 { return 1 }
56 if status == 302 { return 1 }
57 if status == 303 { return 1 }
58 if status == 307 { return 1 }
59 if status == 308 { return 1 }
60 return 0
61}
62
63// Returns 1 if redirect is permanent (304/308); caller may update
64// its bookmark/cache to the new URL.
65func nx_redirect_status_is_permanent(status: i64) -> i64 {
66 if status == 301 { return 1 }
67 if status == 308 { return 1 }
68 return 0
69}
70
71// Returns 1 if redirect preserves method (307/308); GET stays GET,
72// POST stays POST. Other codes MAY change POST -> GET.
73func nx_redirect_status_preserves_method(status: i64) -> i64 {
74 if status == 307 { return 1 }
75 if status == 308 { return 1 }
76 return 0
77}
78
79// Validate a Location value: reject CR, LF, NUL (header injection +
80// terminator confusion). Other bytes are caller's concern.
81func _redirect_location_is_safe(loc: *u8, loc_n: i64) -> i64 {
82 if loc_n <= 0 { return 0 }
83 var i: i64 = 0
84 while i < loc_n {
85 let b: i64 = loc[i] as i64
86 if b == 0x0d { return 0 }
87 if b == 0x0a { return 0 }
88 if b == 0x00 { return 0 }
89 i = i + 1
90 }
91 return 1
92}
93
94// Resolve a 3xx redirect. Returns NX_REDIRECT_RESOLVED with the
95// absolute next-hop URL in `out`, or a non-zero status describing
96// why we did not (or could not) resolve.
97//
98// headers = raw HTTP header block (status line + headers, NOT body)
99// headers_n = byte length of headers
100// status = parsed status code (caller already ran nx_http_response_parse)
101// base_url = the URL we just fetched (the "base" for relative Location)
102// base_url_n = byte length of base_url
103// out = caller buffer for resolved absolute URL
104// out_cap = capacity of out buffer
105// out_len_p = receives bytes written to out
106func nx_http_resolve_redirect(
107 headers: *u8, headers_n: i64,
108 status: i64,
109 base_url: *u8, base_url_n: i64,
110 out: *u8, out_cap: i64,
111 out_len_p: *i64) -> i64 {
112 out_len_p[0] = 0
113 if nx_redirect_status_is_followable(status) != 1 { return NX_REDIRECT_NOT_3XX }
114 let loc_off_p: *i64 = sys_mmap(8) as *i64
115 let loc_len_p: *i64 = sys_mmap(8) as *i64
116 let location_name: *u8 = "Location" as *u8
117 let rc: i64 = nx_http_header_find(headers, headers_n,
118 location_name, 8,
119 loc_off_p, loc_len_p)
120 if rc != NXHF_FOUND { return NX_REDIRECT_NO_LOCATION }
121 let loc_off: i64 = loc_off_p[0]
122 let loc_len: i64 = loc_len_p[0]
123 if loc_len <= 0 { return NX_REDIRECT_NO_LOCATION }
124 let loc_ptr: *u8 = headers + loc_off
125 if _redirect_location_is_safe(loc_ptr, loc_len) != 1 { return NX_REDIRECT_BAD_LOCATION }
126 let resolve_rc: i64 = nx_url_resolve(base_url, base_url_n,
127 loc_ptr, loc_len,
128 out, out_cap, out_len_p)
129 if resolve_rc == NX_URL_RESOLVE_OK { return NX_REDIRECT_RESOLVED }
130 if resolve_rc == NX_URL_RESOLVE_TRUNC { return NX_REDIRECT_TRUNC }
131 return NX_REDIRECT_BAD_BASE
132}