nx_http_proxy.nx source
↩ module page · 291 lines · 14292 B
1// nx_http_proxy.nx -- HTTP upstream forwarding (replaces nginx proxy_pass).
2//
3// module: nishi-core.net.http_proxy
4// depends: nishi-core.perception.profile + nishi-core.io.syscalls
5// disk_kb: 6
6// capability: NET
7// wired_status: PARTIAL_WIRED
8//
9// MISSING_CAPABILITIES:
10// - REQUEST_BUFFER_FORWARDING (read request body from client conn, send
11// to upstream; bounded by payload_size_limit_bytes from nx_deploy
12// manifest)
13// - RESPONSE_STREAM_BACK (streaming forward: upstream chunk -> client;
14// no full-buffering required for typical HTTP responses)
15// - WS_BIDI_PUMP (WebSocket frames bidirectional; upgrade detection +
16// transparent pump; required for /video room WS chat)
17// - HEADER_REWRITE (X-Forwarded-For / X-Real-IP / X-Forwarded-Proto /
18// Host preservation per nginx-compatible rules)
19// - UPSTREAM_KEEPALIVE_POOL (reuse upstream TCP connections for HTTP/1.1
20// keepalive; avoids per-request connect overhead)
21// - PAYLOAD_SIZE_ENFORCE (reject at proxy layer with 413 when request
22// body exceeds declared limit; replaces nginx client_max_body_size)
23// - PROXY_TIMEOUT (configurable connect / send / read timeouts per
24// nginx proxy_connect_timeout / proxy_send_timeout / proxy_read_timeout)
25//
26// license_tier: PUBLIC_NISHI_SUBSTRATE
27// genealogy_id: feedback-no-temporary-third-party-scaffolding-when-substrate-arc-exists_2026 +
28// feedback-bits-up-exceed-never-match +
29// nx_http_server (FULLY_WIRED per status doc) +
30// nx_tls13_server (shipped per nx_tls13_loopback_test KAT) +
31// nx_socket (FULLY_WIRED) +
32// rfc_7230_http_1_1 (math + spec; not adoption)
33//
34// SCAFFOLDING_REPLACED_BY: this primitive IS the Nishi-native replacement
35// for nginx's proxy_pass / proxy_http_version / proxy_set_header chain.
36// When wired + composed with nx_http_server, the nginx /video/ location
37// block becomes a single nx_http_proxy_route_register call. At that
38// point the /etc/nginx/sites-enabled/server.NishiServices.conf /video/
39// block can be REMOVED entirely.
40//
41// Replaces specifically (from existing server.NishiServices.conf):
42// location /video/ {
43// proxy_pass http://127.0.0.1:8282/;
44// proxy_http_version 1.1;
45// proxy_set_header Upgrade $http_upgrade;
46// proxy_set_header Connection $connection_upgrade;
47// proxy_set_header Host $host;
48// proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
49// proxy_set_header X-Forwarded-Proto $scheme;
50// proxy_read_timeout 3600s;
51// proxy_send_timeout 3600s;
52// proxy_connect_timeout 5s;
53// client_max_body_size 32m;
54// }
55//
56// Reuse set: every Nishi public endpoint that proxies to a backend daemon
57// — /video/ -> nishi_video_room :8282, /telemetry/ -> :8215, /relay/ ->
58// :8210, /_diag/ -> :8211, /minecraftclone/ -> :8204, /matrix/ -> :8008,
59// /chat/ -> :3100, every future Nishi-hosted app.
60
61import "nx_syscalls.nx"
62import "nx_connect.nx" // bounded connect: a raw sys_connect hangs ~127s on a black-holed host
63import "nx_perceptual_profile.nx"
64const NX_MAGIC_8282: i64 = 8282
65const NX_MAGIC_65536: i64 = 65536
66
67// ===== Proxy mode sealed enum ====================================
68//
69// HTTP/1.1 vs WebSocket vs HTTP/2 vs HTTP/3. Substrate dispatches the
70// appropriate pump per mode.
71
72const NX_PROXY_MODE_HTTP_1_1: i64 = 1 // request/response, keepalive ok
73const NX_PROXY_MODE_HTTP_1_1_WS: i64 = 2 // WebSocket upgrade
74const NX_PROXY_MODE_HTTP_2: i64 = 3 // multiplexed; queued
75const NX_PROXY_MODE_HTTP_3_QUIC: i64 = 4 // QUIC; queued behind nx_quic arc
76const NX_PROXY_MODE_RAW_TCP_TUNNEL: i64 = 5 // for CONNECT method
77const NX_PROXY_MODE_SSE: i64 = 6 // text/event-stream long-poll
78
79func nx_proxy_mode_name(m: i64) -> *u8 {
80 if m == NX_PROXY_MODE_HTTP_1_1 { return "HTTP_1_1" }
81 if m == NX_PROXY_MODE_HTTP_1_1_WS { return "HTTP_1_1_WS" }
82 if m == NX_PROXY_MODE_HTTP_2 { return "HTTP_2" }
83 if m == NX_PROXY_MODE_HTTP_3_QUIC { return "HTTP_3_QUIC" }
84 if m == NX_PROXY_MODE_RAW_TCP_TUNNEL { return "RAW_TCP_TUNNEL" }
85 if m == NX_PROXY_MODE_SSE { return "SSE" }
86 return "UNKNOWN_PROXY_MODE"
87}
88
89// ===== Proxy verdict sealed enum =================================
90
91const NX_PROXY_OK: i64 = 0
92const NX_PROXY_FAIL_UPSTREAM_CONNECT: i64 = 1
93const NX_PROXY_FAIL_UPSTREAM_TIMEOUT: i64 = 2
94const NX_PROXY_FAIL_UPSTREAM_RESET: i64 = 3
95const NX_PROXY_FAIL_PAYLOAD_TOO_LARGE_413: i64 = 4 // enforced at proxy
96const NX_PROXY_FAIL_BAD_GATEWAY_502: i64 = 5 // upstream malformed
97const NX_PROXY_FAIL_GATEWAY_TIMEOUT_504: i64 = 6
98const NX_PROXY_FAIL_CLIENT_DISCONNECTED: i64 = 7 // not an error;
99 // pump exited cleanly
100const NX_PROXY_FAIL_DEPENDENCY_MISSING: i64 = 8 // PARTIAL_WIRED
101
102func nx_proxy_verdict_name(v: i64) -> *u8 {
103 if v == NX_PROXY_OK { return "OK" }
104 if v == NX_PROXY_FAIL_UPSTREAM_CONNECT { return "FAIL_UPSTREAM_CONNECT" }
105 if v == NX_PROXY_FAIL_UPSTREAM_TIMEOUT { return "FAIL_UPSTREAM_TIMEOUT" }
106 if v == NX_PROXY_FAIL_UPSTREAM_RESET { return "FAIL_UPSTREAM_RESET" }
107 if v == NX_PROXY_FAIL_PAYLOAD_TOO_LARGE_413 { return "FAIL_PAYLOAD_TOO_LARGE_413" }
108 if v == NX_PROXY_FAIL_BAD_GATEWAY_502 { return "FAIL_BAD_GATEWAY_502" }
109 if v == NX_PROXY_FAIL_GATEWAY_TIMEOUT_504 { return "FAIL_GATEWAY_TIMEOUT_504" }
110 if v == NX_PROXY_FAIL_CLIENT_DISCONNECTED { return "CLIENT_DISCONNECTED" }
111 if v == NX_PROXY_FAIL_DEPENDENCY_MISSING { return "FAIL_DEPENDENCY_MISSING" }
112 return "UNKNOWN_PROXY_VERDICT"
113}
114
115// ===== Header rewrite policy sealed enum =========================
116//
117// Standard nginx-equivalent header sets. Caller picks one; substrate
118// applies the rewrite consistently.
119
120const NX_PROXY_HDR_PRESERVE_HOST: i64 = 1 // Host: header passed through
121const NX_PROXY_HDR_REWRITE_HOST: i64 = 2 // Host: replaced with
122 // upstream's name
123const NX_PROXY_HDR_ADD_FORWARDED: i64 = 3 // RFC 7239 Forwarded header
124const NX_PROXY_HDR_ADD_LEGACY_XFF: i64 = 4 // X-Forwarded-For + X-Real-IP
125 // + X-Forwarded-Proto
126const NX_PROXY_HDR_HIDE_BACKEND: i64 = 5 // strip Server: / X-Powered-By:
127
128func nx_proxy_hdr_policy_name(p: i64) -> *u8 {
129 if p == NX_PROXY_HDR_PRESERVE_HOST { return "PRESERVE_HOST" }
130 if p == NX_PROXY_HDR_REWRITE_HOST { return "REWRITE_HOST" }
131 if p == NX_PROXY_HDR_ADD_FORWARDED { return "ADD_FORWARDED" }
132 if p == NX_PROXY_HDR_ADD_LEGACY_XFF { return "ADD_LEGACY_XFF" }
133 if p == NX_PROXY_HDR_HIDE_BACKEND { return "HIDE_BACKEND" }
134 return "UNKNOWN_HDR_POLICY"
135}
136
137// ===== Proxy route descriptor struct =============================
138//
139// One per registered route. Replaces a complete nginx location block.
140
141struct NxProxyRoute {
142 public_path_prefix_ptr: *u8 // e.g., "/video/"
143 public_path_prefix_len: i64
144 upstream_host_ptr: *u8 // e.g., "127.0.0.1"
145 upstream_host_len: i64
146 upstream_port: i64 // e.g., NX_MAGIC_8282
147 proxy_mode: i64 // NX_PROXY_MODE_*
148 header_policy: i64 // NX_PROXY_HDR_*
149 connect_timeout_ms: i64
150 send_timeout_ms: i64
151 read_timeout_ms: i64
152 payload_size_limit_bytes: i64 // 0 = unlimited (refused)
153 enable_keepalive_to_upstream: i64 // 0/1
154 keepalive_pool_size: i64 // per-route upstream keepalive pool
155}
156
157// ===== Top-level entry stubs =====================================
158
159// nx_http_proxy_route_register -- declare a proxy route. Composes with
160// nx_http_router (FULLY_WIRED) to install the route in nx_http_server.
161
162func nx_http_proxy_route_register(route_ptr: *NxProxyRoute) -> i64 {
163 if route_ptr == 0 as *NxProxyRoute { return NX_PROXY_FAIL_DEPENDENCY_MISSING }
164 return NX_PROXY_FAIL_DEPENDENCY_MISSING
165}
166
167// nx_http_proxy_dispatch -- called by nx_http_server when a request
168// matches a registered proxy route. Forwards request + streams response.
169
170func nx_http_proxy_dispatch(client_fd: i64,
171 route_id: i64,
172 request_buf_ptr: *u8, request_buf_len: i64) -> i64 {
173 if client_fd < 0 { return NX_PROXY_FAIL_DEPENDENCY_MISSING }
174 if route_id <= 0 { return NX_PROXY_FAIL_DEPENDENCY_MISSING }
175 if request_buf_len < 0 { return NX_PROXY_FAIL_DEPENDENCY_MISSING }
176 return NX_PROXY_FAIL_DEPENDENCY_MISSING
177}
178
179// nx_http_proxy_pump_ws -- bidirectional WebSocket frame pump. Once
180// upgrade handshake completes, this primitive does the per-frame
181// forwarding both directions until either side closes.
182
183func nx_http_proxy_pump_ws(client_fd: i64, upstream_fd: i64,
184 frame_size_limit_bytes: i64) -> i64 {
185 if client_fd < 0 { return NX_PROXY_FAIL_DEPENDENCY_MISSING }
186 if upstream_fd < 0 { return NX_PROXY_FAIL_DEPENDENCY_MISSING }
187 if frame_size_limit_bytes <= 0 { return NX_PROXY_FAIL_DEPENDENCY_MISSING }
188 return NX_PROXY_FAIL_DEPENDENCY_MISSING
189}
190
191// nx_http_proxy_count_routes -- inspector.
192
193func nx_http_proxy_count_routes() -> i64 {
194 return 0
195}
196
197// nx_http_proxy_get_last_verdict -- inspector.
198
199func nx_http_proxy_get_last_verdict() -> i64 {
200 return NX_PROXY_FAIL_DEPENDENCY_MISSING
201}
202
203// ===== WIRED: HTTP/1.1 GET forward + relay (2026-07-02) ===========
204// Implements REQUEST_BUFFER_FORWARDING + RESPONSE_STREAM_BACK for the
205// request/response mode (GET/HEAD, no body). The TLS edge terminates TLS,
206// so these RETURN the upstream response bytes and the edge TLS-sends them
207// (no raw client_fd write). Body-forwarding (POST) / WS / keepalive stay
208// per the MISSING_CAPABILITIES list above.
209
210func nxp_sockaddr(addr: *u8, port: i64, a: i64, b: i64, c: i64, d: i64) -> i64 {
211 addr[0] = 2 as u8; addr[1] = 0 as u8
212 addr[2] = ((port / 256) & 255) as u8
213 addr[3] = (port & 255) as u8
214 addr[4] = a as u8; addr[5] = b as u8; addr[6] = c as u8; addr[7] = d as u8
215 var i: i64 = 8
216 while i < 16 { addr[i] = 0 as u8; i = i + 1 }
217 return 0
218}
219
220// parse "a.b.c.d" -> out4[0..4]; returns 1 on 4 octets.
221func nxp_parse_ip(s: *u8, slen: i64, out4: *i64) -> i64 {
222 var oct: i64 = 0; var val: i64 = 0; var i: i64 = 0
223 while i < slen {
224 let ch: i64 = s[i] as i64
225 if ch >= 0x30 { if ch <= 0x39 { val = val * 10 + (ch - 0x30) } else { if ch == 0x2e { if oct < 4 { out4[oct] = val } oct = oct + 1; val = 0 } } }
226 else { if ch == 0x2e { if oct < 4 { out4[oct] = val } oct = oct + 1; val = 0 } }
227 i = i + 1
228 }
229 if oct < 4 { out4[oct] = val; oct = oct + 1 }
230 if oct == 4 { return 1 }
231 return 0
232}
233
234// Rewrite a GET request line: strip `prefix` from the path so the backend sees
235// its own routes ("GET /research/work/X HTTP/1.1" + prefix "/research" ->
236// "GET /work/X HTTP/1.0 ... Connection: close"). Preserves ?query. Returns len.
237func nx_http_proxy_rewrite(req: *u8, reqn: i64, prefix: *u8, prefixlen: i64, out: *u8) -> i64 {
238 var sp: i64 = 0 - 1
239 var i: i64 = 0
240 while i < reqn { if sp < 0 { if req[i] == 32 as u8 { sp = i } } i = i + 1 }
241 if sp < 0 { return 0 }
242 var o: i64 = 0
243 var c: i64 = 0
244 while c <= sp { out[o] = req[c]; o = o + 1; c = c + 1 } // "METHOD "
245 var ps: i64 = sp + 1
246 var mtch: i64 = 1
247 var pi: i64 = 0
248 while pi < prefixlen { if ps + pi >= reqn { mtch = 0; pi = prefixlen } else { if req[ps + pi] != prefix[pi] { mtch = 0; pi = prefixlen } else { pi = pi + 1 } } }
249 if mtch == 1 { ps = ps + prefixlen }
250 if ps >= reqn { out[o] = 47 as u8; o = o + 1 }
251 else { if req[ps] != 47 as u8 { out[o] = 47 as u8; o = o + 1 } }
252 var pe: i64 = ps
253 var go: i64 = 1
254 while go == 1 { if pe >= reqn { go = 0 } else { if req[pe] == 32 as u8 { go = 0 } else { out[o] = req[pe]; o = o + 1; pe = pe + 1 } } }
255 let tail: *u8 = " HTTP/1.0\r\nHost: nishi\r\nConnection: close\r\n\r\n" as *u8
256 var ti: i64 = 0
257 while tail[ti] != 0 as u8 { out[o] = tail[ti]; o = o + 1; ti = ti + 1 }
258 return o
259}
260
261// Connect to upstream host:port, send req, relay the full response into out.
262// returns bytes received, or a negative NX_PROXY_FAIL_* verdict.
263func nx_http_proxy_forward(host: *u8, hostlen: i64, port: i64, req: *u8, reqn: i64, out: *u8, out_cap: i64) -> i64 {
264 let ip: *i64 = sys_mmap(64) as *i64
265 if nxp_parse_ip(host, hostlen, ip) != 1 { return 0 - NX_PROXY_FAIL_UPSTREAM_CONNECT }
266 let fd: i64 = sys_socket(2, 1, 0)
267 if fd < 0 { return 0 - NX_PROXY_FAIL_UPSTREAM_CONNECT }
268 let addr: *u8 = sys_mmap(16)
269 nxp_sockaddr(addr, port, ip[0], ip[1], ip[2], ip[3])
270 if nx_connect_bounded(fd, addr, 16, NX_CONN_DEFAULT_MS) < 0 { sys_close(fd); return 0 - NX_PROXY_FAIL_UPSTREAM_CONNECT }
271 var off: i64 = 0
272 while off < reqn { let w: i64 = sys_write(fd, ((req as i64) + off) as *u8, reqn - off); if w <= 0 { off = reqn } else { off = off + w } }
273 var total: i64 = 0
274 var go: i64 = 1
275 while go == 1 {
276 if total >= out_cap { go = 0 }
277 else { let r: i64 = sys_read(fd, ((out as i64) + total) as *u8, out_cap - total); if r <= 0 { go = 0 } else { total = total + r } }
278 }
279 sys_close(fd)
280 return total
281}
282
283// Convenience: rewrite a GET request (strip prefix) + forward to a local
284// backend + return the response bytes. This is the single call the sites edge
285// makes for a /research-style proxy route (replaces an nginx location block).
286func nx_http_proxy_relay(host: *u8, hostlen: i64, port: i64, prefix: *u8, prefixlen: i64, req: *u8, reqn: i64, out: *u8, out_cap: i64) -> i64 {
287 let rw: *u8 = sys_mmap(NX_MAGIC_65536)
288 let rn: i64 = nx_http_proxy_rewrite(req, reqn, prefix, prefixlen, rw)
289 if rn <= 0 { return 0 - NX_PROXY_FAIL_BAD_GATEWAY_502 }
290 return nx_http_proxy_forward(host, hostlen, port, rw, rn, out, out_cap)
291}