code wiki / (root) / nx_acme_response.nx

nx_acme_response.nx source

↩ module page · 228 lines · 7399 B

1// nx_acme_response.nx -- ACME (RFC 8555) HTTP response parser. 2// 3// Extracts the four fields the ACME state machine needs from any 4// CA response: 5// 1. HTTP status code (200, 201, 204, 400, etc.) 6// 2. Replay-Nonce header (RFC 8555 §6.5; the nonce we use for 7// the NEXT request -- single-use per RFC 8555 §6.5.1) 8// 3. Location header (account-URL / order-URL / challenge-URL 9// depending on the endpoint) 10// 4. Body offset + length (JSON payload for the caller to parse) 11// 12// Composes: 13// nx_http_header_find.nx -- case-insensitive header search 14// 15// Per cardinal feedback-defensive-at-boundaries-trusting-internally: 16// the parser validates the response shape ONCE; downstream callers 17// trust the parsed fields. 18// 19// Per RFC 8555 §6.4: ACME servers MAY include Replay-Nonce on ANY 20// response (success or error). We extract it always. If absent, 21// the caller treats the next request as needing a fresh /new-nonce 22// roundtrip. 23// 24// nx_capability_claims: 25// needs: [sealed_enum, http_header_search] 26// provides: [acme_response_parse, replay_nonce_extract, 27// location_header_extract] 28// safety: [no_unchecked_deref, no_floating_point, no_syscall, 29// bounded_iteration, bit_equal_reproducible] 30// verdict: [sealed_enum_6_state] 31// license: ORIGINAL 32// kind: racing_crew_specialist 33// layer: L3 (algorithm: HTTP response -> ACME-typed fields) 34 35import "nx_syscalls_x86_64.nx" 36import "nx_http_header_find.nx" 37 38// ---- Sealed enum: parse verdict ---------------------------------- 39 40const NXAR_OK: i64 = 0 41const NXAR_BAD_STATUS_LINE: i64 = 1 42const NXAR_NO_HEADER_END: i64 = 2 43const NXAR_BODY_TOO_SHORT: i64 = 3 44const NXAR_BAD_ARG: i64 = 4 45const NXAR_VERDICT_N: i64 = 5 46 47func nxar_verdict_is_valid(v: i64) -> i64 { 48 if v < 0 { return 0 } 49 if v >= NXAR_VERDICT_N { return 0 } 50 return 1 51} 52 53func nxar_verdict_name(v: i64) -> *u8 { 54 if v == NXAR_OK { return "OK" as *u8 } 55 if v == NXAR_BAD_STATUS_LINE { return "BAD_STATUS_LINE" as *u8 } 56 if v == NXAR_NO_HEADER_END { return "NO_HEADER_END" as *u8 } 57 if v == NXAR_BODY_TOO_SHORT { return "BODY_TOO_SHORT" as *u8 } 58 if v == NXAR_BAD_ARG { return "BAD_ARG" as *u8 } 59 return "INVALID" as *u8 60} 61 62// ---- Status-line parse ------------------------------------------- 63// 64// HTTP/1.1 200 OK\r\n 65// HTTP/1.1 201 Created\r\n 66// HTTP/1.1 204 No Content\r\n 67// HTTP/1.1 400 Bad Request\r\n 68// 69// We just want the integer. Tolerant: HTTP/1.0 or HTTP/2 also work. 70 71func nxar_parse_status_code(buf: *u8, len: i64) -> i64 { 72 if len < 12 { return -1 } // "HTTP/1.x NNN" minimum 73 // Find first space. 74 var p: i64 = 0 75 while p < len { 76 if buf[p] == 0x20 as u8 { p = p + 1; break } 77 p = p + 1 78 } 79 if p >= len { return -1 } 80 // Parse the integer. 81 var code: i64 = 0 82 var any: i64 = 0 83 while p < len { 84 let b: i64 = buf[p] as i64 85 if b >= 0x30 && b <= 0x39 { 86 code = code * 10 + (b - 0x30) 87 any = 1 88 p = p + 1 89 } else { 90 p = len + 1 // break sentinel 91 } 92 } 93 if any == 0 { return -1 } 94 if code < 100 || code > 599 { return -1 } 95 return code 96} 97 98// ---- Find header / body boundary --------------------------------- 99// 100// HTTP separates headers from body with CRLF CRLF (or LF LF tolerated). 101// Returns the offset of the FIRST byte of the body, or -1 if absent. 102 103func nxar_find_body_offset(buf: *u8, len: i64) -> i64 { 104 if len < 4 { return -1 } 105 var i: i64 = 0 106 while i + 3 < len { 107 if buf[i] == 0x0d as u8 && buf[i+1] == 0x0a as u8 108 && buf[i+2] == 0x0d as u8 && buf[i+3] == 0x0a as u8 { 109 return i + 4 110 } 111 i = i + 1 112 } 113 // LF-only tolerance. 114 var j: i64 = 0 115 while j + 1 < len { 116 if buf[j] == 0x0a as u8 && buf[j+1] == 0x0a as u8 { 117 return j + 2 118 } 119 j = j + 1 120 } 121 return -1 122} 123 124// ---- AcmeResponse struct ----------------------------------------- 125// 126// Caller-allocated. Filled by nx_acme_response_parse. Out fields 127// for absent headers are zero (so callers can check 128// nonce_len > 0 / location_len > 0). 129 130struct AcmeResponse { 131 status_code: i64, 132 nonce_off: i64, 133 nonce_len: i64, 134 location_off: i64, 135 location_len: i64, 136 body_off: i64, 137 body_len: i64, 138} 139 140const ACME_RESPONSE_BYTES: i64 = 56 // 7 i64 fields 141 142// ---- Top-level parse entry -------------------------------------- 143// 144// Caller supplies the FULL raw HTTP response bytes (headers + body). 145// We fill an AcmeResponse struct + return sealed verdict. 146 147func nx_acme_response_parse( 148 resp_buf: *u8, resp_n: i64, 149 out: *AcmeResponse) -> i64 { 150 if resp_buf == (0 as *u8) { return NXAR_BAD_ARG } 151 if out == (0 as *AcmeResponse) { return NXAR_BAD_ARG } 152 if resp_n <= 0 { return NXAR_BAD_ARG } 153 154 // Init out fields. 155 out.status_code = 0 156 out.nonce_off = 0 157 out.nonce_len = 0 158 out.location_off = 0 159 out.location_len = 0 160 out.body_off = 0 161 out.body_len = 0 162 163 // Parse status code. 164 let code: i64 = nxar_parse_status_code(resp_buf, resp_n) 165 if code < 0 { return NXAR_BAD_STATUS_LINE } 166 out.status_code = code 167 168 // Find body offset. 169 let body_off: i64 = nxar_find_body_offset(resp_buf, resp_n) 170 if body_off < 0 { return NXAR_NO_HEADER_END } 171 out.body_off = body_off 172 out.body_len = resp_n - body_off 173 174 // Header block is [0, body_off). Slice for header-find. 175 let headers_n: i64 = body_off 176 177 // Replay-Nonce (absence is OK; out fields stay 0). 178 let n_off: *i64 = sys_mmap(8) as *i64 179 let n_len: *i64 = sys_mmap(8) as *i64 180 let rcn: i64 = nx_http_header_find(resp_buf, headers_n, 181 "Replay-Nonce" as *u8, 12, 182 n_off, n_len) 183 if rcn == NXHF_FOUND { 184 out.nonce_off = n_off[0] 185 out.nonce_len = n_len[0] 186 } 187 188 // Location. 189 let l_off: *i64 = sys_mmap(8) as *i64 190 let l_len: *i64 = sys_mmap(8) as *i64 191 let rcl: i64 = nx_http_header_find(resp_buf, headers_n, 192 "Location" as *u8, 8, 193 l_off, l_len) 194 if rcl == NXHF_FOUND { 195 out.location_off = l_off[0] 196 out.location_len = l_len[0] 197 } 198 199 return NXAR_OK 200} 201 202// ---- Convenience predicates ------------------------------------- 203// 204// Mirror RFC 8555 §6.7 status-code semantics for the state machine. 205 206// 2xx success 207func nx_acme_response_is_success(out: *AcmeResponse) -> i64 { 208 if out == (0 as *AcmeResponse) { return 0 } 209 if out.status_code >= 200 && out.status_code <= 299 { return 1 } 210 return 0 211} 212 213// 4xx client error (caller likely won't retry without payload change) 214func nx_acme_response_is_client_error(out: *AcmeResponse) -> i64 { 215 if out == (0 as *AcmeResponse) { return 0 } 216 if out.status_code >= 400 && out.status_code <= 499 { return 1 } 217 return 0 218} 219 220// 5xx server error or 429 (caller retries with backoff) 221func nx_acme_response_should_retry(out: *AcmeResponse) -> i64 { 222 if out == (0 as *AcmeResponse) { return 0 } 223 if out.status_code == 429 { return 1 } 224 if out.status_code >= 500 && out.status_code <= 599 { return 1 } 225 return 0 226} 227 228// (imports at top of file per NishiLang parser)