code wiki / (root) / nx_compare_gw.nx

nx_compare_gw.nx source

↩ module page · 359 lines · 21033 B

1// nx_compare_gw.nx -- the /compare GATEWAY DAEMON (loopback HTTP): the compare surface behind the same 2// OPAQUE login wall and the same HR access structure as /studio and /hub (operator 2026-08-27: "i can 3// see them as the owner but then others see different levels of /compare so i can provide a client a 4// /compare"). It sits behind sites_daemon's fail-closed `gated` reverse-proxy for /compare, mirroring 5// the /hub -> hub_gw topology exactly, and changes NOTHING about how nx_compare_regen emits the pages -- 6// it gates at SERVE time from knowledge/compare/access.conf, so the single-emission publish pipeline is 7// untouched and a board's visibility is one conf row, live with no restart. 8// 9// THE CHAIN, all sovereign and all already proven elsewhere: 10// olg_whoami (nx_hr_access -> hac_session_level) validates the no-cookie X-Nishi-Session token against 11// the nishi_site_admin realm and resolves the viewer's HR LEVEL (deny-by-default: an invalid/expired 12// token, or a valid token for someone not enrolled in HR, resolves to 0 = public) ; then 13// cg_required(access.conf, domain) gives the board's required level ; then cg_decide serves, 404s or 14// 403s. FAIL-CLOSED and NO-LEAK by construction: a board a viewer may not see is absent from their index 15// bytes AND answers 404, the SAME answer a board that does not exist gives -- an anonymous visitor can 16// never tell a gated board from a missing one, while the owner serves it. 17// 18// HARDENING baked in from the studio/hub post-mortems: binds 127.0.0.1 ONLY (the edge reaches it through 19// the proxy), and sets FD_CLOEXEC on the listener so a forked child can never hold the port (the outage 20// that survives every restart). All buffers are allocated ONCE before the accept loop and reused, and 21// every per-request read is bounded into a reused buffer -> a flat virtual footprint. This daemon never 22// forks. license_tier: ORIGINAL No hw writes (Rule 26). 23import "nx_syscalls.nx" 24import "nx_hr_access.nx" // hac_session_level + NxAuthContext (OPAQUE session -> HR level) 25import "nx_opaque_login.nx" // olg_ctx_setup + NxAuthContext 26import "nx_compare_openapi.nx" 27import "nx_compare_gw_lib.nx" // cg_* : the pure decision core 28 29const CGW_DOCROOT: *u8 = "/volume1/homes/elderwesto/nishihost/sites/nishifamily/compare" as *u8 30const CGW_ACCESS: *u8 = "knowledge/compare/access.conf" as *u8 31// The HR store elderwesto (owner) is enrolled in for the nishifamily realm; deny-by-default for everyone 32// else, so a client handed a session sees only the boards access.conf marks at or below their level. 33const CGW_HR: *u8 = "/volume1/homes/elderwesto/nishihost/nishi_hr-" as *u8 34const CGW_REALM: *u8 = "nishi_site_admin" as *u8 35const CGW_REALM_N: i64 = 16 36const CGW_DISP: *u8 = "Nishi Compare" as *u8 37const CGW_DISP_N: i64 = 13 38 39const CGW_REQ: i64 = 32768 40const CGW_CONF: i64 = 262144 41const CGW_BODY: i64 = 4194304 42const CGW_SCRATCH: i64 = 8388608 43const CGW_DENTS: i64 = 65536 44const CGW_PATH: i64 = 1024 45const CGW_TITLE: i64 = 512 46const CGW_TOK: i64 = 512 47const CGW_DOM: i64 = 128 48const CGW_REST: i64 = 512 49const CGW_STAT: i64 = 256 50const CGW_DEFAULT_STEPS: i64 = 65536 51const CGW_TCOST: i64 = 3 52const CGW_PCOST: i64 = 4 53// stat st_mode lives at byte offset 24 of struct stat; S_IFDIR = 0x4000, the type mask is 0xf000. 54const CGW_STMODE_OFF: i64 = 24 55const CGW_S_IFMT: i64 = 61440 56const CGW_S_IFDIR: i64 = 16384 57const CGW_S_IFREG: i64 = 32768 58const CGW_FCNTL: i64 = 72 59const CGW_FSETFD: i64 = 2 60const CGW_FD_CLOEXEC: i64 = 1 61 62func cgw_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 63func cgw_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; var p: i64 = o; while s[i] != (0 as u8) { d[p] = s[i]; p = p + 1; i = i + 1 } return p } 64func cgw_catn(d: *u8, o: i64, s: *u8, n: i64) -> i64 { var i: i64 = 0; var p: i64 = o; while i < n { d[p] = s[i]; p = p + 1; i = i + 1 } return p } 65func cgw_itoa(d: *u8, o: i64, v: i64) -> i64 { 66 let t: *u8 = sys_mmap(28) 67 var m: i64 = v 68 var p: i64 = o 69 if m == 0 { d[p] = 48 as u8; return p + 1 } 70 var k: i64 = 0 71 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 72 var i: i64 = k - 1 73 while i >= 0 { d[p] = t[i]; p = p + 1; i = i - 1 } 74 return p 75} 76 77// leak-free bounded read into a REUSED buffer; returns bytes (>=0) or -1. 78func cgw_read_file(path: *u8, out: *u8, cap: i64) -> i64 { 79 let fd: i64 = sys_openat_rd(path) 80 if fd < 0 { return 0 - 1 } 81 var total: i64 = 0 82 var go: i64 = 1 83 while go == 1 { 84 let tail: *u8 = (out as i64 + total) as *u8 85 let nr: i64 = sys_read(fd, tail, cap - total) 86 if nr <= 0 { go = 0 } 87 if nr > 0 { total = total + nr } 88 if total >= cap { go = 0 } 89 } 90 sys_close(fd) 91 return total 92} 93 94// stat a path; returns the file type bits (CGW_S_IFDIR / CGW_S_IFREG / ...) or 0 if it does not exist. 95func cgw_kind(path: *u8, stbuf: *u8) -> i64 { 96 if sys_fstatat(path, stbuf) != 0 { return 0 } 97 let mode: i64 = (scm_get_i64(stbuf, CGW_STMODE_OFF)) & 65535 98 return mode & CGW_S_IFMT 99} 100 101// HTTP response into a reused scratch; returns bytes written. 102func cgw_send(cfd: i64, scratch: *u8, status: *u8, ctype: *u8, body: *u8, blen: i64) -> i64 { 103 var o: i64 = 0 104 o = cgw_cat(scratch, o, "HTTP/1.1 " as *u8); o = cgw_cat(scratch, o, status) 105 o = cgw_cat(scratch, o, "\r\nContent-Type: " as *u8); o = cgw_cat(scratch, o, ctype) 106 o = cgw_cat(scratch, o, "\r\nContent-Length: " as *u8); o = cgw_itoa(scratch, o, blen) 107 o = cgw_cat(scratch, o, "\r\nConnection: close\r\nCache-Control: no-store\r\n\r\n" as *u8) 108 o = cgw_catn(scratch, o, body, blen) 109 sys_write(cfd, scratch, o) 110 return o 111} 112func cgw_text(cfd: i64, scratch: *u8, status: *u8, msg: *u8) -> i64 { 113 return cgw_send(cfd, scratch, status, "text/plain; charset=utf-8" as *u8, msg, cgw_slen(msg)) 114} 115 116// Build the leveled index: enumerate the docroot's directories, and for each board the level MAY see, 117// read its api.json title and emit a row. A board above the level is never written -> not-even-listed. 118// json=1 emits the filtered api.json; json=0 emits the HTML index. Returns the byte length. 119func cgw_index(level: i64, with_upgrade: i64, json: i64, conf: *u8, cn: i64, 120 out: *u8, cap: i64, dents: *u8, pathb: *u8, titleb: *u8, jbuf: *u8, stbuf: *u8, now: i64) -> i64 { 121 var o: i64 = 0 122 if json == 1 { o = cg_api_open(out, now) } else { o = cg_index_open(out, level, with_upgrade) } 123 var shown: i64 = 0 124 var first: i64 = 1 125 let fd: i64 = sys_openat_rd(CGW_DOCROOT) 126 if fd >= 0 { 127 var go: i64 = 1 128 while go == 1 { 129 let nr: i64 = sys_getdents64(fd, dents, CGW_DENTS) 130 if nr <= 0 { go = 0 } else { 131 var pos: i64 = 0 132 while pos < nr { 133 let rec: *u8 = ((dents as i64) + pos) as *u8 134 let reclen: i64 = dirent_reclen(rec) 135 let dtype: i64 = dirent_type(rec) 136 let name: *u8 = dirent_name(rec) 137 if dtype == DT_DIR { 138 // skip . and .. 139 var dot: i64 = 0 140 if name[0] == (46 as u8) { dot = 1 } 141 if dot == 0 { 142 let req: i64 = cg_required(conf, cn, name) 143 if level >= req { 144 // read the board title from its api.json (best-effort; falls back to the name) 145 titleb[0] = 0 as u8 146 var po: i64 = cgw_cat(pathb, 0, CGW_DOCROOT) 147 pathb[po] = 47 as u8; po = po + 1 148 po = cgw_cat(pathb, po, name) 149 po = cgw_cat(pathb, po, "/api.json" as *u8); pathb[po] = 0 as u8 150 let jn: i64 = cgw_read_file(pathb, jbuf, CGW_BODY) 151 if jn > 0 { cg_json_str(jbuf, jn, "title" as *u8, titleb, CGW_TITLE) } 152 if json == 1 { o = cg_api_row(out, o, first, name, titleb); first = 0 } 153 else { o = cg_index_row(out, o, name, titleb) } 154 shown = shown + 1 155 } 156 } 157 } 158 if reclen <= 0 { pos = nr } else { pos = pos + reclen } 159 } 160 } 161 } 162 sys_close(fd) 163 } 164 if json == 1 { o = cg_api_close(out, o, shown) } else { o = cg_index_close(out, o, shown) } 165 return o 166} 167 168 169// EC47 (2026-09-16): /compare/feed.xml -- the daily feed, ONE file for every board, projected to the viewer's level by the 170// decision core (an item for a board above the level is ABSENT from the bytes, exactly as the index is). Absent, over the 171// gateway's body reserve, or not a feed the projector understands -> 503, never a half feed. 172const CGW_FEED_FILE: *u8 = "feed.xml" as *u8 173const CGW_FEED_PATH: *u8 = "/feed.xml" as *u8 174const CGW_CTYPE_RSS: *u8 = "application/rss+xml; charset=utf-8" as *u8 175const CGW_FEED_CTR_BYTES: i64 = 16 // kept + dropped, one i64 each 176func cgw_feed(cfd: i64, scratch: *u8, level: i64, conf: *u8, cnv: i64, body: *u8, jbuf: *u8, pathb: *u8, dom: *u8) -> i64 { 177 var fo: i64 = cgw_cat(pathb, 0, CGW_DOCROOT) 178 fo = cgw_cat(pathb, fo, CGW_FEED_PATH); pathb[fo] = 0 as u8 179 let fn: i64 = cgo_read(pathb, jbuf, CGW_BODY) 180 let fk: *i64 = sys_mmap(CGW_FEED_CTR_BYTES) as *i64 181 var fl: i64 = 0 - 1 182 if fn > 0 { if fn < CGW_BODY { fl = cg_feed_project(jbuf, fn, level, conf, cnv, body, CGW_BODY, dom, CGW_DOM, fk, ((fk as i64) + CGW_FEED_CTR_BYTES / 2) as *i64) } } 183 if fl >= 0 { cgw_send(cfd, scratch, "200 OK" as *u8, CGW_CTYPE_RSS, body, fl) } 184 else { cgw_text(cfd, scratch, "503 Service Unavailable" as *u8, "feed unavailable (absent, over the gateway reserve, or not a feed this gateway can project)" as *u8) } 185 sys_munmap(fk as *u8, CGW_FEED_CTR_BYTES) 186 return fl 187} 188 189func main(argc: i64, argv: *i64) -> i64 { 190 if argc < 5 { 191 sys_write(1, "usage: nx_compare_gw <port> <opaque_keys> <opaque_store> <budget> [m] [t] [p]\n" as *u8, 78) 192 sys_exit(2); return 2 193 } 194 let port: i64 = 0 195 var pi: i64 = 0 196 let ps: *u8 = argv[1] as *u8 197 while ps[pi] != (0 as u8) { let c: i64 = ps[pi] as i64; if c >= 48 { if c <= 57 { } } pi = pi + 1 } 198 // parse port 199 var portv: i64 = 0 200 var qi: i64 = 0 201 while ps[qi] != (0 as u8) { let c: i64 = ps[qi] as i64; if c >= 48 { if c <= 57 { portv = portv * 10 + (c - 48) } } qi = qi + 1 } 202 let keys_path: *u8 = argv[2] as *u8 203 let store_path: *u8 = argv[3] as *u8 204 var m_cost: i64 = CGW_DEFAULT_STEPS 205 // (budget argv[4] accepted for shape-parity with the sibling gateways; this daemon serves until killed) 206 207 // OPAQUE context on the SAME realm the /login daemon (:9091) issues, so an existing session works. 208 let ctx: *NxAuthContext = sys_mmap(256) as *NxAuthContext 209 if olg_ctx_setup(ctx, keys_path, store_path, CGW_REALM, CGW_REALM_N, CGW_DISP, CGW_DISP_N, m_cost, CGW_TCOST, CGW_PCOST) != 0 { 210 sys_write(1, "CTX-INIT-FAIL\n" as *u8, 14); sys_exit(1); return 1 211 } 212 213 // bind LOOPBACK ONLY -- the edge reaches this through the sites proxy, never directly. 214 let addr: *u8 = sys_mmap(16) 215 addr[0] = 2 as u8; addr[1] = 0 as u8 216 addr[2] = ((portv >> 8) & 255) as u8; addr[3] = (portv & 255) as u8 217 addr[4] = 127 as u8; addr[5] = 0 as u8; addr[6] = 0 as u8; addr[7] = 1 as u8 218 var zi: i64 = 8; while zi < 16 { addr[zi] = 0 as u8; zi = zi + 1 } 219 let lfd: i64 = sys_socket(2, 1, 0) 220 if lfd < 0 { sys_write(1, "SOCKET-FAIL\n" as *u8, 12); sys_exit(1); return 1 } 221 let optv: *u8 = sys_mmap(4); optv[0] = 1 as u8; optv[1] = 0 as u8; optv[2] = 0 as u8; optv[3] = 0 as u8 222 sys_setsockopt(lfd, 1, 2, optv, 4) 223 if sys_bind(lfd, addr, 16) < 0 { sys_write(1, "BIND-FAIL\n" as *u8, 10); sys_exit(1); return 1 } 224 if sys_listen(lfd, 16) < 0 { sys_write(1, "LISTEN-FAIL\n" as *u8, 12); sys_exit(1); return 1 } 225 // FD_CLOEXEC on the listener: a forked child must never inherit the port (the outage that survives restart). 226 __syscall(CGW_FCNTL, lfd, CGW_FSETFD, CGW_FD_CLOEXEC, 0, 0, 0) 227 sys_write(1, "COMPARE-GATEWAY-UP\n" as *u8, 19) 228 229 // buffers allocated ONCE (flat footprint) 230 let req: *u8 = sys_mmap(CGW_REQ) 231 let conf: *u8 = sys_mmap(CGW_CONF) 232 let body: *u8 = sys_mmap(CGW_BODY) 233 let jbuf: *u8 = sys_mmap(CGW_BODY) 234 let scratch: *u8 = sys_mmap(CGW_SCRATCH) 235 let dents: *u8 = sys_mmap(CGW_DENTS) 236 let path: *u8 = sys_mmap(CGW_PATH) 237 let pathb: *u8 = sys_mmap(CGW_PATH) 238 let title: *u8 = sys_mmap(CGW_TITLE) 239 let tok: *u8 = sys_mmap(CGW_TOK) 240 let dom: *u8 = sys_mmap(CGW_DOM) 241 let rest: *u8 = sys_mmap(CGW_REST) 242 let stbuf: *u8 = sys_mmap(CGW_STAT) 243 244 var go2: i64 = 1 245 while go2 == 1 { 246 let cfd: i64 = sys_accept(lfd) 247 if cfd >= 0 { 248 sys_set_socket_timeout(cfd, 5) 249 let rn: i64 = sys_read(cfd, req, CGW_REQ - 1) 250 if rn > 0 { 251 let now: i64 = sys_now_realtime_sec() 252 let pl: i64 = cg_req_path(req, rn, path, CGW_PATH) 253 let tl: i64 = cg_req_header(req, rn, "X-Nishi-Session:" as *u8, tok, CGW_TOK) 254 // resolve the viewer's HR level (0 for no/invalid token -- deny-by-default) 255 var level: i64 = 0 256 if tl > 0 { level = hac_session_level(ctx, tok, tl, now, CGW_HR) } 257 // HOT-read the access conf per request (an edit is live, no restart) 258 let cn: i64 = cgw_read_file(CGW_ACCESS, conf, CGW_CONF) 259 var cnv: i64 = cn 260 if cnv < 0 { cnv = 0 } 261 let split: i64 = cg_split(path, pl, dom, CGW_DOM, rest, CGW_REST) 262 if split == 1 { 263 // the INDEX. A plain navigation with no token gets the PUBLIC index + upgrade shim, so a 264 // signed-in browser re-fetches with its header; a present-but-invalid token stays public. 265 var upg: i64 = 0 266 if tl == 0 { upg = 1 } 267 let bn: i64 = cgw_index(level, upg, 0, conf, cnv, body, CGW_BODY, dents, pathb, title, jbuf, stbuf, now) 268 cgw_send(cfd, scratch, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, body, bn) 269 } else { 270 if split == (0 - 1) { 271 cgw_text(cfd, scratch, "400 Bad Request" as *u8, "this endpoint serves /compare only" as *u8) 272 } else { 273 // a filtered index api.json at /compare/api.json is handled as the board "api.json"? No: 274 // api.json at the ROOT is the hub listing. cg_split gives dom="api.json" rest="" for it; 275 // treat a bare api.json / openapi.json / index.html at the compare root as the index. 276 var is_root_file: i64 = 0 277 if rest[0] == (0 as u8) { 278 if cg_streq(dom, "api.json" as *u8) == 1 { is_root_file = 1 } 279 if cg_streq(dom, "openapi.json" as *u8) == 1 { is_root_file = 1 } 280 if cg_streq(dom, "index.html" as *u8) == 1 { is_root_file = 1 } 281 if cg_streq(dom, CGW_FEED_FILE) == 1 { is_root_file = 1 } 282 } 283 if is_root_file == 1 { 284 if cg_streq(dom, "api.json" as *u8) == 1 { 285 let bn2: i64 = cgw_index(level, 0, 1, conf, cnv, body, CGW_BODY, dents, pathb, title, jbuf, stbuf, now) 286 cgw_send(cfd, scratch, "200 OK" as *u8, "application/json" as *u8, body, bn2) 287 } else { 288 if cg_streq(dom, "openapi.json" as *u8) == 1 { 289 var op: i64 = cgw_cat(pathb, 0, CGW_DOCROOT) 290 op = cgw_cat(pathb, op, "/openapi.json" as *u8); pathb[op] = 0 as u8 291 let on: i64 = cgo_read(pathb, jbuf, CGW_BODY) 292 var projected: i64 = 0 - 1 293 if on > 0 { projected = cgo_project(jbuf, on, level, conf, cnv, body, CGW_BODY, path, CGW_PATH, dom, CGW_DOM) } 294 if projected >= 0 { cgw_send(cfd, scratch, "200 OK" as *u8, "application/json" as *u8, body, projected) } 295 else { cgw_text(cfd, scratch, "503 Service Unavailable" as *u8, "OpenAPI contract unavailable" as *u8) } 296 } else { 297 if cg_streq(dom, CGW_FEED_FILE) == 1 { cgw_feed(cfd, scratch, level, conf, cnv, body, jbuf, pathb, dom) } else { 298 let bn3: i64 = cgw_index(level, 0, 0, conf, cnv, body, CGW_BODY, dents, pathb, title, jbuf, stbuf, now) 299 cgw_send(cfd, scratch, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, body, bn3) 300 } 301 } 302 } 303 } else { 304 // a board request. required level, does it exist, decide. 305 let required: i64 = cg_required(conf, cnv, dom) 306 var po: i64 = cgw_cat(pathb, 0, CGW_DOCROOT) 307 pathb[po] = 47 as u8; po = po + 1 308 po = cgw_cat(pathb, po, dom); pathb[po] = 0 as u8 309 var exists: i64 = 0 310 if cgw_kind(pathb, stbuf) == CGW_S_IFDIR { exists = 1 } 311 let d: i64 = cg_decide(level, required, exists) 312 if d == CG_D_SERVE { 313 // the file under the board: rest, defaulting to index.html 314 var fp: i64 = cgw_cat(pathb, 0, CGW_DOCROOT) 315 pathb[fp] = 47 as u8; fp = fp + 1 316 fp = cgw_cat(pathb, fp, dom) 317 pathb[fp] = 47 as u8; fp = fp + 1 318 // the served file is DECIDED in the lib (cg_board_file): a rest that names a directory is 319 // served as its index.html, never read as a file (which answered 200 with zero bytes) 320 var safe: i64 = 1 321 if rest[0] != (0 as u8) { 322 if cg_path_safe(rest, cgw_slen(rest)) == 0 { 323 cgw_text(cfd, scratch, "400 Bad Request" as *u8, "bad path" as *u8) 324 safe = 0 325 } 326 } 327 if safe == 1 { 328 var rest_dir: i64 = 0 329 if rest[0] != (0 as u8) { 330 let rp: i64 = cgw_cat(pathb, fp, rest); pathb[rp] = 0 as u8 331 if cgw_kind(pathb, stbuf) == CGW_S_IFDIR { rest_dir = 1 } 332 } 333 let served_file: *u8 = sys_mmap(cgw_slen(rest) + CG_INDEX_SUFFIX_BYTES) 334 cg_board_file(rest, rest_dir, served_file) 335 fp = cgw_cat(pathb, fp, served_file); pathb[fp] = 0 as u8 336 let fbn: i64 = cgw_read_file(pathb, body, CGW_BODY) 337 let bv: i64 = cg_body_verdict(fbn, CGW_BODY) 338 if bv == CG_BODY_SERVE { cgw_send(cfd, scratch, "200 OK" as *u8, cg_ctype(served_file), body, fbn) } 339 if bv == CG_BODY_TOO_LARGE { cgw_text(cfd, scratch, "500 Internal Server Error" as *u8, "page larger than the gateway body reserve -- refused rather than served truncated" as *u8) } 340 if bv == CG_BODY_MISSING { cgw_text(cfd, scratch, "404 Not Found" as *u8, "not found" as *u8) } 341 sys_munmap(served_file, cgw_slen(rest) + CG_INDEX_SUFFIX_BYTES) 342 } 343 } else { 344 if d == CG_D_DENY { 345 cgw_text(cfd, scratch, "403 Forbidden" as *u8, "your access level cannot open this board" as *u8) 346 } else { 347 cgw_text(cfd, scratch, "404 Not Found" as *u8, "not found" as *u8) 348 } 349 } 350 } 351 } 352 } 353 } 354 sys_close(cfd) 355 } 356 } 357 sys_close(lfd); sys_exit(0); return 0 358} 359