code wiki / (root) / nx_https_client.nx

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}