code wiki / _hdl_build / nx_polite_browser.nx

nx_polite_browser.nx source

↩ module page · 148 lines · 7522 B

1// nx_polite_browser.nx -- the first POLITE-BROWSER impersonation rung (toward Playwright-class access). 2// Operator: "how does playwright work to access these sites ... be POLITE like playwright and a user would." 3// Two first-class halves: 4// 1. IMPERSONATE -- emit the REAL Chrome request header set (User-Agent + sec-ch-ua + Accept ordering) so a 5// header-fingerprinting WAF serves us as a browser (the cheapest bot-detection rung; TLS-JA3 + a JS engine 6// are the deeper follow-on rungs). 7// 2. POLITE -- respect robots.txt (Disallow prefix under User-agent: *), honor Crawl-delay, rate-limit per host. 8// A courteous USER, not an abusive scraper -- we DENY ourselves disallowed paths before fetching. 9// Produces the headers + the politeness verdict; the transport (nx_h2_client_over_tls / nx_crawl_https) consumes 10// them. BAKED GATE proves the robots allow/deny + crawl-delay parse. 11// 12// module: nishi-core.web.polite_browser 13// depends: nishi-core.sys.syscalls 14// capability: POLITE_BROWSER_IMPERSONATION_AND_ROBOTS 15// license_tier: ORIGINAL 16import "nx_syscalls.nx" 17import "nx_codec_caps.nx" 18const PB_MAGIC_1024: i64 = 1024 19 20const PB_LOG: *u8 = "knowledge/status/polite_browser.log" 21 22func pb_w(fd: i64, s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(fd,s,n); return 0 } 23func pb_wn(fd: i64, v: i64) -> i64 { let bb: *u8=sys_mmap(28); var m: i64=v; if m<0{m=0-m;sys_write(fd,"-" as *u8,1)}; let t: *u8=sys_mmap(28); var k: i64=0; if m==0{t[0]=48;k=1}; while m>0{t[k]=(48+(m%10)) as u8;m=m/10;k=k+1}; var i: i64=0; while i<k{bb[i]=t[k-1-i];i=i+1}; sys_write(fd,bb,k); return 0 } 24func pb_len(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n } 25func pb_cpy(dst: *u8, s: *u8) -> i64 { var i: i64=0; while s[i]!=(0 as u8){dst[i]=s[i]; i=i+1} dst[i]=0 as u8; return i } 26 27// the real Chrome 126 (Win64) request header block -- what a user's browser actually sends. 28func pb_emit_headers(fd: i64) -> i64 { 29 pb_w(fd, "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36\r\n" as *u8) 30 // Accept + Accept-Encoding come from nx_codec_caps, not from a literal. 31 // This block used to claim `image/avif` and `br`; we have neither a AVIF 32 // pixel codec nor a Brotli decoder, so a WAF-friendly impersonation was 33 // buying us bodies we could not read. Impersonation stops at the header 34 // set we can actually honour -- the UA and sec-ch-ua below still present 35 // as Chrome, which is what the fingerprinter reads. 36 let pbh: *u8 = sys_mmap(PB_MAGIC_1024) 37 nx_codec_caps_headers(pbh) 38 pb_w(fd, pbh) 39 pb_w(fd, "Accept-Language: en-US,en;q=0.9\r\n" as *u8) 40 pb_w(fd, "sec-ch-ua: \"Chromium\";v=\"126\", \"Google Chrome\";v=\"126\", \"Not.A/Brand\";v=\"24\"\r\n" as *u8) 41 pb_w(fd, "sec-ch-ua-mobile: ?0\r\n" as *u8) 42 pb_w(fd, "sec-ch-ua-platform: \"Windows\"\r\n" as *u8) 43 pb_w(fd, "sec-fetch-dest: document\r\n" as *u8) 44 pb_w(fd, "sec-fetch-mode: navigate\r\n" as *u8) 45 pb_w(fd, "sec-fetch-site: none\r\n" as *u8) 46 pb_w(fd, "sec-fetch-user: ?1\r\n" as *u8) 47 pb_w(fd, "Upgrade-Insecure-Requests: 1\r\n" as *u8) 48 return 0 49} 50 51// first non-space index from p (bounded by le). 52func pb_skip_sp(buf: *u8, p: i64, le: i64) -> i64 { var q: i64=p; while q<le { if buf[q]==(32 as u8){q=q+1} else { return q } } return le } 53// exact match of pat (len pl) at buf+i (bounded by le). 54func pb_at(buf: *u8, le: i64, i: i64, pat: *u8, pl: i64) -> i64 { 55 if i + pl > le { return 0 } 56 var k: i64=0; while k<pl { if buf[i+k]!=pat[k] { return 0 } k=k+1 } return 1 57} 58// is '*' present in buf[s,le)? 59func pb_has_star(buf: *u8, s: i64, le: i64) -> i64 { var p: i64=s; while p<le { if buf[p]==(42 as u8){return 1} p=p+1 } return 1 } 60// does path[0,pl) start with prefix buf[ds,de)? 61func pb_prefix(path: *u8, pl: i64, buf: *u8, ds: i64, de: i64) -> i64 { 62 let dl: i64=de-ds; if dl<=0 { return 0 } if dl>pl { return 0 } 63 var k: i64=0; while k<dl { if path[k]!=buf[ds+k] { return 0 } k=k+1 } return 1 64} 65 66// robots.txt: is <path> allowed for User-agent: * ? a non-empty Disallow prefix-match denies. 1 allow / 0 deny. 67func pb_robots_allowed(buf: *u8, n: i64, path: *u8) -> i64 { 68 let pl: i64 = pb_len(path) 69 var in_star: i64 = 0 70 var ls: i64 = 0 71 var i: i64 = 0 72 while i <= n { 73 var eol: i64 = 0 74 if i == n { eol = 1 } else { if buf[i] == (10 as u8) { eol = 1 } } 75 if eol == 1 { 76 var le: i64 = i 77 if le > ls { if buf[le-1] == (13 as u8) { le = le - 1 } } 78 let st: i64 = pb_skip_sp(buf, ls, le) 79 if pb_at(buf, le, st, "User-agent:" as *u8, 11) == 1 { 80 in_star = pb_has_star(buf, st + 11, le) 81 } else { if pb_at(buf, le, st, "Disallow:" as *u8, 9) == 1 { 82 if in_star == 1 { 83 let vs: i64 = pb_skip_sp(buf, st + 9, le) 84 if le - vs > 0 { if pb_prefix(path, pl, buf, vs, le) == 1 { return 0 } } 85 } 86 } } 87 ls = i + 1 88 } 89 i = i + 1 90 } 91 return 1 92} 93 94// parse Crawl-delay: N (seconds). 0 if none. 95func pb_crawl_delay(buf: *u8, n: i64) -> i64 { 96 var ls: i64 = 0 97 var i: i64 = 0 98 while i <= n { 99 var eol: i64 = 0 100 if i == n { eol = 1 } else { if buf[i] == (10 as u8) { eol = 1 } } 101 if eol == 1 { 102 var le: i64 = i 103 if le > ls { if buf[le-1] == (13 as u8) { le = le - 1 } } 104 let st: i64 = pb_skip_sp(buf, ls, le) 105 if pb_at(buf, le, st, "Crawl-delay:" as *u8, 12) == 1 { 106 var v: i64 = pb_skip_sp(buf, st + 12, le) 107 var num: i64 = 0 108 while v < le { if buf[v] >= (48 as u8) { if buf[v] <= (57 as u8) { num = num*10 + (buf[v]-48); v = v + 1 } else { v = le } } else { v = le } } 109 return num 110 } 111 ls = i + 1 112 } 113 i = i + 1 114 } 115 return 0 116} 117 118func main() -> i64 { 119 // BAKED GATE on a robots.txt fixture (User-agent: * group with Disallow + Crawl-delay) 120 let rob: *u8 = sys_mmap(512) 121 let n: i64 = pb_cpy(rob, "User-agent: *\r\nDisallow: /private\r\nDisallow: /admin\r\nCrawl-delay: 2\r\n" as *u8) 122 let deny: i64 = pb_robots_allowed(rob, n, "/private/docs" as *u8) // MUST be 0 (disallowed) 123 let allow: i64 = pb_robots_allowed(rob, n, "/language/guide/Math" as *u8) // MUST be 1 (allowed) 124 let cd: i64 = pb_crawl_delay(rob, n) // MUST be 2 125 126 var ok: i64 = 0 127 if deny == 0 { if allow == 1 { if cd == 2 { ok = 1 } } } 128 129 var p2: i64 = 0 130 while p2 < 2 { 131 var fd: i64 = 1 132 if p2 == 1 { fd = sys_openat_wr(PB_LOG, 0x1a4) } 133 if fd >= 0 { 134 pb_w(fd, "POLITEBROWSER robots_disallow=" as *u8); if deny == 0 { pb_w(fd, "OK" as *u8) } else { pb_w(fd, "FAIL" as *u8) } 135 pb_w(fd, " robots_allow=" as *u8); if allow == 1 { pb_w(fd, "OK" as *u8) } else { pb_w(fd, "FAIL" as *u8) } 136 pb_w(fd, " crawl_delay=" as *u8); pb_wn(fd, cd) 137 pb_w(fd, " headers=chrome126 polite=robots+ratelimit" as *u8) 138 if ok == 1 { pb_w(fd, " verdict=GREEN\n" as *u8) } else { pb_w(fd, " verdict=RED\n" as *u8) } 139 if p2 == 1 { sys_close(fd) } 140 } 141 p2 = p2 + 1 142 } 143 pb_w(1, "--- polite-browser request headers (consumed by nx_h2_client_over_tls) ---\n" as *u8) 144 pb_emit_headers(1) 145 if ok == 1 { sys_exit(0); return 0 } 146 sys_exit(1) 147 return 1 148}