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}