code wiki / (root) / nx_http_resolve_redirect.nx

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}