code wiki / (root) / nx_https_get_cli.nx

nx_https_get_cli.nx source

↩ module page · 181 lines · 9784 B

1// nx_https_get_cli.nx -- the OPERATOR/MCP-facing sovereign HTTPS fetch (replaces WebFetch + curl). 2// nx_https_get <url> [connect-host:port] 3// <url> fetched over OUR TLS 1.3 + Mozilla trust store; raw response (status+headers+body) to stdout. 4// [connect-host:port] OPTIONAL connect override (curl --connect-to): open the TCP+TLS to THIS endpoint while 5// keeping SNI + Host + cert-name = the URL's host. Lets us fetch our OWN vhosts straight 6// from the sovereign edge (sites.elf 127.0.0.1:8443), bypassing the DSM nginx that also 7// squats :443 for unclaimed SNIs (andelinwest.com internally hit DSM's self-signed cert). 8// The tools daemon fork-execs this on a GREEN tool_allowlist.conf row, cap-gated to nx_https_get, and returns 9// stdout as the tools/call result. Uses PRODUCTION entropy (nx_csprng_fill from /dev/urandom). license_tier: ORIGINAL 10import "nx_syscalls.nx" 11import "nx_csprng.nx" 12import "nx_x509_trust_store.nx" 13import "nx_trust_store_load_from_certdata.nx" 14import "nx_tls13_client_validate_certificate.nx" 15import "nx_tls13_client_session_run.nx" 16import "nx_tls13_chrome_session.nx" // Chrome-JA3 ClientHello runner (beats anti-bot CDN TLS walls; inherits the recv_hs reassembly fix) 17import "nx_https_url_for_fetch.nx" 18import "nx_https_url_connect.nx" 19import "nx_https_get_complete.nx" 20import "nx_tls_cert_cache.nx" 21import "nx_https_fetch_lib.nx" // the shared fetch composition (one implementation, shared with nx_mvault_fetch) 22const HGC_MAGIC_4194304: i64 = 4194304 23const HGC_MAGIC_65535: i64 = 65535 24 25const HGC_CERTDATA: *u8 = "data/mozilla_certdata.txt" as *u8 26// 32 MiB. Was 4 MiB, which REFUSED a 5 MB asset -- nx_https_get_cli2 hit exactly that on 27// 2026-08-04 and raised ITS OWN copy of this constant. The sibling binary never got the fix, 28// which is precisely how two fetchers drift into neither being usable for everything. 29const HGC_OUTCAP: i64 = 33554432 30 31func hgc_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 32func hgc_streq(a: *u8, b: *u8) -> i64 { 33 var i: i64 = 0 34 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 } 35 if b[i] != (0 as u8) { return 0 } 36 return 1 37} 38func hgc_put(s: *u8) -> i64 { sys_write(1, s, hgc_slen(s)); return 0 } 39func hgc_putn(v: i64) -> i64 { 40 let b: *u8 = sys_mmap(24) 41 var m: i64 = v 42 if m == 0 { b[0] = 48 as u8; sys_write(1, b, 1); return 0 } 43 if m < 0 { sys_write(1, "-" as *u8, 1); m = 0 - m } 44 var nd: i64 = 0 45 var t: i64 = m 46 while t > 0 { nd = nd + 1; t = t / 10 } 47 var i: i64 = nd - 1 48 while i >= 0 { b[i] = (48 + (m % 10)) as u8; m = m / 10; i = i - 1 } 49 sys_write(1, b, nd) 50 return 0 51} 52 53func main(argc: i64, argv: *i64) -> i64 { 54 if argc < 2 { hgc_put("ERROR: usage: nx_https_get <url> [connect-host:port] [--body | --headers] [--raw] [--hdr 'Name: value']\n" as *u8); return 2 } 55 let url: *u8 = argv[1] as *u8 56 let now: i64 = sys_now_realtime_sec() 57 58 // ---- trust store (loaded once; the fetch composition now lives in 59 // nx_https_fetch_lib so the album downloader shares ONE implementation 60 // instead of duplicating ~90 lines of crypto setup -- 2026-07-31) ---- 61 let store_i: i64 = hf_store_load() 62 if store_i <= 0 { hgc_put("ERROR: trust-store load failed (data/mozilla_certdata.txt on daemon CWD?)\n" as *u8); return 3 } 63 64 // Flags are POSITION-INDEPENDENT so a flag may precede or follow the connect override. 65 // Parsing argv[2] positionally made `nx_https_get <url> --body` fail as a malformed ip:port -- 66 // an error message pointing at the wrong argument entirely. 67 // --body : emit the entity body only (headers stripped). Accepted for parity with 68 // nx_https_get_cli2 so repointing the tool rows here breaks no existing caller. 69 // --raw : emit the true WIRE bytes, transport coding intact (the old default, kept because 70 // a debugging client genuinely needs it -- rule 25). 71 var cip: i64 = 0 72 var cport: i64 = 0 73 var body_only: i64 = 0 74 var headers_only: i64 = 0 75 var want_raw: i64 = 0 76 var xhdr: *u8=0 as *u8 77 var xhdr_len: i64=0 78 let ipbox: *i64 = sys_mmap(8) as *i64 79 let portbox: *i64 = sys_mmap(8) as *i64 80 var ai: i64 = 2 81 while ai < argc { 82 let a: *u8 = argv[ai] as *u8 83 if hgc_streq(a,"--hdr")==1 { 84 if ai+1>=argc { hgc_put("ERROR: --hdr requires Name: value; no request sent\n");return 2 } 85 let value: *u8=argv[ai+1] as *u8 86 let vn: i64=hgc_slen(value) 87 let need: i64=hc_header_line_size(value,vn) 88 if need<0 { hgc_put("ERROR: request header rejected code=");hgc_putn(need);hgc_put("; invalid name/value or builder-owned framing/authority; no request sent\n");return 2 } 89 let next: *u8=sys_mmap(xhdr_len+need) 90 if (next as i64)<=0 { hgc_put("ERROR: request-header allocation failed; no request sent\n");return 3 } 91 var xi: i64=0 92 while xi<xhdr_len { next[xi]=xhdr[xi];xi=xi+1 } 93 var vi: i64=0 94 while vi<vn { next[xi]=value[vi];xi=xi+1;vi=vi+1 } 95 next[xi]=13 as u8;next[xi+1]=10 as u8 96 if xhdr_len>0 { sys_munmap(xhdr,xhdr_len) } 97 xhdr=next;xhdr_len=xhdr_len+need 98 ai=ai+1 99 } else { 100 if hgc_streq(a, "--headers" as *u8) == 1 { headers_only = 1 } else { 101 if hgc_streq(a, "--body" as *u8) == 1 { body_only = 1 } else { 102 if hgc_streq(a, "--raw" as *u8) == 1 { want_raw = 1 } else { 103 if hgc_parse_ipport(a, ipbox, portbox) != 1 { hgc_put("ERROR: bad connect-override (want a.b.c.d:port)\n" as *u8); return 2 } 104 cip = ipbox[0] 105 cport = portbox[0] 106 } } } } 107 ai = ai + 1 108 } 109 110 if body_only == 1 { if headers_only == 1 { 111 hgc_put("ERROR: --body and --headers are mutually exclusive; no request sent\n" as *u8) 112 return 2 113 } } 114 // Header inspection retains the response metadata before transport decoding rewrites it. 115 // This remains a GET and consumes its body; it reduces output, not network traffic. 116 var decode: i64 = 1 117 if headers_only == 1 { decode = 0 } 118 if want_raw == 1 { decode = 0 } 119 let out: *u8 = sys_mmap(HGC_OUTCAP) 120 let n: i64 = hf_fetch_mode_headers(store_i,url,cip,cport,out,HGC_OUTCAP,decode,xhdr,xhdr_len) 121 if xhdr_len>0 { sys_munmap(xhdr,xhdr_len) } 122 if n==HF_ERR_HEADERS { hgc_put("ERROR: malformed request-header block; no request sent\n");return 2 } 123 if n==HF_ERR_HEADER_REDIRECT { hgc_put("ERROR: redirect changes authority for explicit headers; destination not requested; inspect Location and authorize its headers separately\n");return 10 } 124 if n == HF_ERR_URL { hgc_put("ERROR: bad url\n" as *u8); return 2 } 125 if n == HF_ERR_CONNECT { hgc_put("ERROR: connect failed\n" as *u8); return 3 } 126 if n == HF_ERR_TLS { hgc_put("ERROR: TLS handshake failed\n" as *u8); return 4 } 127 if n == HF_ERR_HTTP { hgc_put("ERROR: HTTP fetch failed\n" as *u8); return 5 } 128 // Decode failures are LOUD and distinct. Emitting still-compressed bytes from a path whose 129 // contract is "the document" is success-with-the-wrong-bytes -- the exact shape that once 130 // poisoned the search index by tokenizing compressed bytes as text. 131 if n == HF_DEC_ECODING { hgc_put("ERROR: Content-Encoding we cannot decode (br/zstd/stacked) -- rerun with --raw for the wire bytes\n" as *u8); return 9 } 132 if n == HF_DEC_CHUNK { hgc_put("ERROR: malformed chunked framing -- refusing to emit a partial document; --raw for the wire bytes\n" as *u8); return 7 } 133 if n == HF_DEC_INFLATE { hgc_put("ERROR: Content-Encoding inflate failed -- refusing to emit compressed bytes as the document; --raw for the wire bytes\n" as *u8); return 8 } 134 if n == HF_DEC_CAPACITY { hgc_put("ERROR: decoded response exceeds caller buffer budget="); hgc_putn(HGC_OUTCAP); hgc_put("; no decoded document emitted\n"); return 11 } 135 if n < 0 { hgc_put("ERROR: fetch failed\n" as *u8); return 5 } 136 if headers_only == 1 { 137 let he: i64 = hf_body_off(out, n) 138 if he < 0 { hgc_put("ERROR: --headers: no CRLFCRLF header terminator; refusing partial headers\n" as *u8); return 6 } 139 sys_write(1, out, he) 140 return 0 141 } 142 if body_only == 1 { 143 let he: i64 = hf_body_off(out, n) 144 if he < 0 { hgc_put("ERROR: --body: no CRLFCRLF header terminator; refusing to guess where the body starts\n" as *u8); return 6 } 145 sys_write(1, out + he, n - he) 146 return 0 147 } 148 sys_write(1, out, n) 149 return 0 150} 151 152 153// parse "a.b.c.d:port" -> ip_out (big-endian packed u32, e.g. 127.0.0.1 -> 0x7F000001) + port_out. 1 ok / 0 bad. 154func hgc_parse_ipport(s: *u8, ip_out: *i64, port_out: *i64) -> i64 { 155 var packed: i64 = 0 156 var val: i64 = 0 157 var nocts: i64 = 0 158 var port: i64 = 0 159 var indots: i64 = 1 160 var i: i64 = 0 161 while s[i] != (0 as u8) { 162 let c: i64 = s[i] as i64 163 if indots == 1 { 164 if c == 46 { packed = (packed << 8) | (val & 0xff); nocts = nocts + 1; val = 0 } 165 else { if c == 58 { packed = (packed << 8) | (val & 0xff); nocts = nocts + 1; val = 0; indots = 0 } 166 else { if c < 48 { return 0 } if c > 57 { return 0 } val = val * 10 + (c - 48) } } 167 } else { 168 if c < 48 { return 0 } 169 if c > 57 { return 0 } 170 port = port * 10 + (c - 48) 171 } 172 i = i + 1 173 } 174 if indots == 1 { return 0 } // no ':' -> no port given 175 if nocts != 4 { return 0 } // need exactly 4 octets 176 if port <= 0 { return 0 } 177 if port > HGC_MAGIC_65535 { return 0 } 178 ip_out[0] = packed 179 port_out[0] = port 180 return 1 181}