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