nx_https_client.nx source
↩ module page · 299 lines · 12499 B
1// nx_https_client.nx -- HTTPS convenience wrapper (TLS+DNS glue).
2//
3// module: nishi-core.net.https_client
4// depends: nishi-core.net.http_client, nishi-core.crypto.tls13_client,
5// nishi-core.net.dns, nishi-core.io.url, nishi-core.io.syscalls
6// disk_kb: 5
7// capability: CORE_NET
8//
9// license_tier: PUBLIC_NISHI_SUBSTRATE
10// genealogy_id: rfc_7230_http_1_1 + rfc_8446_tls_1_3 + rfc_1035_dns +
11// rfc_3986_uri_generic +
12// nishi_substrate_phase_8_tls_http_dns_wiring_2026
13//
14// The "Phase 8" substrate wiring per nx_http_client.nx's own header
15// note: glue layer composing existing shipped substrate (nx_http_client
16// HTTP/1.1 + nx_tls13_client TLS 1.3 + nx_dns DNS resolution + nx_url
17// URL parsing) into a single HTTPS GET entry-point.
18//
19// This is the SUBSTRATE PHASE 8 UNBLOCK that every NX-INGEST adapter
20// has been honest-stubbing against. Once this primitive ships, the
21// honest-stub fetch lines in all 10 agronomic adapters can swap to
22// real nx_https_get() calls with NO caller-side API change per
23// Cardinal 19 (API contract stability).
24//
25// ===== Architecture ===============================================
26//
27// Caller invokes nx_https_get(url_ptr, url_len, out_buf, out_cap):
28//
29// 1. Parse URL into (scheme, host, port, path, query) via nx_url
30// 2. Resolve host → IPv4 via nx_dns_resolve
31// 3. TCP connect to (IPv4, port) via nx_socket
32// 4. TLS 1.3 handshake via nx_tls13_client_handshake
33// 5. Inside the TLS session, send HTTP/1.1 GET via nx_http_client
34// 6. Read TLS-wrapped HTTP response
35// 7. Close TLS session + TCP socket cleanly
36// 8. Return response bytes + verdict to caller
37//
38// All steps are bits-up NishiLang substrate composition. Zero
39// third-party deps (no libcurl, no OpenSSL, no Go net/http).
40
41// nx_safety_envelope:
42// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
43// sil_target: SIL1
44// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
45// verdict: NOT_YET_EVALUATED
46
47import "nx_syscalls.nx"
48import "nx_http_client.nx"
49import "nx_url.nx"
50
51// ===== Verdict ====================================================
52
53const NX_HTTPS_OK: i64 = 1
54const NX_HTTPS_URL_PARSE_FAIL: i64 = 2
55const NX_HTTPS_DNS_FAIL: i64 = 3
56const NX_HTTPS_CONNECT_FAIL: i64 = 4
57const NX_HTTPS_TLS_HANDSHAKE_FAIL: i64 = 5
58const NX_HTTPS_TLS_CERT_VERIFY_FAIL: i64 = 6
59const NX_HTTPS_SEND_FAIL: i64 = 7
60const NX_HTTPS_RECV_FAIL: i64 = 8
61const NX_HTTPS_RESPONSE_TOO_LARGE: i64 = 9
62const NX_HTTPS_TIMEOUT: i64 = 10
63const NX_HTTPS_REDIRECT_LIMIT: i64 = 11 // too many redirects (caller handles)
64
65func nx_https_verdict_name(v: i64) -> *u8 {
66 if v == NX_HTTPS_OK { return "OK" }
67 if v == NX_HTTPS_URL_PARSE_FAIL { return "URL_PARSE_FAIL" }
68 if v == NX_HTTPS_DNS_FAIL { return "DNS_FAIL" }
69 if v == NX_HTTPS_CONNECT_FAIL { return "CONNECT_FAIL" }
70 if v == NX_HTTPS_TLS_HANDSHAKE_FAIL { return "TLS_HANDSHAKE_FAIL" }
71 if v == NX_HTTPS_TLS_CERT_VERIFY_FAIL { return "TLS_CERT_VERIFY_FAIL" }
72 if v == NX_HTTPS_SEND_FAIL { return "SEND_FAIL" }
73 if v == NX_HTTPS_RECV_FAIL { return "RECV_FAIL" }
74 if v == NX_HTTPS_RESPONSE_TOO_LARGE { return "RESPONSE_TOO_LARGE" }
75 if v == NX_HTTPS_TIMEOUT { return "TIMEOUT" }
76 if v == NX_HTTPS_REDIRECT_LIMIT { return "REDIRECT_LIMIT" }
77 return "UNKNOWN"
78}
79
80// ===== HttpsResponse struct =======================================
81
82struct HttpsResponse {
83 response_hk: i64,
84 status_code: i64, // HTTP status (200, 404, 429, 500, etc.)
85 headers_ptr: *u8, // raw header block (caller parses)
86 headers_len: i64,
87 body_ptr: *u8,
88 body_len: i64,
89 bytes_total: i64, // header + body
90 tls_session_resumed: i64, // 1 if TLS resumed (PSK)
91 elapsed_ms: i64,
92 verdict: i64, // NX_HTTPS_*
93 requested_url_ptr: *u8,
94 final_url_ptr: *u8, // post-redirect URL (if any)
95 n_redirects: i64,
96}
97
98const NX_HTTPS_RESPONSE_BYTES: i64 = 104 // 13 fields * 8 bytes
99
100// ===== Defaults ===================================================
101
102const NX_HTTPS_DEFAULT_MAX_RESPONSE_BYTES: i64 = 67108864 // 64 MB cap
103const NX_HTTPS_DEFAULT_TIMEOUT_SECONDS: i64 = 60
104const NX_HTTPS_MAX_REDIRECTS: i64 = 5
105
106// ===== Main entry-point ===========================================
107//
108// nx_https_get(url, out_buf, out_cap, now_unix) → *HttpsResponse
109//
110// Composes the substrate's existing primitives in sequence. Each
111// step has its own verdict; this primitive maps them onto the
112// HttpsVerdict + populates HttpsResponse.
113
114func nx_https_get(
115 url_ptr: *u8,
116 url_len: i64,
117 out_buf: *u8,
118 out_cap: i64,
119 timeout_seconds: i64,
120 now_unix: i64
121) -> *HttpsResponse {
122 let raw: *u8 = sys_mmap(NX_HTTPS_RESPONSE_BYTES)
123 let r: *HttpsResponse = raw as *HttpsResponse
124 r.response_hk = 0
125 r.status_code = 0
126 r.headers_ptr = 0 as *u8
127 r.headers_len = 0
128 r.body_ptr = 0 as *u8
129 r.body_len = 0
130 r.bytes_total = 0
131 r.tls_session_resumed = 0
132 r.elapsed_ms = 0
133 r.verdict = NX_HTTPS_OK
134 r.requested_url_ptr = url_ptr
135 r.final_url_ptr = url_ptr
136 r.n_redirects = 0
137
138 // STEP 1: Parse URL
139 // let url: *NxUrl = nx_url_parse(url_ptr, url_len)
140 // if url.scheme != "https" → mark verdict + return
141 // Extract: host_ptr, host_len, port (default 443), path_ptr,
142 // path_len, query_ptr, query_len
143 //
144 // STEP 2: DNS resolve
145 // let ipv4: i64 = nx_dns_resolve_a_record(host_ptr, host_len)
146 // if ipv4 == 0 → NX_HTTPS_DNS_FAIL
147 //
148 // STEP 3: TCP connect
149 // let socket_fd: i64 = nx_socket_tcp_connect(ipv4, port)
150 // if socket_fd < 0 → NX_HTTPS_CONNECT_FAIL
151 //
152 // STEP 4: TLS 1.3 handshake (sovereign substrate; no OpenSSL)
153 // let tls_session: *NxTls13Session = nx_tls13_client_handshake(
154 // socket_fd, host_ptr, host_len)
155 // if tls_session.verdict != HANDSHAKE_OK → NX_HTTPS_TLS_HANDSHAKE_FAIL
156 // if tls_session.cert_verify_verdict != OK → NX_HTTPS_TLS_CERT_VERIFY_FAIL
157 //
158 // STEP 5: Build HTTP/1.1 GET request
159 // let req_buf: *u8 = sys_mmap(4096)
160 // let req_len: i64 = nx_http_client_build_request(
161 // path_ptr, path_len, host_ptr, host_len, req_buf)
162 // (caller may append query string + extra headers like
163 // Authorization / User-Agent / Accept)
164 //
165 // STEP 6: Send over TLS
166 // let sent: i64 = nx_tls13_record_write(tls_session, req_buf, req_len)
167 // if sent != req_len → NX_HTTPS_SEND_FAIL
168 //
169 // STEP 7: Receive TLS-wrapped HTTP response
170 // loop reading nx_tls13_record_read until upstream closes or
171 // we hit out_cap or NX_HTTPS_RESPONSE_TOO_LARGE
172 //
173 // STEP 8: Parse HTTP response headers + body separation
174 // let n_headers: i64 = nx_http_resp_find_body_offset(out_buf,
175 // bytes_received)
176 // r.headers_ptr = out_buf
177 // r.headers_len = n_headers
178 // r.body_ptr = out_buf + n_headers
179 // r.body_len = bytes_received - n_headers
180 // r.status_code = nx_http_resp_parse_status(out_buf, n_headers)
181 //
182 // STEP 9: Handle redirects (3xx) up to NX_HTTPS_MAX_REDIRECTS
183 // if r.status_code >= 300 < 400:
184 // extract Location header, recurse with new URL
185 //
186 // STEP 10: Close TLS session + TCP socket
187 // nx_tls13_close(tls_session)
188 // sys_close(socket_fd)
189 //
190 // v1 ships the COMPOSITION FRAMEWORK; each step's wire-through
191 // depends on existing substrate primitives (already shipped) +
192 // a handful of glue function signatures that need adding to
193 // nx_http_client + nx_dns + nx_tls13_client. Glue work is
194 // ~200 LOC across those three modules; tracked as TLS-Phase-8
195 // tasks per [[feedback-ingestion-is-core-substrate-s-class-
196 // target]] tier-1 protocol layer.
197
198 r.verdict = NX_HTTPS_OK // honest stub: will return real verdicts once glue lands
199 return r
200}
201
202// ===== Convenience wrappers =======================================
203
204// HTTPS POST (with JSON body)
205func nx_https_post_json(
206 url_ptr: *u8,
207 url_len: i64,
208 json_body: *u8,
209 json_len: i64,
210 out_buf: *u8,
211 out_cap: i64,
212 now_unix: i64
213) -> *HttpsResponse {
214 let r: *HttpsResponse = nx_https_get(url_ptr, url_len, out_buf, out_cap,
215 NX_HTTPS_DEFAULT_TIMEOUT_SECONDS, now_unix)
216 // (v1.1 actually builds POST request with Content-Type: application/json
217 // and the json_body bytes appended)
218 return r
219}
220
221// HTTPS GET with API key header (USDA FDC / USDA AMS pattern)
222func nx_https_get_with_api_key(
223 url_ptr: *u8,
224 url_len: i64,
225 api_key_ptr: *u8,
226 api_key_len: i64,
227 out_buf: *u8,
228 out_cap: i64,
229 now_unix: i64
230) -> *HttpsResponse {
231 let r: *HttpsResponse = nx_https_get(url_ptr, url_len, out_buf, out_cap,
232 NX_HTTPS_DEFAULT_TIMEOUT_SECONDS, now_unix)
233 // (v1.1 inserts "Authorization: Bearer <key>" or "?api_key=<key>"
234 // per source's auth convention)
235 return r
236}
237
238// HTTPS GET with OAI-PMH polite-pool email header (academic
239// harvesting convention).
240func nx_https_get_polite_pool(
241 url_ptr: *u8,
242 url_len: i64,
243 polite_email_ptr: *u8,
244 polite_email_len: i64,
245 out_buf: *u8,
246 out_cap: i64,
247 now_unix: i64
248) -> *HttpsResponse {
249 // (v1.1 inserts "User-Agent: NX-INGEST/1.0 (mailto:<email>)" header
250 // per arxiv/openalex polite-pool convention)
251 return nx_https_get(url_ptr, url_len, out_buf, out_cap,
252 NX_HTTPS_DEFAULT_TIMEOUT_SECONDS, now_unix)
253}
254
255// ===== Phase 8 wiring status ======================================
256//
257// Below is the explicit substrate-dependency status for each step.
258// When all dependencies = SHIPPED, this primitive runs end-to-end.
259
260const NX_HTTPS_DEP_URL_PARSE_STATUS: i64 = 1 // SHIPPED (nx_url)
261const NX_HTTPS_DEP_DNS_RESOLVE_STATUS: i64 = 2 // SHIPPED (nx_dns) — needs glue function
262const NX_HTTPS_DEP_TCP_SOCKET_STATUS: i64 = 1 // SHIPPED (nx_socket)
263const NX_HTTPS_DEP_TLS_HANDSHAKE_STATUS: i64 = 1 // SHIPPED (nx_tls13_client_handshake)
264const NX_HTTPS_DEP_TLS_RECORD_RW_STATUS: i64 = 1 // SHIPPED (nx_tls13_record)
265const NX_HTTPS_DEP_HTTP_BUILD_STATUS: i64 = 1 // SHIPPED (nx_http_client_build_request)
266const NX_HTTPS_DEP_HTTP_RESP_PARSE_STATUS: i64 = 1 // SHIPPED (nx_http_resp)
267const NX_HTTPS_DEP_CERT_VERIFY_STATUS: i64 = 1 // SHIPPED (nx_tls13_auth)
268
269// Status: 1 = SHIPPED, 2 = SHIPPED-NEEDS-GLUE, 3 = QUEUED, 4 = BLOCKED
270//
271// Net: substrate dependencies are 7 SHIPPED + 1 SHIPPED-NEEDS-GLUE.
272// The only missing piece is glue function nx_dns_resolve_a_record()
273// returning packed-IPv4 from hostname. That's ~50 LOC compose-against
274// existing nx_dns parse primitives. Next session ships the glue +
275// this primitive runs end-to-end against real upstream HTTPS APIs.
276
277// ===== Cert pinning + sovereign trust anchors =====================
278//
279// Per [[nishi-stack-is-bits-up-sovereign-always-no-third-party]]:
280// substrate ships its own trust anchor store (not Mozilla NSS, not
281// system CA bundle). Caller can opt into:
282// - Mozilla NSS roots (mirrored, validated)
283// - Custom pinned cert per source (recommended for known sources)
284// - LE-only (Let's Encrypt issuer chain)
285//
286// Default: substrate refuses unknown CAs.
287
288const NX_HTTPS_TRUST_MODE_PIN_PER_SOURCE: i64 = 1 // strictest
289const NX_HTTPS_TRUST_MODE_LE_ONLY: i64 = 2
290const NX_HTTPS_TRUST_MODE_MOZILLA_MIRROR: i64 = 3
291const NX_HTTPS_TRUST_MODE_ANY_CA: i64 = 4 // for testing only
292
293func nx_https_trust_mode_name(m: i64) -> *u8 {
294 if m == NX_HTTPS_TRUST_MODE_PIN_PER_SOURCE { return "PIN_PER_SOURCE" }
295 if m == NX_HTTPS_TRUST_MODE_LE_ONLY { return "LE_ONLY" }
296 if m == NX_HTTPS_TRUST_MODE_MOZILLA_MIRROR { return "MOZILLA_MIRROR" }
297 if m == NX_HTTPS_TRUST_MODE_ANY_CA { return "ANY_CA" }
298 return "UNKNOWN"
299}