code wiki / (root) / nx_nishipages_serve.nx

nx_nishipages_serve.nx source

↩ module page · 326 lines · 11903 B

1// nx_nishipages_serve.nx -- sovereign self-contained static server for 2// nishi-pages. 3// 4// Built per operator directive 2026-06-30: "build the apis that will do 5// what you are trying to do in shell" + "sovereign live server first". 6// 7// WHY SELF-CONTAINED (not composing nx_pages_static / nx_http_server): 8// those modules pull nx_syscalls.nx, which needs nx_syscalls_x86_64.nx 9// for __syscall/sys_mmap -- but the two modules BOTH define 26 of the 10// same sys_* symbols, so any organ pulling both double-emits every 11// sys_* label and `as` rejects the duplicate (this is exactly why the 12// "shipped" nx_audit_server_* organs do not assemble on the current 13// toolchain). nx_syscalls.nx has 6098 importers, so it cannot be 14// edited. We therefore import ONLY the leaf x86_64 syscall layer 15// (every socket + file syscall, zero further imports) plus the pure, 16// import-free CWE-22 canonicalizer. Result: no symbol overlap, clean 17// link, fully sovereign. 18// 19// Serves the whole dist/ tree with clean URLs: 20// "/" -> dist/index.html 21// "/foo/" -> dist/foo/index.html 22// "/foo" -> dist/foo (file) else dist/foo/index.html else dist/foo.html 23// "/a/b.ext" -> dist/a/b.ext (literal; MIME by extension, incl. .wasm) 24// Path traversal (../, NUL, backslash, leading /) is rejected by 25// nx_path_canonicalize before any file is opened -- defense at the boundary. 26// 27// Build (x86_64 native Linux ELF): 28// cd nishi-core/nxc2 29// ./nxc2.exe --target x86_64 runtime/nx_nishipages_serve.nx > /tmp/nps.s 30// wsl bash -c "gcc -no-pie /tmp/nps.s -o /tmp/nx_nishipages_serve" 31// Run from the nishi-pages checkout so the relative "dist" root resolves: 32// cd nishi-pages && /tmp/nx_nishipages_serve # listens on :8090 33// 34// nx_capability_claims: 35// needs: [x86_64_syscalls, cwe_22_canonicalize] 36// provides: [nishipages_static_serve, clean_url_routing, 37// full_dist_tree_serve, wasm_mime] 38// safety: [cwe_22_canonicalize_at_boundary, get_only, 39// bounded_request_buffer, no_floating_point, 40// jpl_rule2_bounded_accept_loop] 41// verdict: [http_200_404_405] 42// license: ORIGINAL 43// kind: nishi_pages_specialist 44// layer: L3 (composite over L1 syscalls + L2 path canonicalize) 45// raci: [R=nishi_pages_builder, A=elder_west, 46// C=nx_path_canonicalize_owner, I=conductor] 47 48import "nx_syscalls_x86_64.nx" 49import "nx_path_canonicalize.nx" 50const NPS_MAGIC_2048: i64 = 2048 51const NPS_MAGIC_2040: i64 = 2040 52 53const NPS_PORT: i64 = 8090 54const NPS_BACKLOG: i64 = 128 55const NPS_REQ_CAP: i64 = 16384 56const NPS_BUDGET: i64 = 1000000 // JPL-rule-2 bounded accept loop 57 58// ---- small helpers ------------------------------------------------ 59 60func nps_strlen(s: *u8) -> i64 { 61 var i: i64 = 0 62 while s[i] != 0 { i = i + 1 } 63 return i 64} 65 66// Write all n bytes to fd, looping over partial socket writes. 67func nps_write_all(fd: i64, buf: *u8, n: i64) -> i64 { 68 var off: i64 = 0 69 while off < n { 70 let p: *u8 = ((buf as i64) + off) as *u8 71 let w: i64 = sys_write(fd, p, n - off) 72 if w <= 0 { return 0 } 73 off = off + w 74 } 75 return 0 76} 77 78// Append NUL-terminated s into buf at off; return new off. 79func nps_puts(buf: *u8, off: i64, s: *u8) -> i64 { 80 var o: i64 = off 81 var i: i64 = 0 82 while s[i] != 0 { 83 buf[o] = s[i] 84 o = o + 1 85 i = i + 1 86 } 87 return o 88} 89 90// Append decimal v into buf at off; return new off. (nxc2 has no % ; 91// mod is x - (x/10)*10.) 92func nps_putdec(buf: *u8, off: i64, v: i64) -> i64 { 93 if v == 0 { 94 buf[off] = 0x30 95 return off + 1 96 } 97 let tmp: *u8 = sys_mmap(32) 98 var k: i64 = 0 99 var x: i64 = v 100 while x > 0 { 101 tmp[k] = 0x30 + (x - (x / 10) * 10) 102 x = x / 10 103 k = k + 1 104 } 105 var o: i64 = off 106 var ri: i64 = k - 1 107 while ri >= 0 { 108 buf[o] = tmp[ri] 109 o = o + 1 110 ri = ri - 1 111 } 112 return o 113} 114 115// dst = a[0..a_n) ++ b(NUL-terminated); return total length. 116func nps_cat(dst: *u8, a: *u8, a_n: i64, b: *u8) -> i64 { 117 var i: i64 = 0 118 while i < a_n { 119 dst[i] = a[i] 120 i = i + 1 121 } 122 var j: i64 = 0 123 while b[j] != 0 { 124 dst[i + j] = b[j] 125 j = j + 1 126 } 127 return i + j 128} 129 130func nps_ends_with(s: *u8, n: i64, suf: *u8, suf_n: i64) -> i64 { 131 if n < suf_n { return 0 } 132 var i: i64 = 0 133 while i < suf_n { 134 if (s[n - suf_n + i] as i64) != (suf[i] as i64) { return 0 } 135 i = i + 1 136 } 137 return 1 138} 139 140func nps_mime(path: *u8, n: i64) -> *u8 { 141 if nps_ends_with(path, n, ".html" as *u8, 5) == 1 { return "text/html; charset=utf-8" as *u8 } 142 if nps_ends_with(path, n, ".css" as *u8, 4) == 1 { return "text/css; charset=utf-8" as *u8 } 143 if nps_ends_with(path, n, ".mjs" as *u8, 4) == 1 { return "application/javascript" as *u8 } 144 if nps_ends_with(path, n, ".js" as *u8, 3) == 1 { return "application/javascript" as *u8 } 145 if nps_ends_with(path, n, ".json" as *u8, 5) == 1 { return "application/json" as *u8 } 146 if nps_ends_with(path, n, ".wasm" as *u8, 5) == 1 { return "application/wasm" as *u8 } 147 if nps_ends_with(path, n, ".wat" as *u8, 4) == 1 { return "text/plain; charset=utf-8" as *u8 } 148 if nps_ends_with(path, n, ".png" as *u8, 4) == 1 { return "image/png" as *u8 } 149 if nps_ends_with(path, n, ".jpg" as *u8, 4) == 1 { return "image/jpeg" as *u8 } 150 if nps_ends_with(path, n, ".svg" as *u8, 4) == 1 { return "image/svg+xml" as *u8 } 151 if nps_ends_with(path, n, ".ico" as *u8, 4) == 1 { return "image/x-icon" as *u8 } 152 return "application/octet-stream" as *u8 153} 154 155// 1 if rel has a file extension in its last path segment (a '.' after 156// the last '/'). Clean URLs without an extension ("/voxels") route to 157// dir-index instead of reading the bare directory (which on 9p succeeds 158// and returns garbage bytes). 159func nps_has_ext(rel: *u8, rel_n: i64) -> i64 { 160 var i: i64 = rel_n - 1 161 while i >= 0 { 162 let c: i64 = rel[i] as i64 163 if c == 0x2f { return 0 } 164 if c == 0x2e { return 1 } 165 i = i - 1 166 } 167 return 0 168} 169 170func nps_send_404(cfd: i64) -> i64 { 171 let r: *u8 = "HTTP/1.1 404 Not Found\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: 14\r\nConnection: close\r\n\r\n404 Not Found\n" as *u8 172 nps_write_all(cfd, r, nps_strlen(r)) 173 return 0 174} 175 176func nps_send_405(cfd: i64) -> i64 { 177 let r: *u8 = "HTTP/1.1 405 Method Not Allowed\r\nAllow: GET\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" as *u8 178 nps_write_all(cfd, r, nps_strlen(r)) 179 return 0 180} 181 182// Emit a 200 OK with body. Header built in a small buffer, then body 183// streamed (so a 4 MB wasm body needs no oversized header buffer). 184func nps_send_200(cfd: i64, mime: *u8, body: *u8, body_n: i64) -> i64 { 185 let hdr: *u8 = sys_mmap(512) 186 var o: i64 = 0 187 o = nps_puts(hdr, o, "HTTP/1.1 200 OK\r\nContent-Type: " as *u8) 188 o = nps_puts(hdr, o, mime) 189 o = nps_puts(hdr, o, "\r\nContent-Length: " as *u8) 190 o = nps_putdec(hdr, o, body_n) 191 // no-cache: browsers revalidate, so a freshly-published game wasm is 192 // picked up on next load -- server-side auto-update, zero client JS. 193 o = nps_puts(hdr, o, "\r\nCache-Control: no-cache\r\nConnection: close\r\n\r\n" as *u8) 194 nps_write_all(cfd, hdr, o) 195 nps_write_all(cfd, body, body_n) 196 return 0 197} 198 199// Canonicalize rel, join under dist/, read the file. Returns 1 + sets 200// body_pp/len_p on success, 0 (traversal / not-found) otherwise. 201func nps_try_file(rel: *u8, rel_n: i64, body_pp: **u8, len_p: *i64) -> i64 { 202 let canon: *u8 = sys_mmap(NPS_MAGIC_2048) 203 let canon_n: *i64 = sys_mmap(8) as *i64 204 let cv: i64 = nx_path_canonicalize(rel, rel_n, canon, NPS_MAGIC_2048, canon_n) 205 if cv != NXP_OK { return 0 } 206 let full: *u8 = sys_mmap(NPS_MAGIC_2048) 207 let full_n: *i64 = sys_mmap(8) as *i64 208 let jv: i64 = nx_path_join("dist" as *u8, 4, canon, canon_n[0], full, NPS_MAGIC_2040, full_n) 209 if jv != NXP_OK { return 0 } 210 full[full_n[0]] = 0 211 let len: *i64 = sys_mmap(8) as *i64 212 let body: *u8 = sys_read_file_x86_64(full, len) 213 if body == (0 as *u8) { return 0 } 214 body_pp[0] = body 215 len_p[0] = len[0] 216 return 1 217} 218 219// Clean-URL router for one GET request path. 220func nps_serve(cfd: i64, path: *u8, path_n: i64) -> i64 { 221 if path_n < 1 { nps_send_404(cfd); return 0 } 222 if (path[0] as i64) != 0x2f { nps_send_404(cfd); return 0 } 223 224 let rel: *u8 = ((path as i64) + 1) as *u8 225 let rel_n: i64 = path_n - 1 226 227 let cbuf: *u8 = sys_mmap(NPS_MAGIC_2048) 228 let body_pp: **u8 = sys_mmap(8) as **u8 229 let len_p: *i64 = sys_mmap(8) as *i64 230 var found: i64 = 0 231 var used_n: i64 = 0 232 233 // Probe candidates by URL shape, never reading a bare directory: 234 // "/" -> index.html 235 // ".../" -> .../index.html 236 // "/a/b.ext" -> literal (file with an extension) 237 // "/clean" -> /clean/index.html, then /clean.html 238 if rel_n == 0 { 239 used_n = nps_cat(cbuf, "index.html" as *u8, 10, "" as *u8) 240 if nps_try_file(cbuf, used_n, body_pp, len_p) == 1 { found = 1 } 241 } else { 242 if (path[path_n - 1] as i64) == 0x2f { 243 used_n = nps_cat(cbuf, rel, rel_n, "index.html" as *u8) 244 if nps_try_file(cbuf, used_n, body_pp, len_p) == 1 { found = 1 } 245 } else { 246 if nps_has_ext(rel, rel_n) == 1 { 247 used_n = nps_cat(cbuf, rel, rel_n, "" as *u8) 248 if nps_try_file(cbuf, used_n, body_pp, len_p) == 1 { found = 1 } 249 } else { 250 used_n = nps_cat(cbuf, rel, rel_n, "/index.html" as *u8) 251 if nps_try_file(cbuf, used_n, body_pp, len_p) == 1 { found = 1 } 252 if found == 0 { 253 used_n = nps_cat(cbuf, rel, rel_n, ".html" as *u8) 254 if nps_try_file(cbuf, used_n, body_pp, len_p) == 1 { found = 1 } 255 } 256 } 257 } 258 } 259 260 if found == 1 { 261 let mime: *u8 = nps_mime(cbuf, used_n) 262 nps_send_200(cfd, mime, body_pp[0], len_p[0]) 263 } else { 264 nps_send_404(cfd) 265 } 266 return 0 267} 268 269// Read + parse one request on cfd, dispatch. GET only. 270func nps_handle(cfd: i64) -> i64 { 271 let req: *u8 = sys_mmap(NPS_REQ_CAP) 272 let n: i64 = sys_read(cfd, req, NPS_REQ_CAP) 273 if n < 5 { return 0 } 274 // Method must be "GET ". 275 if (req[0] as i64) != 0x47 { nps_send_405(cfd); return 0 } 276 if (req[1] as i64) != 0x45 { nps_send_405(cfd); return 0 } 277 if (req[2] as i64) != 0x54 { nps_send_405(cfd); return 0 } 278 if (req[3] as i64) != 0x20 { nps_send_405(cfd); return 0 } 279 // Path = req[4 .. first space]. 280 let path: *u8 = ((req as i64) + 4) as *u8 281 var pe: i64 = 4 282 var path_end: i64 = 0 - 1 283 while pe < n { 284 if path_end < 0 { 285 if (req[pe] as i64) == 0x20 { path_end = pe } 286 } 287 pe = pe + 1 288 } 289 if path_end < 0 { path_end = n } 290 let path_n: i64 = path_end - 4 291 nps_serve(cfd, path, path_n) 292 return 0 293} 294 295func main() -> i64 { 296 let fd: i64 = sys_socket(AF_INET, SOCK_STREAM, 0) 297 if fd < 0 { return 10 } 298 299 // SO_REUSEADDR so a quick restart doesn't hit TIME_WAIT bind refusal. 300 let one: *u8 = sys_mmap(4) 301 one[0] = 1 302 one[1] = 0 303 one[2] = 0 304 one[3] = 0 305 sys_setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, one, 4) 306 307 let addr: *u8 = sys_mmap(16) 308 sockaddr_in_init(addr, NPS_PORT) 309 if sys_bind(fd, addr, 16) < 0 { return 11 } 310 if sys_listen(fd, NPS_BACKLOG) < 0 { return 12 } 311 312 let banner: *u8 = "nx_nishipages_serve: listening on 0.0.0.0:8090 (root=dist/)\n" as *u8 313 sys_write(1, banner, nps_strlen(banner)) 314 315 var served: i64 = 0 316 while served < NPS_BUDGET { 317 let cfd: i64 = sys_accept(fd) 318 if cfd >= 0 { 319 nps_handle(cfd) 320 sys_close(cfd) 321 } 322 served = served + 1 323 } 324 sys_close(fd) 325 return 0 326}