code wiki / _hdl_build / nx_office_daemon.nx

nx_office_daemon.nx source

↩ module page · 275 lines · 15024 B

1// nx_office_daemon.nx -- the DEPLOYABLE Nishi Office daemon (ops shell around the gated pure core nx_office_serve). 2// Binds 0.0.0.0:<port> (LAN-reachable on the NAS); files live under CWD "<root>/". Route /office -> this port on the 3// sovereign edge. R-AUTHZ: resolves the OPAQUE session (X-Nishi-Session header, the `sess` form field on a zero-JS 4// POST, or the `s` query parameter the docportal's no-JS links carry) -> handle, and passes it to of_handle_auth so 5// OWNED docs are ReBAC-gated while unowned docs stay PUBLIC in open mode. FAIL-SAFE: if the auth ctx can't init in 6// OPEN mode, serve PUBLIC-only (owned docs lock, public work); in CLOSED mode it refuses to start (never public). 7// 8// 2026-08-19 (LP3, /compare/legalpractice): ONE binary, N instances. Every instance parameter is argv with today's 9// nishifamily values as defaults, so the deployed daemons.reg row (`./nx_office_daemon.elf`) keeps working unchanged: 10// nx_office_daemon [port] [keysfile] [storefile] [realm] [root] [base] [uididx] [mode] [authzprefix] 11// mode = open (default) unowned documents are public -- nishifamily.com/office 12// mode = closed EVERY request needs a valid session of this realm -- the firm instance on a client domain; the 13// open-mode namespace is NEVER exposed there (a 401 names the login surface instead) 14// The uid->handle index may be the login daemon's TAB index OR a docportal "<storefile>.uidmap" ("MAP <hex> <handle>"). 15// When a session is present the HTML response is rewritten so every same-app link carries ?s=<token> (no cookies, C1). 16// 17// FORK-PER-REQUEST (2026-08-19, the docportal's proven shape): of_handle_auth mmaps its per-request scratch (me, 18// fullpath, qs, Origin/Host, the page and capture buffers) and returns from dozens of sites without unmapping -- 19// the exact per-request leak class that took nx_opaque_login to 177 GB. A child that handles one connection and 20// then sys_exit(0) reclaims EVERY page BY CONSTRUCTION; the parent only accepts, forks and reaps (non-blocking 21// wait4 + backpressure at a ceiling DERIVED from the host's core count, never guessed). Functionally safe: sessions 22// are file-validated, every office write is a file write under <root>/ (versions, sign logs, authz tuples), and 23// the parent's request/response buffers are never mutated (the child works on its COW copy). 24// Build with --build-only; run deliberately. license_tier: ORIGINAL 25import "nx_office_serve.nx" 26import "nx_opaque_login.nx" // olg_ctx_setup / olg_whoami -- the SAME keys+realm the /login minter signs with 27import "nx_sysload.nx" // sl_ncpu: the fork ceiling is DERIVED from the live box (the docportal's derivation) 28 29const OD_PORT: i64 = 0x1f5e // 8030 (the nishifamily instance) 30const OD_KEYS: *u8 = "opaque_keys.bin" 31const OD_ASTORE: *u8 = "opaque_store.log" 32const OD_IDX: *u8 = "nishi_uid_handle.tsv" // the login daemon's uid(hex)->handle index (authz keys off handle) 33const OD_REALM: *u8 = "nishi_site_admin" 34const OD_REALM_LABEL: *u8 = "Nishi site admin" 35const OD_ROOT: *u8 = "office" 36const OD_BASE: *u8 = "/office" 37// REQUEST CEILING -- one named bound, and it ANNOUNCES: of_handle_auth refuses an import whose Content-Length 38// exceeds what arrived (413) instead of silently importing a truncated package. 8 MiB covers the 1 MiB document.xml 39// the format reader accepts plus media parts; raise it here, in one place, if the office ever ingests bigger files. 40const OD_REQCAP: i64 = 8388608 41const OD_RESCAP: i64 = 1048576 42const OD_MODE_OPEN: i64 = 0 43const OD_MODE_CLOSED: i64 = 1 44const OD_ARG_PORT: i64 = 1 45const OD_ARG_KEYS: i64 = 2 46const OD_ARG_STORE: i64 = 3 47const OD_ARG_REALM: i64 = 4 48const OD_ARG_ROOT: i64 = 5 49const OD_ARG_BASE: i64 = 6 50const OD_ARG_IDX: i64 = 7 51const OD_ARG_MODE: i64 = 8 52const OD_ARG_AZ: i64 = 9 53// the OPAQUE ctx + session scratch sizes the login daemon itself uses (nx_opaque_login: ctx 256, token 600, uid 64) 54const OD_CTX_SZ: i64 = 256 55const OD_TOK_CAP: i64 = 600 56const OD_UID_CAP: i64 = 64 57const OD_HANDLE_CAP: i64 = 256 58const OD_ADDR_SZ: i64 = 16 59const OD_LISTEN_BACKLOG: i64 = 16 60const OD_I64_SZ: i64 = 16 61// argon2id KSF cost: whoami only VALIDATES (register/login live in the minter), so the cost never runs here; the 62// values are the ones the previous build of this daemon carried (256/2/1) and are kept so the ctx is byte-identical. 63const OD_KSF_M: i64 = 256 64const OD_KSF_T: i64 = 2 65const OD_KSF_P: i64 = 1 66// BOUNDED CONCURRENCY (the docportal's derivation, nx_docportal_admin_daemon.dad_worker_cap): cores deliberately 67// left to the rest of the estate, and a floor of 1 (= today's serial behaviour, so this can only ADD concurrency). 68const OD_CPU_RESERVE: i64 = 2 69const OD_MIN_WORKERS: i64 = 1 70// the docportal uidmap row: "MAP " + 64 hex + " " + handle 71const OD_MAP_PFX: *u8 = "MAP " 72const OD_HEXUID: i64 = 64 73 74func d_addr(out: *u8, port: i64) -> i64 { 75 out[0] = 2 as u8; out[1] = 0 as u8 76 out[2] = ((port >> 8) & 0xff) as u8; out[3] = (port & 0xff) as u8 77 out[4] = 0 as u8; out[5] = 0 as u8; out[6] = 0 as u8; out[7] = 0 as u8 78 var i: i64 = 8 79 while i < OD_ADDR_SZ { out[i] = 0 as u8; i = i + 1 } 80 return 0 81} 82 83// uid -> handle from EITHER index shape: the login daemon's TAB index (rb_resolve_handle, single-exit + freed) or 84// the docportal "<storefile>.uidmap" whose rows read "MAP <64 hex> <handle>" (append-only, LAST row wins -- the 85// docportal's own dad_map_get contract). 0 = unmapped (handle stays empty). Whole file read via sys_read_file and 86// released via its PAIRED sys_free_file -- this runs once per authenticated request. 87func od_resolve_handle(idxpath: *u8, uid: *u8, uidn: i64, out_h: *u8, cap: i64) -> i64 { 88 let n1: i64 = rb_resolve_handle(idxpath, uid, uidn, out_h, cap) 89 if n1 > 0 { return n1 } 90 let szp: *i64 = sys_mmap(OD_I64_SZ) as *i64 91 szp[0] = 0 92 let idx: *u8 = sys_read_file(idxpath, szp) 93 var found: i64 = 0 94 if (idx as i64) != 0 { 95 let ux: *u8 = sys_mmap(OD_HEXUID * 2 + 8) 96 let uxn: i64 = rb_hex(ux, uid, uidn) 97 let pl: i64 = of_slen(OD_MAP_PFX) 98 let n: i64 = szp[0] 99 var ls: i64 = 0 100 while ls < n { 101 var le: i64 = ls 102 while le < n { if (idx[le] as i64) == 10 { break } le = le + 1 } 103 if le - ls > pl + uxn + 1 { if of_memhas((idx as i64 + ls) as *u8, pl, OD_MAP_PFX) == 1 { 104 var m: i64 = 1 105 var k: i64 = 0 106 while k < uxn { if idx[ls + pl + k] != ux[k] { m = 0; k = uxn } else { k = k + 1 } } 107 if m == 1 { if idx[ls + pl + uxn] == (32 as u8) { 108 var q: i64 = ls + pl + uxn + 1 109 var o: i64 = 0 110 while q < le { if o < cap - 1 { out_h[o] = idx[q]; o = o + 1 } q = q + 1 } 111 out_h[o] = 0 as u8 112 found = o 113 } } 114 } } 115 ls = le + 1 116 } 117 sys_munmap(ux, OD_HEXUID * 2 + 8) 118 sys_free_file(idx, n) 119 } 120 sys_munmap(szp as *u8, OD_I64_SZ) 121 if found == 0 { out_h[0] = 0 as u8 } 122 return found 123} 124 125// reap every child that has ALREADY exited, without blocking (the docportal's dad_reap_done idiom) 126func od_reap_done(wst: *i64) -> i64 { 127 var n: i64 = 0 128 var go: i64 = 1 129 while go == 1 { 130 let r: i64 = sys_wait4(0 - 1, wst, WNOHANG) 131 if r > 0 { n = n + 1 } else { go = 0 } 132 } 133 return n 134} 135 136func od_worker_cap() -> i64 { 137 let ncpu: i64 = sl_ncpu() 138 var capw: i64 = ncpu - OD_CPU_RESERVE 139 if capw < OD_MIN_WORKERS { capw = OD_MIN_WORKERS } 140 return capw 141} 142 143// the session token of this request, or "" -- header first, then the zero-JS form field, then the ?s= query 144// parameter the no-JS links carry. A token that does not validate in THIS realm is no session at all. 145func od_session(ctx: *NxAuthContext, reqb: *u8, rn: i64, tokb: *u8, qsb: *u8, uid: *u8, uidn: *i64, idx: *u8, hbuf: *u8) -> i64 { 146 hbuf[0] = 0 as u8; tokb[0] = 0 as u8 147 of_hdr_get(reqb, rn, "X-Nishi-Session:" as *u8, tokb, OD_TOK_CAP) 148 if tokb[0] == (0 as u8) { 149 var bs2: i64 = 0 - 1 150 var jj: i64 = 0 151 while jj + 3 < rn { 152 if reqb[jj] == (13 as u8) { if reqb[jj+1] == (10 as u8) { if reqb[jj+2] == (13 as u8) { if reqb[jj+3] == (10 as u8) { bs2 = jj + 4; jj = rn } } } } 153 jj = jj + 1 154 } 155 if bs2 >= 0 { of_form_get((reqb as i64 + bs2) as *u8, rn - bs2, "sess" as *u8, tokb, OD_TOK_CAP) } 156 } 157 if tokb[0] == (0 as u8) { 158 // the request-line query string: "GET /office/doc/x?s=<tok> HTTP/1.1" 159 var qi: i64 = 0 160 var qstart: i64 = 0 - 1 161 var qend: i64 = 0 - 1 162 while qi < rn { if reqb[qi] == (32 as u8) { if qstart < 0 { qstart = qi + 1 } else { if qend < 0 { qend = qi; qi = rn } } } if qi < rn { if reqb[qi] == (10 as u8) { qi = rn } } qi = qi + 1 } 163 if qstart >= 0 { if qend > qstart { 164 var qq: i64 = qstart 165 var qmark: i64 = 0 - 1 166 while qq < qend { if reqb[qq] == (63 as u8) { qmark = qq; qq = qend } else { qq = qq + 1 } } 167 if qmark >= 0 { 168 var qn: i64 = 0 169 var qc: i64 = qmark + 1 170 while qc < qend { if qn < OD_TOK_CAP - 1 { qsb[qn] = reqb[qc]; qn = qn + 1 } qc = qc + 1 } 171 qsb[qn] = 0 as u8 172 of_form_get(qsb, qn, "s" as *u8, tokb, OD_TOK_CAP) 173 } 174 } } 175 } 176 if tokb[0] != (0 as u8) { 177 if olg_whoami(ctx, tokb, of_slen(tokb), sys_now_realtime_sec(), uid, OD_UID_CAP, uidn) == NX_MAUTH_OK { 178 od_resolve_handle(idx, uid, uidn[0], hbuf, OD_HANDLE_CAP) 179 } 180 if hbuf[0] == (0 as u8) { tokb[0] = 0 as u8 } 181 } 182 return 0 183} 184 185func main(argc: i64, argv: *i64) -> i64 { 186 var port: i64 = OD_PORT 187 var keys: *u8 = OD_KEYS 188 var store: *u8 = OD_ASTORE 189 var realm: *u8 = OD_REALM 190 var root: *u8 = OD_ROOT 191 var base: *u8 = OD_BASE 192 var idx: *u8 = OD_IDX 193 var mode: i64 = OD_MODE_OPEN 194 var az: *u8 = OF_AUTHZ 195 if argc > OD_ARG_PORT { port = of_atoi(argv[OD_ARG_PORT] as *u8) } 196 if argc > OD_ARG_KEYS { keys = argv[OD_ARG_KEYS] as *u8 } 197 if argc > OD_ARG_STORE { store = argv[OD_ARG_STORE] as *u8 } 198 if argc > OD_ARG_REALM { realm = argv[OD_ARG_REALM] as *u8 } 199 if argc > OD_ARG_ROOT { root = argv[OD_ARG_ROOT] as *u8 } 200 if argc > OD_ARG_BASE { base = argv[OD_ARG_BASE] as *u8 } 201 if argc > OD_ARG_IDX { idx = argv[OD_ARG_IDX] as *u8 } 202 if argc > OD_ARG_MODE { if of_seq(argv[OD_ARG_MODE] as *u8, "closed" as *u8) == 1 { mode = OD_MODE_CLOSED } } 203 // the ReBAC authz plane of THIS instance (argv 9): a tenant instance MUST carry its own prefix so document ownership, 204 // shares and groups never cross realms; the default keeps the nishifamily instance on its existing plane unchanged 205 if argc > OD_ARG_AZ { az = argv[OD_ARG_AZ] as *u8 } 206 if port <= 0 { p("NX-OFFICE-DAEMON bad port argument -- fail loud\n" as *u8); return 1 } 207 // authz tuples (officeauthz_*) live in the cwd (nishihost/), which this daemon's user owns -- no mkdir needed. 208 // AUTH ctx -- FAIL-SAFE in open mode: on failure serve PUBLIC-only. In CLOSED mode an unusable auth context is 209 // FATAL, because every request must be authenticated and "public-only" would be exactly the exposure forbidden. 210 let ctx: *NxAuthContext = sys_mmap(OD_CTX_SZ) as *NxAuthContext 211 var auth_off: i64 = 0 212 if olg_ctx_setup(ctx, keys, store, realm, of_slen(realm), OD_REALM_LABEL, of_slen(OD_REALM_LABEL), OD_KSF_M, OD_KSF_T, OD_KSF_P) != 0 { 213 auth_off = 1 214 if mode == OD_MODE_CLOSED { p("NX-OFFICE-DAEMON closed mode and the auth ctx is unavailable -- refusing to start (fail loud, never public)\n" as *u8); return 1 } 215 p("NX-OFFICE-DAEMON auth ctx unavailable -> PUBLIC-ONLY mode (owned docs locked)\n" as *u8) 216 } 217 let addr: *u8 = sys_mmap(OD_ADDR_SZ) 218 d_addr(addr, port) 219 let lfd: i64 = sys_socket(AF_INET, SOCK_STREAM, 0) 220 if lfd < 0 { p("NX-OFFICE-DAEMON socket FAILED -- fail loud\n" as *u8); return 1 } 221 let one: *i64 = (sys_mmap(8)) as *i64 222 one[0] = 1 223 sys_setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, one as *u8, 4) 224 if sys_bind(lfd, addr, OD_ADDR_SZ) < 0 { p("NX-OFFICE-DAEMON bind FAILED (port busy?) -- fail loud\n" as *u8); return 1 } 225 if sys_listen(lfd, OD_LISTEN_BACKLOG) < 0 { p("NX-OFFICE-DAEMON listen FAILED -- fail loud\n" as *u8); return 1 } 226 let wcap: i64 = od_worker_cap() 227 p("NX-OFFICE-DAEMON serving port=" as *u8); pn(port); p(" root=" as *u8); p(root); p(" base=" as *u8); p(base); p(" realm=" as *u8); p(realm) 228 if mode == OD_MODE_CLOSED { p(" mode=closed (every request needs a session)" as *u8) } else { p(" mode=open (unowned docs public)" as *u8) } 229 p(" authz=" as *u8); p(az); p(" AUTHZ=nx_rebac fork-per-request workers=" as *u8); pn(wcap); p(" ncpu=" as *u8); pn(sl_ncpu()); p("\n" as *u8) 230 231 let reqb: *u8 = sys_mmap(OD_REQCAP) 232 let resb: *u8 = sys_mmap(OD_RESCAP) 233 let hbuf: *u8 = sys_mmap(OD_HANDLE_CAP) 234 let tokb: *u8 = sys_mmap(OD_TOK_CAP) 235 let qsb: *u8 = sys_mmap(OD_TOK_CAP) 236 let uid: *u8 = sys_mmap(OD_UID_CAP) 237 let uidn: *i64 = sys_mmap(OD_I64_SZ) as *i64 238 let wst: *i64 = sys_mmap(OD_I64_SZ) as *i64 239 var live: i64 = 0 240 var go: i64 = 1 241 while go == 1 { 242 let cfd: i64 = sys_accept(lfd) 243 if cfd >= 0 { 244 // DoS-starvation bound (nx_dos_timeout_scan seq321): one peer declaring a body it never finishes 245 // sending would otherwise hold its child forever. ACCEPT_TMO_S is the shared named bound. 246 sys_set_socket_timeout(cfd, ACCEPT_TMO_S) 247 let kid: i64 = sys_fork() 248 if kid == 0 { 249 sys_close(lfd) 250 let rn: i64 = of_read_req(cfd, reqb, OD_REQCAP) 251 if rn > 0 { 252 hbuf[0] = 0 as u8; tokb[0] = 0 as u8 253 if auth_off == 0 { od_session(ctx, reqb, rn, tokb, qsb, uid, uidn, idx, hbuf) } 254 var on: i64 = 0 255 if mode == OD_MODE_CLOSED { if hbuf[0] == (0 as u8) { 256 on = of_err(resb, "401 Unauthorized" as *u8, "this office instance is closed: sign in at the firm portal (admin.<domain>) and open the office from there, or send X-Nishi-Session" as *u8) 257 } } 258 if on == 0 { on = of_handle_auth(root, base, reqb, rn, resb, OD_RESCAP, hbuf, az, tokb) } 259 if on > 0 { on = of_propagate_tok(resb, on, base, tokb, OD_RESCAP) } 260 if on > 0 { of_write_all(cfd, resb, on) } 261 } 262 sys_close(cfd) 263 sys_exit(0) 264 } 265 sys_close(cfd) 266 if kid > 0 { live = live + 1 } 267 live = live - od_reap_done(wst) 268 // block ONLY at the ceiling, and only until one slot frees: backpressure, not serialization 269 while live >= wcap { 270 if sys_wait4(0 - 1, wst, 0) > 0 { live = live - 1 } else { live = 0 } 271 } 272 } 273 } 274 return 0 275}