code wiki / (root) / nx_https_get_complete.nx

nx_https_get_complete.nx source

↩ module page · 350 lines · 15393 B

1// nx_https_get_complete.nx -- step 4 of the nx_https_client 2// wiring arc. THE LAST piece before nx_https_get(url, store) 3// works end-to-end. 4// 5// Takes a connected TLS 1.3 session (state == CONNECTED, app 6// traffic keys derived in step 3c.5) + a path + a host + an out 7// buffer. Encrypts an HTTP/1.1 GET request as an application_data 8// TLS record under client_app_traffic_key + sends + reads the 9// server's encrypted response records in a loop + decrypts each 10// under server_app_traffic_key + accumulates plaintext into 11// out_buf until peer-close (close_notify alert OR TCP EOF). 12// 13// Returns total bytes accumulated into out_buf on success 14// (POSITIVE). Negative -NX_HTTPS_GC_* verdict on failure. 15// 16// Composes 4 shipped substrate primitives: 17// nx_http_client_build_request -- 4-line HTTP/1.1 GET 18// nx_tls13_record_encrypt -- AEAD wrap 19// nx_tls13_read_record_from_fd -- partial-read framer 20// nx_tls13_record_decrypt -- AEAD unwrap 21// 22// HTTP/1.1 Connection: close semantics: 23// The shipped nx_http_client_build_request emits "Connection: 24// close" so the server closes after responding. We read until 25// EOF (or close_notify) and return whatever was accumulated. 26// Per-response chunking + Content-Length parsing is a later 27// refinement (the calling Browser arc parses HTTP body from 28// the returned bytes). 29// 30// Public API: 31// nx_https_get_complete( 32// session, fd, path, path_len, host, host_len, 33// out_buf, out_cap 34// ) -> POSITIVE bytes_received | NEGATIVE -NX_HTTPS_GC_* code 35// nx_https_gc_verdict_is_valid(v) -> 0|1 36// 37// Sealed verdict: 38// NX_HTTPS_GC_OK positive rc = bytes received 39// NX_HTTPS_GC_BAD_STATE session not at CONNECTED 40// NX_HTTPS_GC_BUILD_FAIL request builder returned non-positive 41// NX_HTTPS_GC_ENCRYPT_FAIL record encrypt verdict non-OK 42// NX_HTTPS_GC_WRITE_FAIL sys_write returned non-positive 43// NX_HTTPS_GC_READ_FAIL record read verdict non-recoverable 44// NX_HTTPS_GC_DECRYPT_FAIL record decrypt verdict non-OK 45// NX_HTTPS_GC_BUF_OVERFLOW response exceeds out_cap 46// 47// Per Cardinals 9 (single-responsibility -- ONE round trip), 12 48// (defensive at boundaries -- cap on response size + bounds check 49// on every record), 19 (composes shipped primitives unchanged), 50// 22 (composition -- 4 shipped primitives compose into one 51// orchestrator), 23 (preamble names the Connection-close 52// semantics + queued chunking refinement). 53// 54// license_tier: INDEPENDENT_REDERIVE 55// genealogy_id: international-research-sources/ietf/rfc_8446 + rfc_9112 56// lineage_id: nishi_https_get_complete_q10 57 58// nx_safety_envelope: 59// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 60// sil_target: SIL1 61// evidence: [bulk_applied_2026-05-19, https-get-complete-step-4] 62// verdict: NOT_YET_EVALUATED 63 64import "nx_syscalls.nx" 65import "nx_tls13.nx" 66import "nx_tls13_record.nx" 67import "nx_tls13_read_record_from_fd.nx" 68import "nx_tls13_client_session.nx" 69import "nx_http_client.nx" 70import "nx_chacha20_poly1305.nx" 71 72const NX_HTTPS_GC_OK: i64 = 1 73const NX_HTTPS_GC_BAD_STATE: i64 = 2 74const NX_HTTPS_GC_BUILD_FAIL: i64 = 3 75const NX_HTTPS_GC_ENCRYPT_FAIL: i64 = 4 76const NX_HTTPS_GC_WRITE_FAIL: i64 = 5 77const NX_HTTPS_GC_READ_FAIL: i64 = 6 78const NX_HTTPS_GC_DECRYPT_FAIL: i64 = 7 79const NX_HTTPS_GC_BUF_OVERFLOW: i64 = 8 80const NX_HTTPS_GC_VERDICT_N: i64 = 9 81 82// Max bytes for the encrypted request record we build. 83// HTTP GET request is typically ~80-200 bytes; we cap at 4KB to 84// allow long paths/URLs. 85const NX_HTTPS_GC_REQ_BUF_BYTES: i64 = 8192 86 87// Max bytes per response record buffer (TLS record max + header). 88const NX_HTTPS_GC_RESP_RECORD_BYTES: i64 = 16645 89 90func nx_https_gc_verdict_is_valid(v: i64) -> i64 { 91 if v < NX_HTTPS_GC_OK { return 0 } 92 if v >= NX_HTTPS_GC_VERDICT_N { return 0 } 93 return 1 94} 95 96func _gc_pn(v: i64) -> i64 { 97 let b: *u8 = sys_mmap(24) 98 var x: i64 = v 99 if x < 0 { x = 0 - x } 100 var i: i64 = 22 101 if x == 0 { b[i] = 0x30 as u8; i = i - 1 } 102 else { while x > 0 { b[i] = (0x30 + (x - (x/10)*10)) as u8; x = x / 10; i = i - 1 } } 103 sys_write(2, ((b as i64) + i + 1) as *u8, 22 - i) 104 return 0 105} 106 107// Write exactly `n` bytes to fd via looping sys_write. 108// Returns 0 on success, -1 on any sys_write error. 109func _gc_write_n(fd: i64, buf: *u8, n: i64) -> i64 { 110 var off: i64 = 0 111 while off < n { 112 let w: i64 = sys_write(fd, (buf as i64 + off) as *u8, n - off) 113 if w <= 0 { return 0 - 1 } 114 off = off + w 115 } 116 return 0 117} 118 119// --- browser-faithful termination helpers (ADDITIVE; only used to RETURN-EARLY once a 120// Content-Length body is fully received, so the measured time is real latency and not the 121// server keep-alive idle timeout. Responses WITHOUT a Content-Length are unaffected = byte- 122// identical to the old read-until-close path). --- 123func _gc_lc(c: i64) -> i64 { if c >= 65 { if c <= 90 { return c + 32 } } return c } 124// find end-of-headers ("\r\n\r\n"); returns index of the first '\r' or -1. 125func _gc_hdr_end(buf: *u8, n: i64) -> i64 { 126 var i: i64 = 0 127 while i + 4 <= n { 128 if buf[i]==(13 as u8) { if buf[i+1]==(10 as u8) { if buf[i+2]==(13 as u8) { if buf[i+3]==(10 as u8) { return i } } } } 129 i = i + 1 130 } 131 return 0 - 1 132} 133// case-insensitive match of pat at buf[off], bounded by end. 134func _gc_ci_match(buf: *u8, off: i64, end: i64, pat: *u8, patlen: i64) -> i64 { 135 if off + patlen > end { return 0 } 136 var j: i64 = 0 137 while j < patlen { if _gc_lc(buf[off+j] as i64) != _gc_lc(pat[j] as i64) { return 0 } j = j + 1 } 138 return 1 139} 140// parse the Content-Length header value within buf[0..hdr_end]; -1 if absent. 141// matched only at a line start (offset 0 or preceded by '\n') so "X-Content-Length:" can't false-match. 142func _gc_clen(buf: *u8, hdr_end: i64) -> i64 { 143 let pat: *u8 = "content-length:" as *u8 144 var i: i64 = 0 145 while i < hdr_end { 146 var atline: i64 = 0 147 if i == 0 { atline = 1 } else { if buf[i-1]==(10 as u8) { atline = 1 } } 148 if atline == 1 { if _gc_ci_match(buf, i, hdr_end, pat, 15) == 1 { 149 var p: i64 = i + 15 150 while p < hdr_end { if buf[p]==(32 as u8) { p = p + 1 } else { break } } // skip OWS 151 var v: i64 = 0; var any: i64 = 0; var go: i64 = 1 152 while go == 1 { if p >= hdr_end { go = 0 } else { let c: i64 = buf[p] as i64; if c >= 48 { if c <= 57 { v = v*10 + (c - 48); any = 1; p = p + 1 } else { go = 0 } } else { go = 0 } } } 153 if any == 1 { return v } 154 return 0 - 1 155 } } 156 i = i + 1 157 } 158 return 0 - 1 159} 160 161// Send the HTTP GET as an encrypted TLS record + drain the 162// server's response into out_buf. See preamble for semantics. 163// nx_https_req_complete: send a PREBUILT request over the connected TLS session + read the full response. 164// The shared TLS send/recv core -- nx_https_get_complete builds a plain GET and delegates here; the CC range 165// fetch builds a "Range: bytes=S-E" GET and delegates here too. Existing GET behavior is byte-identical 166// (same request bytes, same recv loop). (2026-07-04 additive extraction; no logic change -- proven by the 167// unchanged nx_https_get_complete_test.) 168func nx_https_req_complete( 169 s: *Tls13ClientSession, 170 fd: i64, 171 req: *u8, req_len: i64, 172 out_buf: *u8, out_cap: i64 173) -> i64 { 174 if s.state != NX_TLS13_CSESSION_STATE_CONNECTED { 175 return 0 - NX_HTTPS_GC_BAD_STATE 176 } 177 if req_len <= 0 { return 0 - NX_HTTPS_GC_BUILD_FAIL } 178 179 // ---- Encrypt as TLS application_data record ---- 180 // Layout (matching nx_tls13_record_encrypt's three-buffer split): 181 // header(5) + ciphertext(req_len + 1) + tag(16) = req_len + 22 182 let rec_buf: *u8 = sys_mmap(req_len + 64) 183 let header_out: *u8 = rec_buf 184 let ct_out: *u8 = rec_buf + NX_TLS13_RECORD_HEADER_LEN 185 let tag_out: *u8 = rec_buf + NX_TLS13_RECORD_HEADER_LEN + req_len + 1 186 187 let enc_v: i64 = nx_tls13_record_encrypt_v2( 188 s.cipher_suite, 189 s.client_app_traffic_key, 190 s.client_app_iv, 191 s.client_app_seq, 192 req, req_len, 193 NX_TLS13_CT_APPLICATION_DATA, 194 0, // no padding 195 header_out, ct_out, tag_out 196 ) 197 s.client_app_seq = s.client_app_seq + 1 198 if enc_v != NX_TLS13_REC_VERDICT_OK { return 0 - NX_HTTPS_GC_ENCRYPT_FAIL } 199 200 // ---- Write encrypted request record ---- 201 let total_rec_len: i64 = NX_TLS13_RECORD_HEADER_LEN + req_len + 1 + NX_TLS13_RECORD_TAG_LEN 202 let wr_v: i64 = _gc_write_n(fd, rec_buf, total_rec_len) 203 if wr_v < 0 { return 0 - NX_HTTPS_GC_WRITE_FAIL } 204 205 // ---- Loop reading + decrypting response records ---- 206 var accumulated: i64 = 0 207 var t_recv: i64 = 0 208 var t_dec: i64 = 0 209 var nrecs: i64 = 0 210 var body_target: i64 = 0 // >0 once Content-Length body length is known; 0 = read-to-close (unchanged path) 211 var hdr_parsed: i64 = 0 212 // Reusable per-record scratch -- allocate ONCE, not per record. The per-record 213 // sys_mmap churn (~6/record, never freed) was the measured transfer overhead 214 // (~13ms/record) -- the cipher itself is ~0.26ms/record. 215 let rec_in: *u8 = sys_mmap(NX_HTTPS_GC_RESP_RECORD_BYTES) 216 let plaintext: *u8 = sys_mmap(NX_HTTPS_GC_RESP_RECORD_BYTES) 217 let plaintext_ct_p: *i64 = sys_mmap(16) as *i64 218 let plaintext_len_p: *i64 = sys_mmap(16) as *i64 219 220 while accumulated < out_cap { 221 let _r0: i64 = sys_now_ms() 222 let rec_in_total: i64 = nx_tls13_read_record_from_fd( 223 fd, rec_in, NX_HTTPS_GC_RESP_RECORD_BYTES 224 ) 225 t_recv = t_recv + (sys_now_ms() - _r0) 226 nrecs = nrecs + 1 227 // EOF or peer-close is a NORMAL termination of "Connection: 228 // close" responses. Any other negative verdict is an error. 229 if rec_in_total < 0 { 230 let nv: i64 = 0 - rec_in_total 231 if nv == NX_TLS13_READ_REC_EOF { 232 sys_write(2, "nishi-xfer recv=" as *u8, 16); _gc_pn(t_recv) 233 sys_write(2, "ms dec=" as *u8, 7); _gc_pn(t_dec); sys_write(2, "ms\n" as *u8, 3) 234 return accumulated 235 } 236 if nv == NX_TLS13_READ_REC_PAYLOAD_EOF { return accumulated } 237 return 0 - NX_HTTPS_GC_READ_FAIL 238 } 239 240 // Split header/ciphertext/tag 241 let rec_in_header: *u8 = rec_in 242 let rec_in_ct: *u8 = rec_in + NX_TLS13_RECORD_HEADER_LEN 243 let rec_in_ct_len: i64 = rec_in_total - NX_TLS13_RECORD_HEADER_LEN - NX_TLS13_RECORD_TAG_LEN 244 let rec_in_tag: *u8 = rec_in + rec_in_total - NX_TLS13_RECORD_TAG_LEN 245 246 let _d0: i64 = sys_now_ms() 247 let dec_v: i64 = nx_tls13_record_decrypt_v2( 248 s.cipher_suite, 249 s.server_app_traffic_key, 250 s.server_app_iv, 251 s.server_app_seq, 252 rec_in_header, 253 rec_in_ct, rec_in_ct_len, 254 rec_in_tag, 255 plaintext, 256 plaintext_ct_p, plaintext_len_p 257 ) 258 t_dec = t_dec + (sys_now_ms() - _d0) 259 s.server_app_seq = s.server_app_seq + 1 260 if dec_v != NX_TLS13_REC_VERDICT_OK { return 0 - NX_HTTPS_GC_DECRYPT_FAIL } 261 262 // Process by content type. RFC 8446 ยง6 close_notify alert 263 // (level=1=warning, description=0=close_notify) terminates 264 // the session gracefully -- return what we have. 265 if *plaintext_ct_p == NX_TLS13_CT_ALERT { 266 // Alert payload is 2 bytes: [level][description]; close_notify = description 0. 267 // Any alert ends the response gracefully -- emit the native transfer split first. 268 sys_write(2, "nishi-xfer recv=" as *u8, 16); _gc_pn(t_recv) 269 sys_write(2, "ms dec=" as *u8, 7); _gc_pn(t_dec) 270 sys_write(2, "ms (macbuild=" as *u8, 13); _gc_pn(nx_cp_mac_ms()) 271 sys_write(2, " poly=" as *u8, 6); _gc_pn(nx_cp_poly_ms()) 272 sys_write(2, " cha=" as *u8, 5); _gc_pn(nx_cp_cha_ms()) 273 sys_write(2, ") nrecs=" as *u8, 8); _gc_pn(nrecs) 274 sys_write(2, " aead_calls=" as *u8, 12); _gc_pn(nx_cp_calls()) 275 sys_write(2, " scratch=" as *u8, 9); _gc_pn(nx_cp_scratch()); sys_write(2, "\n" as *u8, 1) 276 return accumulated 277 } 278 279 if *plaintext_ct_p == NX_TLS13_CT_APPLICATION_DATA { 280 // Append plaintext into out_buf 281 let avail: i64 = out_cap - accumulated 282 let to_copy: i64 = *plaintext_len_p 283 if to_copy > avail { return 0 - NX_HTTPS_GC_BUF_OVERFLOW } 284 var i: i64 = 0 285 while i < to_copy { 286 out_buf[accumulated + i] = plaintext[i] 287 i = i + 1 288 } 289 accumulated = accumulated + to_copy 290 // browser-faithful early return: once Content-Length is known and the full body is in, 291 // stop instead of blocking until the server's keep-alive close (which falsely measured 292 // the idle timeout). Additive: only fires for Content-Length responses fully received. 293 if hdr_parsed == 0 { 294 let he: i64 = _gc_hdr_end(out_buf, accumulated) 295 if he >= 0 { 296 hdr_parsed = 1 297 let cl: i64 = _gc_clen(out_buf, he) 298 if cl >= 0 { body_target = he + 4 + cl } 299 } 300 } 301 if body_target > 0 { if accumulated >= body_target { 302 sys_write(2, "nishi-xfer recv=" as *u8, 16); _gc_pn(t_recv) 303 sys_write(2, "ms dec=" as *u8, 7); _gc_pn(t_dec); sys_write(2, "ms cl-stop\n" as *u8, 11) 304 return accumulated 305 } } 306 } 307 308 // Other content types (handshake, change_cipher_spec) are 309 // unexpected post-CONNECTED but we tolerate them by skipping 310 // (defense against post-handshake messages that some real 311 // servers send, e.g. NewSessionTicket). 312 } 313 314 return accumulated 315} 316 317// nx_https_get_complete: build a plain GET for `path` and send it via the shared core (unchanged public API). 318func nx_https_get_complete( 319 s: *Tls13ClientSession, 320 fd: i64, 321 path: *u8, path_len: i64, 322 host: *u8, host_len: i64, 323 out_buf: *u8, out_cap: i64 324) -> i64 { 325 let req: *u8 = sys_mmap(NX_HTTPS_GC_REQ_BUF_BYTES) 326 let req_len: i64 = nx_http_client_build_request(path, path_len, host, host_len, req) 327 return nx_https_req_complete(s, fd, req, req_len, out_buf, out_cap) 328} 329 330// Cookie-aware variant: builds a plain GET carrying a "Cookie: <cookie>\r\n" header (when 331// cookie_len>0) via nx_http_client_build_request_cookie, then delegates to the same shared TLS 332// send/recv core. Lets a redirect-following fetch replay a session across hops. Additive; the 333// original nx_https_get_complete is untouched (Cardinal 19). 334func nx_https_get_complete_cookie( 335 s: *Tls13ClientSession, 336 fd: i64, 337 path: *u8, path_len: i64, 338 host: *u8, host_len: i64, 339 cookie: *u8, cookie_len: i64, 340 out_buf: *u8, out_cap: i64 341) -> i64 { 342 let req: *u8 = sys_mmap(NX_HTTPS_GC_REQ_BUF_BYTES) 343 let req_len: i64 = nx_http_client_build_request_cookie(path, path_len, host, host_len, cookie, cookie_len, req) 344 return nx_https_req_complete(s, fd, req, req_len, out_buf, out_cap) 345} 346 347// Compile-only smoke. Real KAT in nx_https_get_complete_test.nx. 348func main() -> i64 { 349 return 0 350}