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}