code wiki / (root) / nx_https_post_complete.nx

nx_https_post_complete.nx source

↩ module page · 187 lines · 9961 B

1// nx_https_post_complete.nx -- HTTPS POST companion to 2// nx_https_get_complete. Sends an HTTP/1.1 POST request with a 3// caller-supplied body (typically application/jose+json for ACME) 4// over a CONNECTED TLS 1.3 session, then drains the encrypted 5// response into out_buf. 6// 7// Composes: 8// nx_http_client_build_request_post -- POST line + headers + body 9// nx_tls13_record_encrypt_v2 -- AEAD wrap (one or more request records) 10// nx_tls13_read_record_from_fd -- partial-read framer 11// nx_tls13_record_decrypt_v2 -- AEAD unwrap per response rec 12// 13// Body size: UNBOUNDED by this module (2026-09-04). The old 8 KB ceiling was not a statement about 14// bodies -- it was an under-approximation of the TLS record limit, needed while the request was 15// written as a SINGLE record. nx_https_req_complete now fragments across records and the request 16// buffer is derived from body_len, so neither constraint remains. The instruction this paragraph 17// used to give -- 'caller should fragment larger payloads explicitly' -- was never followed by any 18// caller: they inherited the ceiling as a hand-picked constant instead, which is how the sovereign 19// content shipper spent eleven days unable to send the chunks its own server offered. 20// 21// Verdict mirrors NX_HTTPS_GC_* with a PC prefix; same sealed-enum 22// shape (positive bytes on success, negative -verdict on failure). 23// 24// Per Cardinals 9 (single-responsibility -- ONE round trip), 22 25// (composition of shipped primitives), 25 (build intelligence, don't 26// strip features -- adds POST capability without weakening GET). 27// 28// license_tier: INDEPENDENT_REDERIVE 29// genealogy_id: international-research-sources/ietf/rfc_8446 + rfc_9112 + rfc_8555 30// lineage_id: nishi_https_post_complete_q1 31 32import "nx_syscalls.nx" 33import "nx_tls13.nx" 34import "nx_tls13_record.nx" 35import "nx_tls13_read_record_from_fd.nx" 36import "nx_tls13_client_session.nx" 37import "nx_http_client.nx" 38import "nx_https_get_complete.nx" // nx_https_req_complete: the shared send-raw-request/read-response spine 39 40const NX_HTTPS_PC_OK: i64 = 1 41const NX_HTTPS_PC_BAD_STATE: i64 = 2 42const NX_HTTPS_PC_BUILD_FAIL: i64 = 3 43const NX_HTTPS_PC_ENCRYPT_FAIL: i64 = 4 44const NX_HTTPS_PC_WRITE_FAIL: i64 = 5 45const NX_HTTPS_PC_READ_FAIL: i64 = 6 46const NX_HTTPS_PC_DECRYPT_FAIL: i64 = 7 47const NX_HTTPS_PC_BUF_OVERFLOW: i64 = 8 48const NX_HTTPS_PC_BODY_TOO_BIG: i64 = 9 49const NX_HTTPS_PC_VERDICT_N: i64 = 10 50 51const NX_HTTPS_PC_REQ_BUF_BYTES: i64 = 16384 // header (~512) + body (<= 8 KB) + slack 52const NX_HTTPS_PC_BODY_BYTES: i64 = 8192 53const NX_HTTPS_PC_RESP_RECORD_BYTES: i64 = 16645 54const NX_HTTPS_PC_POST_HDR_SLACK: i64 = 128 // Content-Type: + Content-Length: <n> + CRLFs 55 56func nx_https_pc_verdict_is_valid(v: i64) -> i64 { 57 if v < NX_HTTPS_PC_OK { return 0 } 58 if v >= NX_HTTPS_PC_VERDICT_N { return 0 } 59 return 1 60} 61 62func _pc_write_n(fd: i64, buf: *u8, n: i64) -> i64 { 63 var off: i64 = 0 64 while off < n { 65 let w: i64 = sys_write(fd, (buf as i64 + off) as *u8, n - off) 66 if w <= 0 { return 0 - 1 } 67 off = off + w 68 } 69 return 0 70} 71 72func nx_https_post_complete( 73 s: *Tls13ClientSession, 74 fd: i64, 75 path: *u8, path_len: i64, 76 host: *u8, host_len: i64, 77 content_type: *u8, content_type_len: i64, 78 body: *u8, body_len: i64, 79 out_buf: *u8, out_cap: i64 80) -> i64 { 81 if s.state != NX_TLS13_CSESSION_STATE_CONNECTED { 82 return 0 - NX_HTTPS_PC_BAD_STATE 83 } 84 // THE 8 KB BODY REFUSAL IS GONE HERE TOO, AND FOR THE SAME REASON AS IN ITS SIBLING: it was never 85 // a statement about bodies. It was a conservative under-approximation of the TLS record limit, 86 // forced by the INLINE single-record write this function used to carry. That write is now delegated 87 // to nx_https_req_complete -- the spine this file's own import comment already calls shared -- which 88 // fragments across records, so the constraint the constant stood for no longer exists. 89 90 // ---- Build HTTP POST request ---- 91 // DERIVED, not a fixed NX_HTTPS_PC_REQ_BUF_BYTES. With the body refusal removed, a fixed buffer 92 // would silently TRUNCATE an oversized request instead of refusing it -- turning a loud, named 93 // failure into the silent-corruption class, which is strictly worse than the cap it replaced. 94 // Same formula as the xhdr variant, with cookie and xhdr lengths zero. 95 let req_cap: i64 = nx_http_client_request_cap(path_len, host_len, 0, 0) 96 + content_type_len + body_len + NX_HTTPS_PC_POST_HDR_SLACK 97 let req: *u8 = sys_mmap(req_cap) 98 let req_len: i64 = nx_http_client_build_request_post( 99 path, path_len, 100 host, host_len, 101 content_type, content_type_len, 102 body, body_len, 103 req 104 ) 105 if req_len <= 0 { return 0 - NX_HTTPS_PC_BUILD_FAIL } 106 107 // ---- Send and read through THE SHARED SPINE ---- 108 // This function carried its own copy of encrypt -> write -> read/decrypt: ~80 lines duplicating 109 // nx_https_req_complete. That duplicate is why fragmenting the request in ONE place did not fix 110 // the other, and it is the duplicate-ruler defect exactly -- two implementations of one invariant, 111 // drifting the moment either is touched. The import comment at the head of this file already 112 // declared that spine shared, so this COMPLETES a migration that was started and left half done 113 // rather than inventing a new arrangement. Error codes now come from the spine (NX_HTTPS_GC_*), 114 // which is what the xhdr variant beside it has always returned; every caller tests for < 0. 115 return nx_https_req_complete(s, fd, req, req_len, out_buf, out_cap) 116} 117 118/// ---- POST with a User-Agent, a cookie jar and caller headers ------------------------------------- 119// WHY: nx_https_post_complete above composes nx_http_client_build_request_post, which emits ONLY 120// Host, Content-Type, Content-Length and Connection: close. That request carries NO User-Agent and 121// there is no way for a caller to add one, so the sovereign POST path is structurally unable to 122// reach an edge that fingerprints its clients. Measured 2026-09-01 while wiring the chaturbate 123// live-capture adapter: its /get_edge_hls_url_ajax/ endpoint is Cloudflare-fronted and wants 124// X-Requested-With: XMLHttpRequest, and the room path wants the /?next= age-gate cookies that 125// nx_http_client's own build_request_cookie comment already documents. 126// 127// STRICTLY ADDITIVE, TWO WAYS: 128// 1. nx_https_post_complete is untouched, so nx_acme_http, nx_funcheck, nx_edge_probe and 129// nx_porkbun_ping_probe emit byte-identical requests. This is deliberate -- silently adding a 130// User-Agent to production ACME traffic is a behaviour change nobody asked for. 131// 2. The transport is NOT re-implemented. This delegates to nx_https_req_complete, the same 132// send-raw-request / read-complete-response spine nx_https_get_complete uses, so there is no 133// second TLS record loop to drift. (The function above ALSO delegates to that spine as of 134// 2026-09-04; its inlined copy -- the reason fragmenting the request in one place did not 135// fix the other -- has been removed, so there is now exactly one record loop in this file.) 136// 137// Returns response bytes written to out_buf, or -NX_HTTPS_PC_* on failure. 138 139func nx_https_post_complete_xhdr( 140 s: *Tls13ClientSession, 141 fd: i64, 142 path: *u8, path_len: i64, 143 host: *u8, host_len: i64, 144 content_type: *u8, content_type_len: i64, 145 body: *u8, body_len: i64, 146 cookie: *u8, cookie_len: i64, 147 xhdr: *u8, xhdr_len: i64, 148 out_buf: *u8, out_cap: i64 149) -> i64 { 150 if s.state != NX_TLS13_CSESSION_STATE_CONNECTED { return 0 - NX_HTTPS_PC_BAD_STATE } 151 // THE 8 KB BODY REFUSAL IS REMOVED HERE, AND ONLY HERE, BECAUSE ONLY HERE IS IT OBSOLETE. 152 // It was never about the body: it was a conservative under-approximation of the TLS record limit, 153 // because nx_https_req_complete used to write the whole request as ONE record. That function now 154 // fragments across records, and this variant's request buffer is DERIVED from body_len just below, 155 // so neither constraint the constant stood for survives. Removing it is what lets a caller send the 156 // 48,402-byte chunks nx_content_put offers; raising it would only have moved the guess. 157 // NOT REMOVED FROM THE SIBLING nx_https_post_complete ABOVE, DELIBERATELY: that variant allocates a 158 // FIXED NX_HTTPS_PC_REQ_BUF_BYTES buffer and carries its own inline single-record write, so there 159 // the cap is load-bearing and deleting it would be a buffer overflow. Collapsing that duplicate 160 // record-write into nx_https_req_complete is OWED and named here rather than left as a silent 161 // asymmetry -- a fix that lives in one function and not its sibling is half a fix. 162 163 // DERIVED buffer size -- the request cap the GET family already computes for 164 // path/host/cookie/xhdr, plus this variant's own Content-Type and body. Not a guessed ceiling: 165 // a hand-picked constant here would truncate the request in silence the first time a caller 166 // passed a long jar, which is exactly the class the estate keeps paying for. 167 let req_cap: i64 = nx_http_client_request_cap(path_len, host_len, cookie_len, xhdr_len) 168 + content_type_len + body_len + NX_HTTPS_PC_POST_HDR_SLACK 169 let req: *u8 = sys_mmap(req_cap) 170 let req_len: i64 = nx_http_client_build_request_post_xhdr( 171 path, path_len, 172 host, host_len, 173 content_type, content_type_len, 174 body, body_len, 175 cookie, cookie_len, 176 xhdr, xhdr_len, 177 req 178 ) 179 if req_len <= 0 { return 0 - NX_HTTPS_PC_BUILD_FAIL } 180 return nx_https_req_complete(s, fd, req, req_len, out_buf, out_cap) 181} 182 183// Compile-only smoke. Live exercise belongs to a separate end-to-end 184// test against ACME staging (queued: nx_acme_directory_live_smoke). 185func main() -> i64 { 186 return 0 187}