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}