nx_http_client_gate.nx source
↩ module page · 286 lines · 18979 B
1// nx_http_client_gate.nx -- THE REQUEST BUILDER EVERY ESTATE FETCH CROSSES HAD NO GATE AT ALL.
2// Measured 2026-09-03 with nx_catalog: nx_http_client_gate and nx_httpclient_gate both ABSENT --
3// "no source, no binary, nothing promoted". nx_http_client.nx is imported by nx_https_get_complete,
4// which is imported by nx_https_get, which is what nx_research_fetch and every sovereign fetch ride.
5// An unguarded builder on that path is the widest ungated surface in the fetch lane.
6//
7// WHAT IT PROVES. On 2026-09-03 hc_put_hints was added so the request carries the Chrome client hints
8// and fetch-metadata that match the Chrome-JA3 ClientHello we ALREADY pay for (nx_tls13_chrome_session).
9// The measured cause: fab.com (Cloudflare) answers 403 to the UA-alone profile and 200 to the full
10// profile OVER THE SAME TLS STACK. So the risk this gate exists to hold down is NOT "did we add the
11// headers" -- it is the COHERENCE of the identity we present:
12//
13// * a Chrome UA whose version disagrees with its own sec-ch-ua version is a MISMATCH, and a mismatch
14// is the exact signal anti-bot scoring hunts. T5 compares the two NUMBERS and emits both, so the
15// bar is readable from outside instead of living in a tooth name.
16// * HC_UA_BOT must NEVER acquire Chrome hints. An honest crawler identity that ships sec-ch-ua is no
17// longer honest, and that is a promise to site operators, not a preference. T6/T7 are neg-controls.
18// * a caller that declared its OWN User-Agent must not be contradicted (the 2026-08-25 duplicate-header
19// lesson: THE CALLER WINS). T8 is the neg-control for that.
20//
21// NON-VACUITY. T1-T4 alone would pass for an implementation that emitted the header block to EVERY
22// identity -- which is precisely the honesty regression. The three neg-controls are what make the
23// forward teeth mean anything, and they are named neg-control- so nx_gatelaw_gate's L2 census can see
24// them (a control nobody can find is a control nobody maintains).
25//
26// BITE-PROVEN 2026-09-03 (laptop, admission refused the NAS build): removing the hc_put_hints call site
27// takes it 16/16 GREEN exit 0 -> 11/16 RED exit 1 with exactly the 5 forward teeth failing and all three
28// neg-controls still passing (they assert ABSENCE, which the mutant preserves -- correct discrimination,
29// not vacuity). Restore verified byte-identical, sha256 1952206f85deae97667635964936a64fcabfc5328c47b00bd365805694d4c75e.
30//
31// license_tier: ORIGINAL expect_exit: 0
32import "nx_syscalls.nx"
33import "nx_gate_verdict.nx"
34import "nx_http_client.nx"
35import "nx_https_fetch_lib.nx"
36
37// Historical failure boundary, retained only as a regression-test input.
38const HG_RETIRED_URL_SCRATCH_BYTES: i64=4096
39const HG_ABSENT: i64 = 0 - 1
40const HG_DIGIT_0: i64 = 48
41const HG_DIGIT_9: i64 = 57
42const HG_SCAN_SLACK: i64 = 16
43
44// Length-bounded substring search. nx_cc_find is NUL-TERMINATED and a built request is
45// LENGTH-DELIMITED, so this is a different contract rather than a duplicate ruler: running a
46// NUL-terminated finder over a buffer whose tail is uninitialised is how a gate reads garbage
47// and calls it a pass.
48func hg_find(buf: *u8, n: i64, pat: *u8) -> i64 {
49 var pl: i64 = 0
50 while pat[pl] != (0 as u8) { pl = pl + 1 }
51 if pl == 0 { return HG_ABSENT }
52 var i: i64 = 0
53 while i + pl <= n {
54 var m: i64 = 1
55 var j: i64 = 0
56 while j < pl {
57 if buf[i + j] != pat[j] { m = 0; j = pl } else { j = j + 1 }
58 }
59 if m == 1 { return i }
60 i = i + 1
61 }
62 return HG_ABSENT
63}
64
65func hg_has(buf: *u8, n: i64, pat: *u8) -> i64 {
66 if hg_find(buf, n, pat) >= 0 { return 1 }
67 return 0
68}
69
70func hg_count(buf: *u8, n: i64, pat: *u8) -> i64 {
71 var pl: i64 = 0
72 while pat[pl] != (0 as u8) { pl = pl + 1 }
73 if pl == 0 { return 0 }
74 var c: i64 = 0
75 var i: i64 = 0
76 while i + pl <= n {
77 var m: i64 = 1
78 var j: i64 = 0
79 while j < pl {
80 if buf[i + j] != pat[j] { m = 0; j = pl } else { j = j + 1 }
81 }
82 if m == 1 { c = c + 1; i = i + pl } else { i = i + 1 }
83 }
84 return c
85}
86
87// Find `pat`, then read the first run of decimal digits that follows it within a short slack window.
88// Returns HG_ABSENT when the pattern is missing, so "not found" can never be mistaken for a value.
89func hg_num_after(buf: *u8, n: i64, pat: *u8) -> i64 {
90 let at: i64 = hg_find(buf, n, pat)
91 if at < 0 { return HG_ABSENT }
92 var pl: i64 = 0
93 while pat[pl] != (0 as u8) { pl = pl + 1 }
94 var i: i64 = at + pl
95 var stop: i64 = i + HG_SCAN_SLACK
96 if stop > n { stop = n }
97 while i < stop {
98 let c: i64 = buf[i] as i64
99 if c >= HG_DIGIT_0 {
100 if c <= HG_DIGIT_9 {
101 var v: i64 = 0
102 while i < n {
103 let d: i64 = buf[i] as i64
104 if d < HG_DIGIT_0 { return v }
105 if d > HG_DIGIT_9 { return v }
106 v = v * 10 + (d - HG_DIGIT_0)
107 i = i + 1
108 }
109 return v
110 }
111 }
112 i = i + 1
113 }
114 return HG_ABSENT
115}
116
117
118func hg_resolve(base: *u8,ref: *u8,want: *u8) -> i64 {
119 let cap: i64=_gc_slen(base)+_gc_slen(ref)+1
120 let out: *u8=sys_mmap(cap)
121 let size: *i64=sys_mmap(__size_of(i64)) as *i64
122 let rc: i64=nx_url_resolve(base,_gc_slen(base),ref,_gc_slen(ref),out,cap,size)
123 var ok: i64=(rc==NX_URL_RESOLVE_OK && size[0]==_gc_slen(want)) as i64
124 var i: i64=0
125 while ok==1 && i<size[0] { if out[i]!=want[i] { ok=0 };i=i+1 }
126 sys_munmap(out,cap);sys_munmap(size,__size_of(i64))
127 return ok
128}
129
130func main() -> i64 {
131 let ctr: *i64 = gv_ctr()
132 let plan: *i64=gv_plan_new("browser-emits-sec-ch-ua\nbrowser-emits-sec-ch-ua-platform\nbrowser-emits-sec-fetch-mode-navigate\nbrowser-emits-upgrade-insecure-requests\nua-version-equals-sec-ch-ua-version\nneg-control-bot-emits-no-sec-ch-ua\nneg-control-bot-emits-no-sec-fetch\nneg-control-bot-keeps-nishibot-identity\nneg-control-caller-ua-suppresses-chrome-hints\nneg-control-caller-ua-survives\nbrowser-exactly-one-user-agent\nbot-exactly-one-user-agent\nxhdr-exactly-one-user-agent\nbrowser-terminates-blank-line\nbrowser-fits-declared-cap\nbot-fits-declared-cap\nheader-range-valid\nheader-empty-name-refused\nheader-whitespace-name-refused\nheader-missing-colon-refused\nheader-injected-request-refused\nheader-host-override-refused\nheader-length-override-refused\nheader-transfer-framing-refused\nheader-block-valid\nheader-block-missing-lf-refused\nheader-block-blank-line-refused\nheader-empty-block-compatible\nheader-redirect-same-authority\nheader-redirect-other-host-refused\nheader-redirect-other-port-refused\nheader-redirect-downgrade-refused\nrange-request-exact-header-once\nempty-header-block-preserves-ordinary-request-bytes\nuri-reference-1\nuri-reference-2\nuri-reference-3\nuri-reference-4\nuri-reference-5\nuri-reference-6\nuri-reference-7\nuri-reference-8\nuri-reference-9\nuri-reference-10\nuri-reference-11\nuri-reference-12\nuri-reference-20\nuri-reference-21\nuri-reference-22\nuri-reference-23\nuri-reference-24\nuri-reference-40\nuri-reference-41\nuri-reference-42\nuri-reference-43\nbeach-staged-relative-redirect-resolved\nresolved-relative-redirect-retains-header-authority\nrelative-url-exceeds-retired-scratch-and-stack\nabsolute-url-exceeds-retired-segment-stack\n")
133 gv_head("=== nx_http_client_gate -- identity coherence on the request builder ===" as *u8)
134
135 let path: *u8 = "/" as *u8
136 let host: *u8 = "example.com" as *u8
137 let path_len: i64 = 1
138 let host_len: i64 = 11
139 let cap: i64 = nx_http_client_request_cap(path_len, host_len, 0, 0)
140
141 // ---- BROWSER IDENTITY ----
142 let rb: *u8 = sys_mmap(cap)
143 hc_set_identity(HC_UA_BROWSER)
144 let nb: i64 = nx_http_client_build_request(path, path_len, host, host_len, rb)
145
146 gv_plan_check(plan,"browser-emits-sec-ch-ua", hg_has(rb, nb, "sec-ch-ua: " as *u8), ctr)
147 gv_plan_check(plan,"browser-emits-sec-ch-ua-platform", hg_has(rb, nb, "sec-ch-ua-platform: " as *u8), ctr)
148 gv_plan_check(plan,"browser-emits-sec-fetch-mode-navigate", hg_has(rb, nb, "Sec-Fetch-Mode: navigate" as *u8), ctr)
149 gv_plan_check(plan,"browser-emits-upgrade-insecure-requests", hg_has(rb, nb, "Upgrade-Insecure-Requests: 1" as *u8), ctr)
150
151 // T5 -- THE COHERENCE TOOTH. Both numbers are EMITTED, not just compared, so an outside reader
152 // (or a second method class) can recompute the claim instead of trusting a PASS.
153 let ua_ver: i64 = hg_num_after(rb, nb, "Chrome/" as *u8)
154 let ch_ver: i64 = hg_num_after(rb, nb, "Chromium" as *u8)
155 gv_plan_check(plan,"ua-version-equals-sec-ch-ua-version",(ua_ver==ch_ver) as i64,ctr)
156
157 // ---- NEG-CONTROL: THE HONEST CRAWLER MUST STAY HONEST ----
158 let rn: *u8 = sys_mmap(cap)
159 hc_set_identity(HC_UA_BOT)
160 let nn: i64 = nx_http_client_build_request(path, path_len, host, host_len, rn)
161
162 gv_plan_check(plan,"neg-control-bot-emits-no-sec-ch-ua", 1 - hg_has(rn, nn, "sec-ch-ua" as *u8), ctr)
163 gv_plan_check(plan,"neg-control-bot-emits-no-sec-fetch", 1 - hg_has(rn, nn, "Sec-Fetch-" as *u8), ctr)
164 gv_plan_check(plan,"neg-control-bot-keeps-nishibot-identity", hg_has(rn, nn, "NishiBot" as *u8), ctr)
165
166 // ---- NEG-CONTROL: A CALLER THAT DECLARED ITS OWN IDENTITY IS NOT CONTRADICTED ----
167 let rx: *u8 = sys_mmap(cap + 64)
168 hc_set_identity(HC_UA_BROWSER)
169 let xh: *u8 = "User-Agent: HonestClient/1.0\r\n" as *u8
170 var xl: i64 = 0
171 while xh[xl] != (0 as u8) { xl = xl + 1 }
172 let nx: i64 = nx_http_client_build_request_xhdr(path, path_len, host, host_len, xh, xl, rx)
173
174 gv_plan_check(plan,"neg-control-caller-ua-suppresses-chrome-hints", 1 - hg_has(rx, nx, "sec-ch-ua" as *u8), ctr)
175 gv_plan_check(plan,"neg-control-caller-ua-survives", hg_has(rx, nx, "HonestClient/1.0" as *u8), ctr)
176
177 // ---- WELL-FORMEDNESS: these hold for EVERY identity, so they are asserted on all three ----
178 gv_plan_check(plan,"browser-exactly-one-user-agent",(hg_count(rb,nb,"User-Agent:")==1) as i64,ctr)
179 gv_plan_check(plan,"bot-exactly-one-user-agent",(hg_count(rn,nn,"User-Agent:")==1) as i64,ctr)
180 gv_plan_check(plan,"xhdr-exactly-one-user-agent",(hg_count(rx,nx,"User-Agent:")==1) as i64,ctr)
181
182 // A request that lost its terminating blank line is a request the peer waits forever for.
183 gv_plan_check(plan,"browser-terminates-blank-line", hg_has(rb, nb, "\r\n\r\n" as *u8), ctr)
184
185 // THE BOUND IS OWNED BY nx_http_client_request_cap AND THIS ASSERTS WE STILL FIT INSIDE IT.
186 // Adding headers without moving the cap is exactly how a builder overruns its own buffer, and
187 // the overrun would be silent -- mmap'd slack reads as zero, not as a fault.
188 gv_plan_check(plan,"browser-fits-declared-cap", (nb < cap) as i64, ctr)
189 gv_plan_check(plan,"bot-fits-declared-cap", (nn < cap) as i64, ctr)
190
191
192 gv_plan_check(plan,"header-range-valid",hc_header_line_size("Range: bytes=0-7",_gc_slen("Range: bytes=0-7"))==_gc_slen("Range: bytes=0-7\r\n"),ctr)
193 gv_plan_check(plan,"header-empty-name-refused",hc_header_line_size(": x",_gc_slen(": x"))==HC_HEADER_BAD_NAME,ctr)
194 gv_plan_check(plan,"header-whitespace-name-refused",hc_header_line_size("Bad Name: x",_gc_slen("Bad Name: x"))==HC_HEADER_BAD_NAME,ctr)
195 gv_plan_check(plan,"header-missing-colon-refused",hc_header_line_size("Range",_gc_slen("Range"))==HC_HEADER_BAD_NAME,ctr)
196 gv_plan_check(plan,"header-injected-request-refused",hc_header_line_size("X: x\r\nGET /",_gc_slen("X: x\r\nGET /"))==HC_HEADER_BAD_VALUE,ctr)
197 gv_plan_check(plan,"header-host-override-refused",hc_header_line_size("hOsT: evil",_gc_slen("hOsT: evil"))==HC_HEADER_OWNED_FIELD,ctr)
198 gv_plan_check(plan,"header-length-override-refused",hc_header_line_size("Content-Length: 1",_gc_slen("Content-Length: 1"))==HC_HEADER_OWNED_FIELD,ctr)
199 gv_plan_check(plan,"header-transfer-framing-refused",hc_header_line_size("Transfer-Encoding: chunked",_gc_slen("Transfer-Encoding: chunked"))==HC_HEADER_OWNED_FIELD,ctr)
200 let range_headers: *u8="Range: bytes=0-7\r\n"
201 let range_n: i64=_gc_slen(range_headers)
202 gv_plan_check(plan,"header-block-valid",hf_header_block_valid(range_headers,range_n),ctr)
203 gv_plan_check(plan,"header-block-missing-lf-refused",1-hf_header_block_valid(range_headers,range_n-1),ctr)
204 gv_plan_check(plan,"header-block-blank-line-refused",1-hf_header_block_valid("\r\n",2),ctr)
205 gv_plan_check(plan,"header-empty-block-compatible",hf_header_block_valid(0 as *u8,0),ctr)
206 gv_plan_check(plan,"header-redirect-same-authority",hf_header_redirect_same_origin("https://example.com/a","https://example.com/b"),ctr)
207 gv_plan_check(plan,"header-redirect-other-host-refused",1-hf_header_redirect_same_origin("https://example.com/a","https://example.com.evil/b"),ctr)
208 gv_plan_check(plan,"header-redirect-other-port-refused",1-hf_header_redirect_same_origin("https://example.com/a","https://example.com:8443/b"),ctr)
209 gv_plan_check(plan,"header-redirect-downgrade-refused",1-hf_header_redirect_same_origin("https://example.com/a","http://example.com/b"),ctr)
210 let range_cap: i64=nx_http_client_request_cap(path_len,host_len,0,range_n)
211 let range_req: *u8=sys_mmap(range_cap)
212 let range_len: i64=nx_http_client_build_request_cookie_xhdr(path,path_len,host,host_len,0 as *u8,0,range_headers,range_n,range_req)
213 gv_plan_check(plan,"range-request-exact-header-once",hg_count(range_req,range_len,range_headers)==1,ctr)
214 let plain_req: *u8=sys_mmap(cap)
215 let plain_n: i64=nx_http_client_build_request_cookie_xhdr(path,path_len,host,host_len,0 as *u8,0,0 as *u8,0,plain_req)
216 var identical: i64=(plain_n==nb) as i64
217 var pi: i64=0
218 while pi<plain_n && pi<nb { if plain_req[pi]!=rb[pi] { identical=0 };pi=pi+1 }
219 gv_plan_check(plan,"empty-header-block-preserves-ordinary-request-bytes",identical,ctr)
220
221 gv_plan_check(plan,"uri-reference-1",hg_resolve("http://a/b/c/d;p?q","g:h","g:h"),ctr)
222 gv_plan_check(plan,"uri-reference-2",hg_resolve("http://a/b/c/d;p?q","g","http://a/b/c/g"),ctr)
223 gv_plan_check(plan,"uri-reference-3",hg_resolve("http://a/b/c/d;p?q","./g","http://a/b/c/g"),ctr)
224 gv_plan_check(plan,"uri-reference-4",hg_resolve("http://a/b/c/d;p?q","g/","http://a/b/c/g/"),ctr)
225 gv_plan_check(plan,"uri-reference-5",hg_resolve("http://a/b/c/d;p?q","/g","http://a/g"),ctr)
226 gv_plan_check(plan,"uri-reference-6",hg_resolve("http://a/b/c/d;p?q","//g","http://g"),ctr)
227 gv_plan_check(plan,"uri-reference-7",hg_resolve("http://a/b/c/d;p?q","?y","http://a/b/c/d;p?y"),ctr)
228 gv_plan_check(plan,"uri-reference-8",hg_resolve("http://a/b/c/d;p?q","g?y","http://a/b/c/g?y"),ctr)
229 gv_plan_check(plan,"uri-reference-9",hg_resolve("http://a/b/c/d;p?q","#s","http://a/b/c/d;p?q#s"),ctr)
230 gv_plan_check(plan,"uri-reference-10",hg_resolve("http://a/b/c/d;p?q","g#s","http://a/b/c/g#s"),ctr)
231 gv_plan_check(plan,"uri-reference-11",hg_resolve("http://a/b/c/d;p?q","g?y#s","http://a/b/c/g?y#s"),ctr)
232 gv_plan_check(plan,"uri-reference-12",hg_resolve("http://a/b/c/d;p?q",";x","http://a/b/c/;x"),ctr)
233 gv_plan_check(plan,"uri-reference-20",hg_resolve("http://a/b/c/d;p?q","../g","http://a/b/g"),ctr)
234 gv_plan_check(plan,"uri-reference-21",hg_resolve("http://a/b/c/d;p?q","../","http://a/b/"),ctr)
235 gv_plan_check(plan,"uri-reference-22",hg_resolve("http://a/b/c/d;p?q","../..","http://a/"),ctr)
236 gv_plan_check(plan,"uri-reference-23",hg_resolve("http://a/b/c/d;p?q","../../","http://a/"),ctr)
237 gv_plan_check(plan,"uri-reference-24",hg_resolve("http://a/b/c/d;p?q","../../g","http://a/g"),ctr)
238 gv_plan_check(plan,"uri-reference-40",hg_resolve("https://www.google.com/","/images/logo.png","https://www.google.com/images/logo.png"),ctr)
239 gv_plan_check(plan,"uri-reference-41",hg_resolve("https://www.google.com/","images/logo.png","https://www.google.com/images/logo.png"),ctr)
240 gv_plan_check(plan,"uri-reference-42",hg_resolve("https://www.google.com/","https://other.com/x.png","https://other.com/x.png"),ctr)
241 gv_plan_check(plan,"uri-reference-43",hg_resolve("https://www.google.com/","//cdn.com/a.png","https://cdn.com/a.png"),ctr)
242
243 let redir_response: *u8="HTTP/1.1 301 Moved Permanently\r\nLocation: /releases/beach-contact-16c8ca4d\r\nContent-Length: 0\r\n\r\n"
244 let redir_base: *u8="https://nishifamily.com/releases/beach-contact-16c8ca4d.html"
245 let redir_cap: i64=_gc_slen(redir_base)+_gc_slen(redir_response)+1
246 let redir_out: *u8=sys_mmap(redir_cap)
247 let redir_n: *i64=sys_mmap(__size_of(i64)) as *i64
248 let redir_rc: i64=nx_http_resolve_redirect(redir_response,_gc_slen(redir_response),301,redir_base,_gc_slen(redir_base),redir_out,redir_cap-1,redir_n)
249 redir_out[redir_n[0]]=0 as u8
250 gv_plan_check(plan,"beach-staged-relative-redirect-resolved",(redir_rc==NX_REDIRECT_RESOLVED && hg_has(redir_out,redir_n[0],"https://nishifamily.com/releases/beach-contact-16c8ca4d")==1) as i64,ctr)
251 gv_plan_check(plan,"resolved-relative-redirect-retains-header-authority",hf_header_redirect_same_origin(redir_base,redir_out),ctr)
252 sys_munmap(redir_out,redir_cap);sys_munmap(redir_n,__size_of(i64))
253 // Regression fixture exceeds the former 4096-byte scratch and 256-segment stack.
254 let retired_scratch_bytes: i64=HG_RETIRED_URL_SCRATCH_BYTES
255 let segment_count: i64=retired_scratch_bytes+1
256 let long_n: i64=segment_count*2
257 let long_ref: *u8=sys_mmap(long_n+1)
258 let expected_prefix: *u8="https://example.com/"
259 let prefix_n: i64=_gc_slen(expected_prefix)
260 let long_want: *u8=sys_mmap(prefix_n+long_n+1)
261 var li: i64=0
262 while li<long_n { if li%2==0 { long_ref[li]=97 as u8 } else { long_ref[li]=47 as u8 };li=li+1 }
263 long_ref[long_n]=0 as u8
264 li=0;while li<prefix_n { long_want[li]=expected_prefix[li];li=li+1 }
265 li=0;while li<=long_n { long_want[prefix_n+li]=long_ref[li];li=li+1 }
266 gv_plan_check(plan,"relative-url-exceeds-retired-scratch-and-stack",hg_resolve(expected_prefix,long_ref,long_want),ctr)
267 let absolute_ref: *u8=sys_mmap(long_n+2)
268 absolute_ref[0]=47 as u8
269 li=0;while li<=long_n { absolute_ref[li+1]=long_ref[li];li=li+1 }
270 gv_plan_check(plan,"absolute-url-exceeds-retired-segment-stack",hg_resolve(expected_prefix,absolute_ref,long_want),ctr)
271 sys_munmap(absolute_ref,long_n+2)
272 sys_munmap(long_ref,long_n+1);sys_munmap(long_want,prefix_n+long_n+1)
273
274 // ---- VALUES: a gate that prints only PASS is unfalsifiable from outside (2026-09-03 L4 law) ----
275 gv_values_head()
276 gv_kv("browser_request_bytes" as *u8, nb)
277 gv_kv("bot_request_bytes" as *u8, nn)
278 gv_kv("xhdr_request_bytes" as *u8, nx)
279 gv_kv("declared_cap_bytes" as *u8, cap)
280 gv_kv("browser_minus_bot_bytes" as *u8, nb - nn)
281 gv_kv("ua_version" as *u8, ua_ver)
282 gv_kv("sec_ch_ua_version" as *u8, ch_ver)
283
284 gv_plan_finish(plan,ctr)
285 return gv_verdict("nx_http_client_gate" as *u8, ctr, "identity coherence on the ONE request builder every sovereign fetch crosses" as *u8)
286}