code wiki / (root) / nx_http_proxy.nx

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}