nx_browse_text.nx source
↩ module page · 184 lines · 6465 B
1// nx_browse_text.nx -- production-shaped browser fetch entry point.
2//
3// Compose: TCP connect → send HTTP GET → drain response → HTTP-parse
4// → HTML→text → caller-owned buffer. This is the actual callable
5// API the sovereign browser exposes for text-mode page fetch.
6//
7// Distinct from nx_https_get (HTTPS path is gated on the X.509
8// chain-validation arc, queued separately). This module ships the
9// PLAINTEXT HTTP/1.1 path because every piece it depends on is
10// already proven (TCP loopback works, nx_http_response_parse passes
11// 6 KATs, nx_html_to_text passes 6 KATs).
12//
13// CAPABILITY_COMPLETENESS: PARTIAL. MISSING_CAPABILITIES:
14// - TLS (separate nx_https_get arc, depends on X.509 chain
15// validation)
16// - Redirect following (3xx Location header)
17// - Chunked transfer dechunking before render
18// - gzip / deflate Content-Encoding
19// - HTTP/2 + HTTP/3
20//
21// Per [[feedback-no-skip-paths-as-error-codes]] these are named
22// follow-on arcs, NOT silent gaps.
23//
24// Built per F7 post-order DFS: callees defined before callers.
25// expect_exit: 0
26// license_tier: ORIGINAL
27
28import "nx_syscalls.nx"
29import "nx_connect.nx" // bounded connect: a raw sys_connect hangs ~127s on a black-holed host
30import "nx_http_response_parse.nx"
31import "nx_html_to_text.nx"
32const NX_MAGIC_4096: i64 = 4096
33
34// ===== Verdicts ===================================================
35
36const NX_BR_OK: i64 = 0
37const NX_BR_SOCKET_FAIL: i64 = 1
38const NX_BR_CONNECT_FAIL: i64 = 2
39const NX_BR_SEND_FAIL: i64 = 3
40const NX_BR_RECV_FAIL: i64 = 4
41const NX_BR_PARSE_FAIL: i64 = 5
42const NX_BR_STATUS_FAIL: i64 = 6
43const NX_BR_OVERFLOW: i64 = 7
44
45// ===== LEAF: address + I/O =======================================
46
47func br_addr_ipv4(out: *u8, a: i64, b: i64, c: i64, d: i64, port: i64) -> i64 {
48 out[0] = 2; out[1] = 0
49 out[2] = (port >> 8) & 0xff
50 out[3] = port & 0xff
51 out[4] = a; out[5] = b; out[6] = c; out[7] = d
52 var i: i64 = 8
53 while i < 16 { out[i] = 0; i = i + 1 }
54 return 16
55}
56
57func br_drain(fd: i64, buf: *u8, cap: i64) -> i64 {
58 var off: i64 = 0
59 var keep: i64 = 1
60 while keep == 1 {
61 if off >= cap { keep = 0 }
62 if keep == 1 {
63 let r: i64 = sys_read(fd, buf + off, cap - off)
64 if r <= 0 { keep = 0 } else { off = off + r }
65 }
66 }
67 return off
68}
69
70// Append "GET <path> HTTP/1.1\r\nHost: <host>\r\nConnection: close\r\n\r\n"
71// into req. Returns request length.
72func br_build_get(
73 path: *u8, path_len: i64,
74 host: *u8, host_len: i64,
75 req: *u8
76) -> i64 {
77 var p: i64 = 0
78 req[p]=0x47; p=p+1 // G
79 req[p]=0x45; p=p+1 // E
80 req[p]=0x54; p=p+1 // T
81 req[p]=0x20; p=p+1 // ' '
82 var i: i64 = 0
83 while i < path_len { req[p]=path[i]; p=p+1; i=i+1 }
84 req[p]=0x20; p=p+1
85 req[p]=0x48; p=p+1 // H
86 req[p]=0x54; p=p+1 // T
87 req[p]=0x54; p=p+1 // T
88 req[p]=0x50; p=p+1 // P
89 req[p]=0x2f; p=p+1 // /
90 req[p]=0x31; p=p+1 // 1
91 req[p]=0x2e; p=p+1 // .
92 req[p]=0x31; p=p+1 // 1
93 req[p]=0x0d; p=p+1
94 req[p]=0x0a; p=p+1
95 req[p]=0x48; p=p+1 // H
96 req[p]=0x6f; p=p+1 // o
97 req[p]=0x73; p=p+1 // s
98 req[p]=0x74; p=p+1 // t
99 req[p]=0x3a; p=p+1 // :
100 req[p]=0x20; p=p+1
101 var j: i64 = 0
102 while j < host_len { req[p]=host[j]; p=p+1; j=j+1 }
103 req[p]=0x0d; p=p+1
104 req[p]=0x0a; p=p+1
105 // "Connection: close\r\n"
106 req[p]=0x43; p=p+1; req[p]=0x6f; p=p+1; req[p]=0x6e; p=p+1
107 req[p]=0x6e; p=p+1; req[p]=0x65; p=p+1; req[p]=0x63; p=p+1
108 req[p]=0x74; p=p+1; req[p]=0x69; p=p+1; req[p]=0x6f; p=p+1
109 req[p]=0x6e; p=p+1; req[p]=0x3a; p=p+1; req[p]=0x20; p=p+1
110 req[p]=0x63; p=p+1; req[p]=0x6c; p=p+1; req[p]=0x6f; p=p+1
111 req[p]=0x73; p=p+1; req[p]=0x65; p=p+1
112 req[p]=0x0d; p=p+1; req[p]=0x0a; p=p+1
113 req[p]=0x0d; p=p+1; req[p]=0x0a; p=p+1
114 return p
115}
116
117// ===== Mid: fetch + parse + render ===============================
118
119// Connect, send GET, drain. Returns bytes received or negative
120// verdict. *socket_out gets the closed fd (informational).
121func br_fetch_raw(
122 addr: *u8,
123 path: *u8, path_len: i64,
124 host: *u8, host_len: i64,
125 out: *u8, out_cap: i64,
126 verdict_out: *i64
127) -> i64 {
128 verdict_out[0] = NX_BR_OK
129 let fd: i64 = sys_socket(AF_INET, SOCK_STREAM, 0)
130 if fd < 0 { verdict_out[0] = NX_BR_SOCKET_FAIL; return 0 - 1 }
131 if nx_connect_bounded(fd, addr, 16, NX_CONN_DEFAULT_MS) < 0 {
132 sys_close(fd); verdict_out[0] = NX_BR_CONNECT_FAIL; return 0 - 1
133 }
134 let req: *u8 = sys_mmap(NX_MAGIC_4096)
135 let req_len: i64 = br_build_get(path, path_len, host, host_len, req)
136 let wr: i64 = sys_write(fd, req, req_len)
137 if wr != req_len {
138 sys_close(fd); verdict_out[0] = NX_BR_SEND_FAIL; return 0 - 1
139 }
140 let got: i64 = br_drain(fd, out, out_cap)
141 sys_close(fd)
142 if got <= 0 { verdict_out[0] = NX_BR_RECV_FAIL; return 0 - 1 }
143 return got
144}
145
146// Parse response in resp[0..resp_len], render body to out, return
147// rendered length or negative verdict. *status_out gets HTTP code.
148func br_render_response(
149 resp: *u8, resp_len: i64,
150 out: *u8, out_cap: i64,
151 status_out: *i64, verdict_out: *i64
152) -> i64 {
153 verdict_out[0] = NX_BR_OK
154 status_out[0] = 0
155 let r: *i64 = nx_http_resp_alloc()
156 let v: i64 = nx_http_response_parse(resp, resp_len, r)
157 if v != NX_HTTP_RESP_OK { verdict_out[0] = NX_BR_PARSE_FAIL; return 0 - 1 }
158 status_out[0] = r[1]
159 if r[7] <= 0 { return 0 } // empty body OK; render nothing
160 let m: i64 = nx_html_to_text(resp + r[6], r[7], out, out_cap)
161 if m < 0 { verdict_out[0] = NX_BR_OVERFLOW; return 0 - 1 }
162 return m
163}
164
165// ===== High: nx_browse_text ======================================
166
167// Top-level: fetch + render in one call. Returns rendered length or
168// negative verdict. Caller owns out and resp_scratch buffers.
169//
170// scratch must be at least 4096 bytes (raw HTTP response cap).
171// out is the rendered-text destination.
172func nx_browse_text(
173 addr: *u8,
174 path: *u8, path_len: i64,
175 host: *u8, host_len: i64,
176 scratch: *u8, scratch_cap: i64,
177 out: *u8, out_cap: i64,
178 status_out: *i64, verdict_out: *i64
179) -> i64 {
180 let got: i64 = br_fetch_raw(addr, path, path_len, host, host_len,
181 scratch, scratch_cap, verdict_out)
182 if got < 0 { return 0 - 1 }
183 return br_render_response(scratch, got, out, out_cap, status_out, verdict_out)
184}