code wiki / (root) / nx_https_get_complete.nx

nx_https_get_complete.nx source

↩ module page · 516 lines · 25993 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// --- TLS-1.3 BODY COMPLETENESS: THE TRANSPORT ALREADY KNEW AND THREW IT AWAY ------------------- 162// nx_https_req_complete below computes body_target from Content-Length purely so it can RETURN EARLY 163// once the whole body is in. At the three OTHER termination exits -- record EOF, payload EOF, and the 164// close_notify alert -- it returned `accumulated` with NO comparison at all, so a body the peer cut 165// short left this function as a POSITIVE byte count and every caller (ff_core_x included) reads 166// positive + status 200 as success. The TLS-1.2 sibling leg has carried TR_TRUNCATED since 167// 2026-08-18; this is THE SAME LAW APPLIED TO THE LEG THAT WAS MISSING IT. 168// 169// ANNOUNCE-FIRST BY CONSTRUCTION -- THE RETURN CONTRACT IS UNCHANGED (Cardinal 19). A short body is 170// still returned and still positive; nothing that succeeds today begins to fail. What changes is that 171// it becomes SAYABLE: stderr carries a distinguishable marker and the outcome is queryable. A guard 172// that converts silent successes into estate-wide refusals on the day it ships is a guard everyone 173// turns off; the refusal is armed later, from a drained ratchet, and the arming condition is named in 174// nx_research_fetch.nx beside the counter that has to reach zero first. 175// 176// STATIC + ACCESSOR, composing the ff_last_hops_g idiom already live in nx_https_fetch_follow.nx 177// rather than inventing a second convention: this core is reached through five wrappers and ~200 call 178// sites, so a signature change would be a large blast radius for a read-only diagnostic. 179// SCOPE, DECLARED: single-threaded fork-per-job organs, last-call-wins. Read it immediately after the 180// fetch you care about. It is a diagnostic, never an authorisation input. 181// 182// FOUR STATES, BECAUSE AN AXIS THAT CANNOT SEE MUST ABSTAIN RATHER THAN ACQUIT. A chunked or 183// Connection-close response declares no length at all, so "complete" is NOT derivable from it and 184// reporting COMPLETE there would be a false proof wearing an authoritative name. 185const NX_HTTPS_BODY_COMPLETE: i64 = 0 // Content-Length declared N and N bytes arrived 186const NX_HTTPS_BODY_TRUNCATED: i64 = 1 // Content-Length declared N, the peer stopped at M < N 187const NX_HTTPS_BODY_UNJUDGEABLE: i64 = 2 // no Content-Length reached us: chunked, or read-to-close 188const NX_HTTPS_BODY_UNOBSERVED: i64 = 3 // the read loop never ran (bad state / build / write fail) 189 190static gc_body_state_g: i64 191static gc_body_expected_g: i64 192static gc_body_got_g: i64 193 194func nx_https_last_body_state() -> i64 { return gc_body_state_g } 195func nx_https_last_body_expected() -> i64 { return gc_body_expected_g } 196func nx_https_last_body_got() -> i64 { return gc_body_got_g } 197 198// THE PURE PREDICATE, EXTRACTED SO A GATE CAN EXERCISE IT WITH FIXTURES AND NO NETWORK. Every 199// termination exit calls THIS rather than re-deriving the comparison, so the rule cannot drift 200// between exits -- which is precisely how three of the four exits came to be missing it. 201// body_target is the absolute offset of the end of the body (hdr_end + 4 + content_length), and 0 202// when no Content-Length was ever parsed. accumulated is what actually arrived. 203func nx_https_body_state(body_target: i64, accumulated: i64) -> i64 { 204 if body_target <= 0 { return NX_HTTPS_BODY_UNJUDGEABLE } 205 if accumulated < body_target { return NX_HTTPS_BODY_TRUNCATED } 206 return NX_HTTPS_BODY_COMPLETE 207} 208// Shortfall in BYTES, and 0 for every state that is not TRUNCATED -- so a caller that wants only the 209// number cannot read "I could not judge this" as "no damage". 210func nx_https_body_shortfall(body_target: i64, accumulated: i64) -> i64 { 211 if nx_https_body_state(body_target, accumulated) != NX_HTTPS_BODY_TRUNCATED { return 0 } 212 return body_target - accumulated 213} 214// DERIVE the length of a literal, never hand-count it beside the literal: a hand-counted length is a 215// second copy of the string's shape and the two drift the moment anybody edits the words. 216func _gc_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 217func _gc_e(s: *u8) -> i64 { sys_write(2, s, _gc_slen(s)); return 0 } 218// Seal the outcome into the statics and ANNOUNCE a short body with BOTH numbers, so the marker is 219// greppable and self-explaining instead of a bare flag somebody has to go and decode. 220func _gc_body_seal(body_target: i64, accumulated: i64) -> i64 { 221 let st: i64 = nx_https_body_state(body_target, accumulated) 222 gc_body_state_g = st 223 gc_body_expected_g = body_target 224 gc_body_got_g = accumulated 225 if st == NX_HTTPS_BODY_TRUNCATED { 226 _gc_e("nishi-xfer TRUNCATED expected=" as *u8); _gc_pn(body_target) 227 _gc_e(" got=" as *u8); _gc_pn(accumulated) 228 _gc_e(" short=" as *u8); _gc_pn(body_target - accumulated) 229 _gc_e(" -- the peer declared a Content-Length this TLS-1.3 read did not reach\n" as *u8) 230 } 231 return st 232} 233 234// Send the HTTP GET as an encrypted TLS record + drain the 235// server's response into out_buf. See preamble for semantics. 236// nx_https_req_complete: send a PREBUILT request over the connected TLS session + read the full response. 237// The shared TLS send/recv core -- nx_https_get_complete builds a plain GET and delegates here; the CC range 238// fetch builds a "Range: bytes=S-E" GET and delegates here too. Existing GET behavior is byte-identical 239// (same request bytes, same recv loop). (2026-07-04 additive extraction; no logic change -- proven by the 240// unchanged nx_https_get_complete_test.) 241func nx_https_req_complete( 242 s: *Tls13ClientSession, 243 fd: i64, 244 req: *u8, req_len: i64, 245 out_buf: *u8, out_cap: i64 246) -> i64 { 247 return _gc_req_core(s, fd, req, req_len, out_buf, out_cap, 0) 248} 249 250// nx_https_req_prefix: the SAME send + recv core, but a BOUNDED PREFIX READ (2026-09-05). A completing 251// read must refuse a body that does not fit (NX_HTTPS_GC_BUF_OVERFLOW) because a silent cap-truncation is 252// the defect this file family exists to stop. A prefix read is the caller SAYING it wants at most out_cap 253// bytes -- a verifier checking eight magic bytes of a ten-megabyte rig, a browser sniffing a media head -- 254// so here the record that crosses the cap is copied up to the cap, the body state is sealed TRUNCATED (the 255// seal announces expected/got on stderr, so the partial is loud), and the prefix is RETURNED. Read 256// gc_body_state_g after the call to know which. Measured need: /world/ref9d.nxa, 10,758,232 B, fetched 257// whole by nx_page_verify against an 8 MB cap -> -NX_FF_GET, and the edge ignores Range, so a bounded read 258// is the only door to the magic. Additive extraction: nx_https_req_complete delegates with prefix_ok=0 and 259// is byte-for-byte the old behaviour for every existing caller. 260func nx_https_req_prefix( 261 s: *Tls13ClientSession, 262 fd: i64, 263 req: *u8, req_len: i64, 264 out_buf: *u8, out_cap: i64 265) -> i64 { 266 return _gc_req_core(s, fd, req, req_len, out_buf, out_cap, 1) 267} 268 269func _gc_req_core( 270 s: *Tls13ClientSession, 271 fd: i64, 272 req: *u8, req_len: i64, 273 out_buf: *u8, out_cap: i64, 274 prefix_ok: i64 275) -> i64 { 276 // UNOBSERVED until the read loop actually runs. A state that cannot say "I never looked" 277 // reports absence of evidence as evidence of absence -- the ff_last_hops_g lesson, same file family. 278 gc_body_state_g = NX_HTTPS_BODY_UNOBSERVED 279 gc_body_expected_g = 0 280 gc_body_got_g = 0 281 if s.state != NX_TLS13_CSESSION_STATE_CONNECTED { 282 return 0 - NX_HTTPS_GC_BAD_STATE 283 } 284 if req_len <= 0 { return 0 - NX_HTTPS_GC_BUILD_FAIL } 285 286 // ---- Encrypt and write the request as ONE OR MORE TLS application_data records ---- 287 // RFC 8446 5.1 bounds a record at 2^14 bytes of plaintext, and nx_tls13_record_encrypt_v2 already 288 // enforces it (NX_TLS13_MAX_INNER_PLAINTEXT, where inner = content + one content-type byte). 289 // Writing the whole request as a SINGLE record therefore gave this transport a hard ~16 KB request 290 // ceiling -- and every caller above it wore that ceiling as a hand-picked body constant instead of 291 // as the record limit it actually was. Measured cost 2026-09-04: the sovereign content shipper 292 // could not send the 48,402-byte chunks its own server offers, so ~40 abandoned transfers had piled 293 // up in the staging directory over eleven days while each failure reported only "post failed". 294 // THE FRAGMENT SIZE IS DERIVED FROM THE RECORD LAYER'S OWN LIMIT, NEVER RE-STATED HERE: a second 295 // 16384 would be the duplicate-ruler defect and would drift silently the moment that layer moved. 296 // For any request that already fitted one record this is byte-identical to the write it replaces -- 297 // one iteration, same buffer arithmetic, same sequence advance. 298 // Composes the record layer's own pure arithmetic -- see nx_tls13_frag_len/_count. Re-deriving the 299 // split here would put a second copy of the rule beside the one a gate can actually test. 300 let frag_max: i64 = nx_tls13_frag_max() 301 var rec_cap: i64 = req_len 302 if rec_cap > frag_max { rec_cap = frag_max } 303 // Allocated ONCE, outside the loop: a per-record mmap here would be an allocation in a hot path 304 // for exactly the large payloads this change exists to enable. 305 let rec_buf: *u8 = sys_mmap(rec_cap + 64) 306 var off: i64 = 0 307 while off < req_len { 308 let frag: i64 = nx_tls13_frag_len(req_len, off) 309 let header_out: *u8 = rec_buf 310 let ct_out: *u8 = rec_buf + NX_TLS13_RECORD_HEADER_LEN 311 let tag_out: *u8 = rec_buf + NX_TLS13_RECORD_HEADER_LEN + frag + 1 312 let enc_v: i64 = nx_tls13_record_encrypt_v2( 313 s.cipher_suite, 314 s.client_app_traffic_key, 315 s.client_app_iv, 316 s.client_app_seq, 317 (req as i64 + off) as *u8, frag, 318 NX_TLS13_CT_APPLICATION_DATA, 319 0, // no padding 320 header_out, ct_out, tag_out 321 ) 322 // Advanced per RECORD, not per request: every fragment is its own AEAD nonce, so a shared 323 // sequence number across fragments would produce a peer-rejected stream that reads as a 324 // network fault rather than as a client defect. 325 s.client_app_seq = s.client_app_seq + 1 326 if enc_v != NX_TLS13_REC_VERDICT_OK { return 0 - NX_HTTPS_GC_ENCRYPT_FAIL } 327 let total_rec_len: i64 = NX_TLS13_RECORD_HEADER_LEN + frag + 1 + NX_TLS13_RECORD_TAG_LEN 328 let wr_v: i64 = _gc_write_n(fd, rec_buf, total_rec_len) 329 if wr_v < 0 { return 0 - NX_HTTPS_GC_WRITE_FAIL } 330 off = off + frag 331 } 332 333 // ---- Loop reading + decrypting response records ---- 334 var accumulated: i64 = 0 335 var t_recv: i64 = 0 336 var t_dec: i64 = 0 337 var nrecs: i64 = 0 338 var body_target: i64 = 0 // >0 once Content-Length body length is known; 0 = read-to-close (unchanged path) 339 var hdr_parsed: i64 = 0 340 // Reusable per-record scratch -- allocate ONCE, not per record. The per-record 341 // sys_mmap churn (~6/record, never freed) was the measured transfer overhead 342 // (~13ms/record) -- the cipher itself is ~0.26ms/record. 343 let rec_in: *u8 = sys_mmap(NX_HTTPS_GC_RESP_RECORD_BYTES) 344 let plaintext: *u8 = sys_mmap(NX_HTTPS_GC_RESP_RECORD_BYTES) 345 let plaintext_ct_p: *i64 = sys_mmap(16) as *i64 346 let plaintext_len_p: *i64 = sys_mmap(16) as *i64 347 348 while accumulated < out_cap { 349 let _r0: i64 = sys_now_ms() 350 let rec_in_total: i64 = nx_tls13_read_record_from_fd( 351 fd, rec_in, NX_HTTPS_GC_RESP_RECORD_BYTES 352 ) 353 t_recv = t_recv + (sys_now_ms() - _r0) 354 nrecs = nrecs + 1 355 // EOF or peer-close is a NORMAL termination of "Connection: 356 // close" responses. Any other negative verdict is an error. 357 if rec_in_total < 0 { 358 let nv: i64 = 0 - rec_in_total 359 if nv == NX_TLS13_READ_REC_EOF { 360 sys_write(2, "nishi-xfer recv=" as *u8, 16); _gc_pn(t_recv) 361 sys_write(2, "ms dec=" as *u8, 7); _gc_pn(t_dec); sys_write(2, "ms\n" as *u8, 3) 362 _gc_body_seal(body_target, accumulated) 363 return accumulated 364 } 365 if nv == NX_TLS13_READ_REC_PAYLOAD_EOF { _gc_body_seal(body_target, accumulated); return accumulated } 366 return 0 - NX_HTTPS_GC_READ_FAIL 367 } 368 369 // Split header/ciphertext/tag 370 let rec_in_header: *u8 = rec_in 371 let rec_in_ct: *u8 = rec_in + NX_TLS13_RECORD_HEADER_LEN 372 let rec_in_ct_len: i64 = rec_in_total - NX_TLS13_RECORD_HEADER_LEN - NX_TLS13_RECORD_TAG_LEN 373 let rec_in_tag: *u8 = rec_in + rec_in_total - NX_TLS13_RECORD_TAG_LEN 374 375 let _d0: i64 = sys_now_ms() 376 let dec_v: i64 = nx_tls13_record_decrypt_v2( 377 s.cipher_suite, 378 s.server_app_traffic_key, 379 s.server_app_iv, 380 s.server_app_seq, 381 rec_in_header, 382 rec_in_ct, rec_in_ct_len, 383 rec_in_tag, 384 plaintext, 385 plaintext_ct_p, plaintext_len_p 386 ) 387 t_dec = t_dec + (sys_now_ms() - _d0) 388 s.server_app_seq = s.server_app_seq + 1 389 if dec_v != NX_TLS13_REC_VERDICT_OK { return 0 - NX_HTTPS_GC_DECRYPT_FAIL } 390 391 // Process by content type. RFC 8446 ยง6 close_notify alert 392 // (level=1=warning, description=0=close_notify) terminates 393 // the session gracefully -- return what we have. 394 if *plaintext_ct_p == NX_TLS13_CT_ALERT { 395 // Alert payload is 2 bytes: [level][description]; close_notify = description 0. 396 // Any alert ends the response gracefully -- emit the native transfer split first. 397 sys_write(2, "nishi-xfer recv=" as *u8, 16); _gc_pn(t_recv) 398 sys_write(2, "ms dec=" as *u8, 7); _gc_pn(t_dec) 399 sys_write(2, "ms (macbuild=" as *u8, 13); _gc_pn(nx_cp_mac_ms()) 400 sys_write(2, " poly=" as *u8, 6); _gc_pn(nx_cp_poly_ms()) 401 sys_write(2, " cha=" as *u8, 5); _gc_pn(nx_cp_cha_ms()) 402 sys_write(2, ") nrecs=" as *u8, 8); _gc_pn(nrecs) 403 sys_write(2, " aead_calls=" as *u8, 12); _gc_pn(nx_cp_calls()) 404 sys_write(2, " scratch=" as *u8, 9); _gc_pn(nx_cp_scratch()); sys_write(2, "\n" as *u8, 1) 405 _gc_body_seal(body_target, accumulated) 406 return accumulated 407 } 408 409 if *plaintext_ct_p == NX_TLS13_CT_APPLICATION_DATA { 410 // Append plaintext into out_buf 411 let avail: i64 = out_cap - accumulated 412 let to_copy: i64 = *plaintext_len_p 413 if to_copy > avail { 414 if prefix_ok == 1 { 415 // A BOUNDED PREFIX READ STOPS AT THE CAP AND SAYS SO (nx_https_req_prefix): copy what fits, 416 // seal the body state TRUNCATED (the seal announces expected/got on stderr) and return the 417 // prefix. Every completing caller keeps the refusal below, unchanged. 418 var j: i64 = 0 419 while j < avail { out_buf[accumulated + j] = plaintext[j]; j = j + 1 } 420 accumulated = accumulated + avail 421 _gc_body_seal(body_target, accumulated) 422 return accumulated 423 } 424 return 0 - NX_HTTPS_GC_BUF_OVERFLOW 425 } 426 var i: i64 = 0 427 while i < to_copy { 428 out_buf[accumulated + i] = plaintext[i] 429 i = i + 1 430 } 431 accumulated = accumulated + to_copy 432 // browser-faithful early return: once Content-Length is known and the full body is in, 433 // stop instead of blocking until the server's keep-alive close (which falsely measured 434 // the idle timeout). Additive: only fires for Content-Length responses fully received. 435 if hdr_parsed == 0 { 436 let he: i64 = _gc_hdr_end(out_buf, accumulated) 437 if he >= 0 { 438 hdr_parsed = 1 439 let cl: i64 = _gc_clen(out_buf, he) 440 if cl >= 0 { body_target = he + 4 + cl } 441 } 442 } 443 if body_target > 0 { if accumulated >= body_target { 444 sys_write(2, "nishi-xfer recv=" as *u8, 16); _gc_pn(t_recv) 445 sys_write(2, "ms dec=" as *u8, 7); _gc_pn(t_dec); sys_write(2, "ms cl-stop\n" as *u8, 11) 446 _gc_body_seal(body_target, accumulated) 447 return accumulated 448 } } 449 } 450 451 // Other content types (handshake, change_cipher_spec) are 452 // unexpected post-CONNECTED but we tolerate them by skipping 453 // (defense against post-handshake messages that some real 454 // servers send, e.g. NewSessionTicket). 455 } 456 457 // The loop ran out of out_cap rather than reaching an end-of-body signal. That is exactly the 458 // shape a silent cap-truncation takes, so it gets the same seal as every other exit. 459 _gc_body_seal(body_target, accumulated) 460 return accumulated 461} 462 463// nx_https_get_complete: build a plain GET for `path` and send it via the shared core (unchanged public API). 464func nx_https_get_complete( 465 s: *Tls13ClientSession, 466 fd: i64, 467 path: *u8, path_len: i64, 468 host: *u8, host_len: i64, 469 out_buf: *u8, out_cap: i64 470) -> i64 { 471 let req: *u8 = sys_mmap(NX_HTTPS_GC_REQ_BUF_BYTES) 472 let req_len: i64 = nx_http_client_build_request(path, path_len, host, host_len, req) 473 return nx_https_req_complete(s, fd, req, req_len, out_buf, out_cap) 474} 475 476// Cookie-aware variant: builds a plain GET carrying a "Cookie: <cookie>\r\n" header (when 477// cookie_len>0) via nx_http_client_build_request_cookie, then delegates to the same shared TLS 478// send/recv core. Lets a redirect-following fetch replay a session across hops. Additive; the 479// original nx_https_get_complete is untouched (Cardinal 19). 480func nx_https_get_complete_cookie( 481 s: *Tls13ClientSession, 482 fd: i64, 483 path: *u8, path_len: i64, 484 host: *u8, host_len: i64, 485 cookie: *u8, cookie_len: i64, 486 out_buf: *u8, out_cap: i64 487) -> i64 { 488 let req: *u8 = sys_mmap(NX_HTTPS_GC_REQ_BUF_BYTES) 489 let req_len: i64 = nx_http_client_build_request_cookie(path, path_len, host, host_len, cookie, cookie_len, req) 490 return nx_https_req_complete(s, fd, req, req_len, out_buf, out_cap) 491} 492 493// Cookie + extra-header variant: builds a GET carrying BOTH the cookie jar (when cookie_len>0) and the 494// caller's xhdr line(s) (when xhdr_len>0) via nx_http_client_build_request_cookie_xhdr, then delegates to the 495// SAME shared TLS send/recv core. Additive; the cookie-only + base variants are untouched (Cardinal 19). 496func nx_https_get_complete_cookie_xhdr( 497 s: *Tls13ClientSession, 498 fd: i64, 499 path: *u8, path_len: i64, 500 host: *u8, host_len: i64, 501 cookie: *u8, cookie_len: i64, 502 xhdr: *u8, xhdr_len: i64, 503 out_buf: *u8, out_cap: i64 504) -> i64 { 505 let req_cap: i64=nx_http_client_request_cap(path_len,host_len,cookie_len,xhdr_len) 506 let req: *u8 = sys_mmap(req_cap) 507 let req_len: i64 = nx_http_client_build_request_cookie_xhdr(path, path_len, host, host_len, cookie, cookie_len, xhdr, xhdr_len, req) 508 let result: i64=nx_https_req_complete(s,fd,req,req_len,out_buf,out_cap) 509 sys_munmap(req,req_cap) 510 return result 511} 512 513// Compile-only smoke. Real KAT in nx_https_get_complete_test.nx. 514func main() -> i64 { 515 return 0 516}