code wiki / _hdl_build / nx_project_serve.nx

nx_project_serve.nx source

↩ module page · 738 lines · 30928 B

1// nx_project_serve.nx -- the /project GATEWAY core (pure request->response; sockets live in the _daemon 2// wrapper, the gate drives THIS core with real tokens and no sockets -- the relate_serve idiom). 3// Sits behind the sites.elf proxy row `nishifamily.com /project 8028 gated` (fail-closed 302 on down). 4// Per request: resolve the OPAQUE no-cookie X-Nishi-Session token -> uid (olg_whoami, realm 5// nishi_site_admin, the SAME keys/store the /login minter uses) -> handle (uid-hex over the login 6// daemon's index file) -> person (data-driven map rows in serve.conf) -> the ROLE-SCOPED project page 7// via pj_html_build (owner/team all tasks, contractor own-only, non-member 403 DENY -- fail-closed). 8// Token-less top-level navigation -> the sessionStorage bootstrap (a bare 302 would loop: no-cookie navs 9// carry no header). Present-but-invalid token -> 302 /login. /project/demo -> the public demo tenant. 10// serve.conf (hot, per-request): `store <prefix>` `pid <id>` `demo <person>` `map <handle> <person>`. 11// license_tier: ORIGINAL 12import "nx_opaque_login.nx" 13import "nx_hub_gw_decide.nx" 14import "nx_site_lock_lib.nx" 15import "nx_project.nx" 16const PSV_MAGIC_719468: i64 = 719468 17const PSV_MAGIC_146097: i64 = 146097 18const PSV_MAGIC_146096: i64 = 146096 19const PSV_MAGIC_1460: i64 = 1460 20const PSV_MAGIC_36524: i64 = 36524 21const PSV_MAGIC_86400: i64 = 86400 22const PSV_MAGIC_65536: i64 = 65536 23const PSV_MAGIC_8192: i64 = 8192 24const PSV_MAGIC_262144: i64 = 262144 25const PSV_MAGIC_1024: i64 = 1024 26const PSV_MAGIC_16384: i64 = 16384 27const PSV_MAGIC_131072: i64 = 131072 28const PSV_MAGIC_16400: i64 = 16400 29const PSV_MAGIC_16383: i64 = 16383 30 31const PSV_BOOTSTRAP: *u8 = "<!DOCTYPE html><html lang='en'><head><meta charset='utf-8'><meta name='viewport' content='width=device-width, initial-scale=1'><title>Nishi Project</title></head><body><script>(function(){var t=sessionStorage.getItem('nsess');if(!t){location='/login?return=/project';return}fetch(location.pathname,{headers:{'X-Nishi-Session':t}}).then(function(r){return r.ok?r.text():null}).then(function(x){if(x){document.open();document.write(x);document.close()}else{location='/login?return=/project'}})})();</script></body></html>" 32const PSV_DENY: *u8 = "<!DOCTYPE html><html lang='en'><head><meta charset='utf-8'><title>Nishi Project - no access</title></head><body><h1>Not a member of this project</h1><p>Your login is valid, but this account has no membership record on the project. Ask the project owner to add you.</p><p><a href='/login'>switch account</a></p></body></html>" 33 34// leak-free bounded read into a REUSED caller buffer; returns bytes (>=0) or -1 (hub gw discipline) 35func ps_read_file(path: *u8, out: *u8, cap: i64) -> i64 { 36 let fd: i64 = sys_openat_rd(path) 37 if fd < 0 { return 0 - 1 } 38 var total: i64 = 0 39 var go: i64 = 1 40 while go == 1 { 41 let base: i64 = out as i64 42 let tail: *u8 = (base + total) as *u8 43 let nr: i64 = sys_read(fd, tail, cap - total) 44 if nr <= 0 { go = 0 } 45 if nr > 0 { total = total + nr } 46 if total >= cap { go = 0 } 47 } 48 sys_close(fd) 49 return total 50} 51 52func ps_hex(dst: *u8, off: i64, src: *u8, n: i64) -> i64 { 53 let hx: *u8 = "0123456789abcdef" as *u8 54 var o: i64 = off 55 var i: i64 = 0 56 while i < n { 57 let c: i64 = (src[i] as i64) & 0xff 58 dst[o] = hx[(c >> 4) & 15] 59 dst[o + 1] = hx[c & 15] 60 o = o + 2 61 i = i + 1 62 } 63 return o 64} 65 66// uidhex -> handle from the login daemon's index (field0=uidhex field1=handle); returns handle len or 0 67func ps_idx_lookup(idxbuf: *u8, idxlen: i64, uidhex: *u8, uxn: i64, out_h: *u8) -> i64 { 68 let fs: *i64 = sys_mmap(8) as *i64 69 let fe: *i64 = sys_mmap(8) as *i64 70 var ls: i64 = 0 71 var found: i64 = 0 72 while ls < idxlen { 73 let le: i64 = slk_line_end(idxbuf, idxlen, ls) 74 if found == 0 { if le > ls { if idxbuf[ls] != (35 as u8) { 75 if slk_field(idxbuf, ls, le, 0, fs, fe) == 1 { 76 if slk_eq(slk_at(idxbuf, fs[0]), fe[0] - fs[0], uidhex, uxn) == 1 { 77 if slk_field(idxbuf, ls, le, 1, fs, fe) == 1 { 78 var o: i64 = 0 79 let hl: i64 = fe[0] - fs[0] 80 while o < hl { if o < 127 { out_h[o] = idxbuf[fs[0] + o] } o = o + 1 } 81 out_h[o] = 0 as u8 82 found = hl 83 } 84 } 85 } 86 } } } 87 ls = le + 1 88 } 89 return found 90} 91 92// conf pair: first line `<key> <v1> <v2>` -> both tokens; 1 if found (e.g. `demo <pid> <person>`) 93func ps_conf_pair(b: *u8, n: i64, key: *u8, out1: *u8, out2: *u8) -> i64 { 94 out1[0] = 0 as u8 95 out2[0] = 0 as u8 96 let t0: *u8 = sys_mmap(128) 97 var i: i64 = 0 98 var found: i64 = 0 99 while i < n { 100 var e: i64 = i 101 var go: i64 = 1 102 while go == 1 { 103 if e >= n { go = 0 } else { 104 if b[e] == (10 as u8) { go = 0 } else { e = e + 1 } 105 } 106 } 107 if found == 0 { 108 var p: i64 = pj_tok(b, i, e, t0, 128) 109 if pj_eq(t0, key) == 1 { 110 p = pj_tok(b, p, e, out1, 128) 111 p = pj_tok(b, p, e, out2, 128) 112 if out1[0] != (0 as u8) { if out2[0] != (0 as u8) { found = 1 } } 113 } 114 } 115 i = e + 1 116 } 117 return found 118} 119 120// project row `project <pid> <store-prefix>` -> store prefix for pid; 1 if found 121func ps_proj_store(b: *u8, n: i64, pid: *u8, out: *u8, cap: i64) -> i64 { 122 out[0] = 0 as u8 123 let t0: *u8 = sys_mmap(128) 124 let t1: *u8 = sys_mmap(128) 125 var i: i64 = 0 126 var found: i64 = 0 127 while i < n { 128 var e: i64 = i 129 var go: i64 = 1 130 while go == 1 { 131 if e >= n { go = 0 } else { 132 if b[e] == (10 as u8) { go = 0 } else { e = e + 1 } 133 } 134 } 135 if found == 0 { 136 var p: i64 = pj_tok(b, i, e, t0, 128) 137 if pj_eq(t0, "project" as *u8) == 1 { 138 p = pj_tok(b, p, e, t1, 128) 139 if pj_eq(t1, pid) == 1 { 140 p = pj_tok(b, p, e, out, cap) 141 if out[0] != (0 as u8) { found = 1 } 142 } 143 } 144 } 145 i = e + 1 146 } 147 return found 148} 149 150// pid charset guard: 1..40 of [a-z0-9_-] (fail-closed; the path segment is caller data) 151func ps_pid_ok(s: *u8) -> i64 { 152 var i: i64 = 0 153 while s[i] != (0 as u8) { 154 let c: i64 = s[i] as i64 155 var okc: i64 = 0 156 if c >= 97 { if c <= 122 { okc = 1 } } 157 if c >= 48 { if c <= 57 { okc = 1 } } 158 if c == 95 { okc = 1 } 159 if c == 45 { okc = 1 } 160 if okc == 0 { return 0 } 161 i = i + 1 162 } 163 if i < 1 { return 0 } 164 if i > 40 { return 0 } 165 return 1 166} 167 168// the PICKER page: every `project` conf row where person is a member -> a link card. 169// returns page len, or -2 when person belongs to no project (deny). 170func ps_picker(confbuf: *u8, cl: i64, person: *u8, hb: *u8) -> i64 { 171 let t0: *u8 = sys_mmap(128) 172 let t1: *u8 = sys_mmap(128) 173 let t2: *u8 = sys_mmap(256) 174 var o: i64 = 0 175 o = pj_head(hb, o, "Nishi Projects" as *u8) 176 o = pj_cat(hb, o, "<h1>Nishi Projects</h1><p class='meta'>signed in as " as *u8) 177 o = pj_cat(hb, o, person) 178 o = pj_cat(hb, o, " - you see only projects where you hold a membership record</p>" as *u8) 179 var nproj: i64 = 0 180 var i: i64 = 0 181 while i < cl { 182 var e: i64 = i 183 var go: i64 = 1 184 while go == 1 { 185 if e >= cl { go = 0 } else { 186 if confbuf[e] == (10 as u8) { go = 0 } else { e = e + 1 } 187 } 188 } 189 var p: i64 = pj_tok(confbuf, i, e, t0, 128) 190 if pj_eq(t0, "project" as *u8) == 1 { 191 p = pj_tok(confbuf, p, e, t1, 128) 192 p = pj_tok(confbuf, p, e, t2, 256) 193 if t1[0] != (0 as u8) { if t2[0] != (0 as u8) { 194 let stg: *i64 = pj_st_new() 195 if pj_gather(t2, t1, stg) == 0 { if stg[12] == 1 { 196 let role: i64 = pj_roleof(stg, person) 197 if role != 0 { 198 o = pj_cat(hb, o, "<a class='card' href='/project/p/" as *u8) 199 o = pj_cat(hb, o, t1) 200 o = pj_cat(hb, o, "'>" as *u8) 201 o = pj_cat(hb, o, stg[14] as *u8) 202 o = pj_cat(hb, o, " <span class='meta'>(" as *u8) 203 o = pj_cat(hb, o, t1) 204 o = pj_cat(hb, o, " - your role: " as *u8) 205 o = pj_cat(hb, o, role as *u8) 206 o = pj_cat(hb, o, ")</span></a>" as *u8) 207 nproj = nproj + 1 208 } 209 } } 210 } } 211 } 212 i = e + 1 213 } 214 if nproj == 0 { return 0 - 2 } 215 o = pj_foot(hb, o) 216 return o 217} 218 219// map row `map <handle> <person>`; absent -> person = handle (natural handle==person default) 220func ps_map(b: *u8, n: i64, handle: *u8, out: *u8, cap: i64) -> i64 { 221 let t0: *u8 = sys_mmap(128) 222 let t1: *u8 = sys_mmap(128) 223 var i: i64 = 0 224 var found: i64 = 0 225 while i < n { 226 var e: i64 = i 227 var go: i64 = 1 228 while go == 1 { 229 if e >= n { go = 0 } else { 230 if b[e] == (10 as u8) { go = 0 } else { e = e + 1 } 231 } 232 } 233 if found == 0 { 234 var p: i64 = pj_tok(b, i, e, t0, 128) 235 if pj_eq(t0, "map" as *u8) == 1 { 236 p = pj_tok(b, p, e, t1, 128) 237 if pj_eq(t1, handle) == 1 { 238 p = pj_tok(b, p, e, out, cap) 239 if out[0] != (0 as u8) { found = 1 } 240 } 241 } 242 } 243 i = e + 1 244 } 245 if found == 0 { 246 var k: i64 = 0 247 var go2: i64 = 1 248 while go2 == 1 { 249 out[k] = handle[k] 250 if handle[k] == (0 as u8) { go2 = 0 } else { k = k + 1 } 251 } 252 } 253 return found 254} 255 256// epoch-days -> civil (Howard Hinnant algorithm, mirrors the proven planner math) 257func ps_civil(z0: i64, out: *i64) -> i64 { 258 var z: i64 = z0 + PSV_MAGIC_719468 259 var era: i64 = z / PSV_MAGIC_146097 260 if z < 0 { era = (z - PSV_MAGIC_146096) / PSV_MAGIC_146097 } 261 let doe: i64 = z - era * PSV_MAGIC_146097 262 let yoe: i64 = (doe - doe / PSV_MAGIC_1460 + doe / PSV_MAGIC_36524 - doe / PSV_MAGIC_146096) / 365 263 let y0: i64 = yoe + era * 400 264 let doy: i64 = doe - (365 * yoe + yoe / 4 - yoe / 100) 265 let mp: i64 = (5 * doy + 2) / 153 266 let d: i64 = doy - (153 * mp + 2) / 5 + 1 267 var m: i64 = mp + 3 268 if mp >= 10 { m = mp - 9 } 269 var y: i64 = y0 270 if m <= 2 { y = y0 + 1 } 271 out[0] = y 272 out[1] = m 273 out[2] = d 274 return 0 275} 276 277func ps_today(out: *u8) -> i64 { 278 let secs: i64 = sys_now_realtime_sec() 279 let days: i64 = secs / PSV_MAGIC_86400 280 let ymd: *i64 = sys_mmap(32) as *i64 281 ps_civil(days, ymd) 282 var o: i64 = 0 283 o = pj_catn(out, o, ymd[0]) 284 out[o] = 45 as u8 285 o = o + 1 286 if ymd[1] < 10 { out[o] = 48 as u8; o = o + 1 } 287 o = pj_catn(out, o, ymd[1]) 288 out[o] = 45 as u8 289 o = o + 1 290 if ymd[2] < 10 { out[o] = 48 as u8; o = o + 1 } 291 o = pj_catn(out, o, ymd[2]) 292 out[o] = 0 as u8 293 return o 294} 295 296// build a full HTTP response into resp; returns total bytes 297func ps_resp(resp: *u8, status: *u8, ctype: *u8, body: *u8, blen: i64) -> i64 { 298 var o: i64 = 0 299 o = pj_cat(resp, o, "HTTP/1.1 " as *u8) 300 o = pj_cat(resp, o, status) 301 o = pj_cat(resp, o, "\r\nContent-Type: " as *u8) 302 o = pj_cat(resp, o, ctype) 303 o = pj_cat(resp, o, "\r\nContent-Length: " as *u8) 304 o = pj_catn(resp, o, blen) 305 o = pj_cat(resp, o, "\r\nConnection: close\r\nCache-Control: no-store\r\n\r\n" as *u8) 306 var i: i64 = 0 307 while i < blen { resp[o] = body[i]; o = o + 1; i = i + 1 } 308 return o 309} 310 311func ps_redirect(resp: *u8, loc: *u8) -> i64 { 312 var o: i64 = 0 313 o = pj_cat(resp, o, "HTTP/1.1 302 Found\r\nLocation: " as *u8) 314 o = pj_cat(resp, o, loc) 315 o = pj_cat(resp, o, "\r\nContent-Length: 0\r\nConnection: close\r\nCache-Control: no-store\r\n\r\n" as *u8) 316 return o 317} 318 319func ps_text(resp: *u8, status: *u8, msg: *u8) -> i64 { 320 return ps_resp(resp, status, "text/plain; charset=utf-8" as *u8, msg, pj_slen(msg)) 321} 322 323// ---- st slots: 0=ctx 1=confpath 2=idxpath 3=pagebuf 4=confbuf 5=idxbuf 6=tokbuf 7=pathbuf 324// 8=uid 9=uxbuf 10=handle 11=person 12=todaybuf 13=storebuf 14=pidbuf 15=demobuf 16=uidn ---- 325func psv_st_new(ctx: *NxAuthContext, confpath: *u8, idxpath: *u8) -> *i64 { 326 let st: *i64 = sys_mmap(8 * 20) as *i64 327 st[0] = ctx as i64 328 st[1] = confpath as i64 329 st[2] = idxpath as i64 330 st[3] = sys_mmap(PSV_MAGIC_65536) 331 st[4] = sys_mmap(PSV_MAGIC_8192) 332 st[5] = sys_mmap(PSV_MAGIC_262144) 333 st[6] = sys_mmap(512) 334 st[7] = sys_mmap(PSV_MAGIC_1024) 335 st[8] = sys_mmap(64) 336 st[9] = sys_mmap(160) 337 st[10] = sys_mmap(128) 338 st[11] = sys_mmap(128) 339 st[12] = sys_mmap(32) 340 st[13] = sys_mmap(256) 341 st[14] = sys_mmap(128) 342 st[15] = sys_mmap(128) 343 st[16] = sys_mmap(16) 344 return st 345} 346 347func psv_handle(st: *i64, req: *u8, rn: i64, resp: *u8) -> i64 { 348 let path: *u8 = st[7] as *u8 349 var pl: i64 = hgw_path(req, rn, path, PSV_MAGIC_1024) 350 // strip the query string: the edge relays the FULL request-target (/project?b=x) and exact-match 351 // routing would 404 it (found live: WebFetch cache-busters). '?' truncates. 352 var qi: i64 = 0 353 var qgo: i64 = 1 354 while qgo == 1 { 355 if qi >= pl { qgo = 0 } else { 356 if path[qi] == (63 as u8) { path[qi] = 0 as u8; pl = qi; qgo = 0 } else { qi = qi + 1 } 357 } 358 } 359 let confbuf: *u8 = st[4] as *u8 360 let cl: i64 = ps_read_file(st[1] as *u8, confbuf, PSV_MAGIC_8192) 361 if cl <= 0 { return ps_text(resp, "503 Service Unavailable" as *u8, "project portal not configured" as *u8) } 362 let today: *u8 = st[12] as *u8 363 ps_today(today) 364 let pagebuf: *u8 = st[3] as *u8 365 let storebuf: *u8 = st[13] as *u8 366 // ---- /project/demo = the PUBLIC demo tenant (conf: `demo <pid> <person>`) ---- 367 var isdemo: i64 = 0 368 if slk_eq(path, pl, "/project/demo" as *u8, 13) == 1 { isdemo = 1 } 369 if slk_eq(path, pl, "/project/demo/" as *u8, 14) == 1 { isdemo = 1 } 370 if isdemo == 1 { 371 let dpid: *u8 = st[14] as *u8 372 let dper: *u8 = st[15] as *u8 373 if ps_conf_pair(confbuf, cl, "demo" as *u8, dpid, dper) == 0 { return ps_text(resp, "404 Not Found" as *u8, "no public demo configured" as *u8) } 374 if ps_proj_store(confbuf, cl, dpid, storebuf, 256) == 0 { return ps_text(resp, "503 Service Unavailable" as *u8, "demo project not in conf" as *u8) } 375 let dn: i64 = pj_html_build(storebuf, dpid, dper, today, pagebuf) 376 if dn < 0 { return ps_text(resp, "503 Service Unavailable" as *u8, "demo unavailable" as *u8) } 377 return ps_resp(resp, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, pagebuf, dn) 378 } 379 // ---- route shape: the picker root, or a per-project page /project/p/<pid> ---- 380 var isroot: i64 = 0 381 if slk_eq(path, pl, "/project" as *u8, 8) == 1 { isroot = 1 } 382 if slk_eq(path, pl, "/project/" as *u8, 9) == 1 { isroot = 1 } 383 var ispage: i64 = 0 384 if isroot == 0 { if pl > 11 { if pj_starts(path, "/project/p/" as *u8) == 1 { ispage = 1 } } } 385 if isroot == 0 { if ispage == 0 { return ps_text(resp, "404 Not Found" as *u8, "not found" as *u8) } } 386 // ---- session resolution (fail-closed; token-less nav gets the sessionStorage bootstrap) ---- 387 let tok: *u8 = st[6] as *u8 388 let tl: i64 = hgw_hdr_val(req, rn, "X-Nishi-Session:" as *u8, 16, tok, 512) 389 if tl == 0 { return ps_resp(resp, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, PSV_BOOTSTRAP, pj_slen(PSV_BOOTSTRAP)) } 390 let ctx: *NxAuthContext = st[0] as *NxAuthContext 391 let uid: *u8 = st[8] as *u8 392 let uidn: *i64 = st[16] as *i64 393 let handle: *u8 = st[10] as *u8 394 var hl: i64 = 0 395 let now: i64 = sys_now_realtime_sec() 396 if olg_whoami(ctx, tok, tl, now, uid, 64, uidn) == NX_MAUTH_OK { 397 let uxbuf: *u8 = st[9] as *u8 398 let uxn: i64 = ps_hex(uxbuf, 0, uid, uidn[0]) 399 let idxbuf: *u8 = st[5] as *u8 400 let il: i64 = ps_read_file(st[2] as *u8, idxbuf, PSV_MAGIC_262144) 401 if il > 0 { hl = ps_idx_lookup(idxbuf, il, uxbuf, uxn, handle) } 402 } 403 if hl == 0 { return ps_redirect(resp, "/login?return=/project" as *u8) } 404 let person: *u8 = st[11] as *u8 405 ps_map(confbuf, cl, handle, person, 128) 406 // ---- the PICKER: projects where this person holds a membership record ---- 407 if isroot == 1 { 408 let kn: i64 = ps_picker(confbuf, cl, person, pagebuf) 409 if kn >= 0 { return ps_resp(resp, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, pagebuf, kn) } 410 return ps_resp(resp, "403 Forbidden" as *u8, "text/html; charset=utf-8" as *u8, PSV_DENY, pj_slen(PSV_DENY)) 411 } 412 // ---- per-project page: /project/p/<pid> (unknown pid = 404, non-member = 403) ---- 413 let pidp: *u8 = ((path as i64) + 11) as *u8 414 if ps_pid_ok(pidp) == 0 { return ps_text(resp, "404 Not Found" as *u8, "not found" as *u8) } 415 if ps_proj_store(confbuf, cl, pidp, storebuf, 256) == 0 { return ps_text(resp, "404 Not Found" as *u8, "no such project" as *u8) } 416 let n2: i64 = pj_html_build(storebuf, pidp, person, today, pagebuf) 417 if n2 >= 0 { return ps_resp(resp, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, pagebuf, n2) } 418 if n2 == 0 - 2 { return ps_resp(resp, "403 Forbidden" as *u8, "text/html; charset=utf-8" as *u8, PSV_DENY, pj_slen(PSV_DENY)) } 419 return ps_text(resp, "503 Service Unavailable" as *u8, "project store unavailable" as *u8) 420} 421 422// ---------------- SELFTEST (socket-free; REAL tokens against fresh auto-init keys) ---------------- 423func psv_req(dst: *u8, path: *u8, tok: *u8) -> i64 { 424 var o: i64 = 0 425 o = pj_cat(dst, o, "GET " as *u8) 426 o = pj_cat(dst, o, path) 427 o = pj_cat(dst, o, " HTTP/1.1\r\nHost: t\r\n" as *u8) 428 if tok[0] != (0 as u8) { 429 o = pj_cat(dst, o, "X-Nishi-Session: " as *u8) 430 o = pj_cat(dst, o, tok) 431 o = pj_cat(dst, o, "\r\n" as *u8) 432 } 433 o = pj_cat(dst, o, "\r\n" as *u8) 434 return o 435} 436 437func psv_selftest() -> i64 { 438 let uniq: i64 = sys_now_us() 439 let base: *u8 = sys_mmap(256) 440 var bo: i64 = 0 441 bo = pj_cat(base, bo, "/tmp/psg_" as *u8) 442 bo = pj_catn(base, bo, uniq) 443 base[bo] = 0 as u8 444 let keys: *u8 = sys_mmap(256) 445 var o: i64 = pj_cat(keys, 0, base) 446 o = pj_cat(keys, o, "_keys.bin" as *u8) 447 keys[o] = 0 as u8 448 let astore: *u8 = sys_mmap(256) 449 o = pj_cat(astore, 0, base) 450 o = pj_cat(astore, o, "_astore.log" as *u8) 451 astore[o] = 0 as u8 452 let idxf: *u8 = sys_mmap(256) 453 o = pj_cat(idxf, 0, base) 454 o = pj_cat(idxf, o, "_idx.tsv" as *u8) 455 idxf[o] = 0 as u8 456 let conff: *u8 = sys_mmap(256) 457 o = pj_cat(conff, 0, base) 458 o = pj_cat(conff, o, "_serve.conf" as *u8) 459 conff[o] = 0 as u8 460 let planf: *u8 = sys_mmap(256) 461 o = pj_cat(planf, 0, base) 462 o = pj_cat(planf, o, "_plan.txt" as *u8) 463 planf[o] = 0 as u8 464 let projf: *u8 = sys_mmap(256) 465 o = pj_cat(projf, 0, base) 466 o = pj_cat(projf, o, "_proj.txt" as *u8) 467 projf[o] = 0 as u8 468 let prefix: *u8 = sys_mmap(256) 469 o = pj_cat(prefix, 0, base) 470 o = pj_cat(prefix, o, "_stA-" as *u8) 471 prefix[o] = 0 as u8 472 let prefB: *u8 = sys_mmap(256) 473 o = pj_cat(prefB, 0, base) 474 o = pj_cat(prefB, o, "_stB-" as *u8) 475 prefB[o] = 0 as u8 476 let planb: *u8 = sys_mmap(256) 477 o = pj_cat(planb, 0, base) 478 o = pj_cat(planb, o, "_planb.txt" as *u8) 479 planb[o] = 0 as u8 480 let projb: *u8 = sys_mmap(256) 481 o = pj_cat(projb, 0, base) 482 o = pj_cat(projb, o, "_projb.txt" as *u8) 483 projb[o] = 0 as u8 484 let outf: *u8 = sys_mmap(256) 485 o = pj_cat(outf, 0, base) 486 o = pj_cat(outf, o, "_run.out" as *u8) 487 outf[o] = 0 as u8 488 489 pj_wfile(planf, "TASK fence f0 amber 2026-07-01 30 1 - permits\nTASK fence f1 amber 2026-07-20 200 3 - posts\nTASK fence f2 bo 2026-07-18 100 4 - rails\nTASK fence f3 bo 2026-07-19 80 2 f2 gates\nDONE fence f1 2026-07-08\n" as *u8) 490 pj_wfile(projf, "PROJ fence elder fence-build\nMEMBER fence amber team 240\nMEMBER fence bo contractor 120\nWCONF fence 120 1\n" as *u8) 491 pj_wfile(planb, "TASK barn b1 amber 2027-01-01 60 3 - paint\n" as *u8) 492 pj_wfile(projb, "PROJ barn elder barn-paint\nMEMBER barn amber team -\n" as *u8) 493 let confc: *u8 = sys_mmap(PSV_MAGIC_1024) 494 o = 0 495 o = pj_cat(confc, o, "project fence " as *u8) 496 o = pj_cat(confc, o, prefix) 497 o = pj_cat(confc, o, "\nproject barn " as *u8) 498 o = pj_cat(confc, o, prefB) 499 o = pj_cat(confc, o, "\ndemo fence elder\nmap bo_test bo\n" as *u8) 500 confc[o] = 0 as u8 501 pj_wfile(conff, confc) 502 503 var pass: i64 = 0 504 // T1 the independent planner writes the task records for BOTH projects 505 let a1: *i64 = sys_mmap(8 * 8) as *i64 506 a1[0] = "load" as *u8 as i64 507 a1[1] = planf as i64 508 a1[2] = prefix as i64 509 a1[3] = "7301" as *u8 as i64 510 a1[4] = 0 511 let rc1: i64 = pj_run("_offc/nx_vizsla_plan.elf" as *u8, a1, outf) 512 let a1b: *i64 = sys_mmap(8 * 8) as *i64 513 a1b[0] = "load" as *u8 as i64 514 a1b[1] = planb as i64 515 a1b[2] = prefB as i64 516 a1b[3] = "7303" as *u8 as i64 517 a1b[4] = 0 518 let rc1b: i64 = pj_run("_offc/nx_vizsla_plan.elf" as *u8, a1b, outf) 519 let av: *i64 = sys_mmap(8 * 8) as *i64 520 av[0] = "pj" as *u8 as i64 521 av[1] = "load" as *u8 as i64 522 av[2] = projf as i64 523 av[3] = prefix as i64 524 av[4] = "7302" as *u8 as i64 525 let rc2: i64 = pj_cmd_load(5, av) 526 let avb: *i64 = sys_mmap(8 * 8) as *i64 527 avb[0] = "pj" as *u8 as i64 528 avb[1] = "load" as *u8 as i64 529 avb[2] = projb as i64 530 avb[3] = prefB as i64 531 avb[4] = "7304" as *u8 as i64 532 let rc2b: i64 = pj_cmd_load(5, avb) 533 var ok: i64 = 0 534 if rc1 == 0 { if rc1b == 0 { if rc2 == 0 { if rc2b == 0 { ok = 1 } } } } 535 pass = pass + pj_check(1, ok, "fixtures-loaded-2-projects" as *u8) 536 // T2 auth context on FRESH keys (load_or_init) + register + login = REAL tokens 537 let ctx: *NxAuthContext = sys_mmap(256) as *NxAuthContext 538 let crc: i64 = olg_ctx_setup(ctx, keys, astore, "nishi_site_admin" as *u8, 16, "Nishi site admin" as *u8, 16, 256, 2, 1) 539 let mn: *u8 = sys_mmap(PSV_MAGIC_1024) 540 let mnn: *i64 = sys_mmap(16) as *i64 541 let rr1: i64 = olg_register(ctx, "bo_test" as *u8, 7, "pw_bo_9931x" as *u8, 11, mn, PSV_MAGIC_1024, mnn) 542 let rr2: i64 = olg_register(ctx, "mallory_t" as *u8, 9, "pw_mal_442x" as *u8, 11, mn, PSV_MAGIC_1024, mnn) 543 let tokb: *u8 = sys_mmap(512) 544 let tbn: *i64 = sys_mmap(16) as *i64 545 let lr1: i64 = olg_login(ctx, "bo_test" as *u8, 7, "pw_bo_9931x" as *u8, 11, tokb, 512, tbn) 546 let tokm: *u8 = sys_mmap(512) 547 let tmn: *i64 = sys_mmap(16) as *i64 548 let lr2: i64 = olg_login(ctx, "mallory_t" as *u8, 9, "pw_mal_442x" as *u8, 11, tokm, 512, tmn) 549 tokb[tbn[0]] = 0 as u8 550 tokm[tmn[0]] = 0 as u8 551 ok = 0 552 if crc == 0 { if rr1 == 0 { if rr2 == 0 { if lr1 == 0 { if lr2 == 0 { ok = 1 } } } } } 553 pass = pass + pj_check(2, ok, "opaque-register-login" as *u8) 554 // build the uid->handle index from the REAL whoami uids 555 let uid: *u8 = sys_mmap(64) 556 let uidn: *i64 = sys_mmap(16) as *i64 557 let now: i64 = sys_now_realtime_sec() 558 let idxc: *u8 = sys_mmap(PSV_MAGIC_1024) 559 var io: i64 = 0 560 if olg_whoami(ctx, tokb, tbn[0], now, uid, 64, uidn) == NX_MAUTH_OK { 561 io = ps_hex(idxc, io, uid, uidn[0]) 562 idxc[io] = 9 as u8 563 io = io + 1 564 io = pj_cat(idxc, io, "bo_test\n" as *u8) 565 } 566 if olg_whoami(ctx, tokm, tmn[0], now, uid, 64, uidn) == NX_MAUTH_OK { 567 io = ps_hex(idxc, io, uid, uidn[0]) 568 idxc[io] = 9 as u8 569 io = io + 1 570 io = pj_cat(idxc, io, "mallory_t\n" as *u8) 571 } 572 idxc[io] = 0 as u8 573 pj_wfile(idxf, idxc) 574 // the handler state 575 let st: *i64 = psv_st_new(ctx, conff, idxf) 576 let req: *u8 = sys_mmap(PSV_MAGIC_16384) 577 let resp: *u8 = sys_mmap(PSV_MAGIC_131072) 578 let none: *u8 = sys_mmap(8) 579 none[0] = 0 as u8 580 // T3 token-less nav -> bootstrap 200 581 var rn: i64 = psv_req(req, "/project" as *u8, none) 582 var n: i64 = psv_handle(st, req, rn, resp) 583 ok = 0 584 if pj_contains(resp, n, "HTTP/1.1 200" as *u8) == 1 { if pj_contains(resp, n, "sessionStorage" as *u8) == 1 { ok = 1 } } 585 pass = pass + pj_check(3, ok, "bootstrap-no-token" as *u8) 586 // T4 garbage token -> 302 /login (fail-closed) 587 rn = psv_req(req, "/project" as *u8, "AAAAgarbagegarbage" as *u8) 588 n = psv_handle(st, req, rn, resp) 589 ok = 0 590 if pj_contains(resp, n, "HTTP/1.1 302" as *u8) == 1 { ok = 1 } 591 pass = pass + pj_check(4, ok, "neg-bad-token-302" as *u8) 592 // T5 the PICKER: bo sees fence (member) and NEVER barn (not a member) 593 rn = psv_req(req, "/project" as *u8, tokb) 594 n = psv_handle(st, req, rn, resp) 595 ok = 0 596 if pj_contains(resp, n, "HTTP/1.1 200" as *u8) == 1 { 597 if pj_contains(resp, n, "/project/p/fence" as *u8) == 1 { 598 if pj_contains(resp, n, "/project/p/barn" as *u8) == 0 { ok = 1 } 599 } 600 } 601 pass = pass + pj_check(5, ok, "picker-membership-scoped" as *u8) 602 // T6 per-project page: contractor scope holds on /project/p/fence 603 rn = psv_req(req, "/project/p/fence" as *u8, tokb) 604 n = psv_handle(st, req, rn, resp) 605 ok = 0 606 if pj_contains(resp, n, "HTTP/1.1 200" as *u8) == 1 { 607 if pj_contains(resp, n, "<td>f2</td>" as *u8) == 1 { 608 if pj_contains(resp, n, "<td>f1</td>" as *u8) == 0 { 609 if pj_contains(resp, n, "role=contractor" as *u8) == 1 { ok = 1 } 610 } 611 } 612 } 613 pass = pass + pj_check(6, ok, "page-contractor-scope" as *u8) 614 // T7 CROSS-PROJECT deny: bo on barn -> 403 615 rn = psv_req(req, "/project/p/barn" as *u8, tokb) 616 n = psv_handle(st, req, rn, resp) 617 ok = 0 618 if pj_contains(resp, n, "HTTP/1.1 403" as *u8) == 1 { ok = 1 } 619 pass = pass + pj_check(7, ok, "neg-cross-project-403" as *u8) 620 // T8 valid login, NO membership anywhere -> picker denies 621 rn = psv_req(req, "/project" as *u8, tokm) 622 n = psv_handle(st, req, rn, resp) 623 ok = 0 624 if pj_contains(resp, n, "HTTP/1.1 403" as *u8) == 1 { if pj_contains(resp, n, "Not a member" as *u8) == 1 { ok = 1 } } 625 pass = pass + pj_check(8, ok, "neg-no-membership-403" as *u8) 626 // T9 unknown pid -> 404 (no leak) 627 rn = psv_req(req, "/project/p/ghost" as *u8, tokb) 628 n = psv_handle(st, req, rn, resp) 629 ok = 0 630 if pj_contains(resp, n, "HTTP/1.1 404" as *u8) == 1 { ok = 1 } 631 pass = pass + pj_check(9, ok, "neg-unknown-pid-404" as *u8) 632 // T10 public demo: owner view shows all rows + the risk column/summary (f0 overdue by real today) 633 rn = psv_req(req, "/project/demo" as *u8, none) 634 n = psv_handle(st, req, rn, resp) 635 ok = 0 636 if pj_contains(resp, n, "HTTP/1.1 200" as *u8) == 1 { 637 if pj_contains(resp, n, "<td>f1</td>" as *u8) == 1 { 638 if pj_contains(resp, n, "OVERDUE" as *u8) == 1 { 639 if pj_contains(resp, n, "risk: overdue=" as *u8) == 1 { ok = 1 } 640 } 641 } 642 } 643 pass = pass + pj_check(10, ok, "public-demo-risk-column" as *u8) 644 // T11 unknown subpath -> 404 645 rn = psv_req(req, "/project/zzz" as *u8, none) 646 n = psv_handle(st, req, rn, resp) 647 ok = 0 648 if pj_contains(resp, n, "HTTP/1.1 404" as *u8) == 1 { ok = 1 } 649 pass = pass + pj_check(11, ok, "neg-404-subpath" as *u8) 650 // T12 query string stripped on the picker 651 rn = psv_req(req, "/project?b=cachebust" as *u8, tokb) 652 n = psv_handle(st, req, rn, resp) 653 ok = 0 654 if pj_contains(resp, n, "HTTP/1.1 200" as *u8) == 1 { if pj_contains(resp, n, "/project/p/fence" as *u8) == 1 { ok = 1 } } 655 pass = pass + pj_check(12, ok, "query-string-stripped" as *u8) 656 // T13 pid traversal guard: dot/slash pids are refused 657 rn = psv_req(req, "/project/p/../etc" as *u8, tokb) 658 n = psv_handle(st, req, rn, resp) 659 ok = 0 660 if pj_contains(resp, n, "HTTP/1.1 404" as *u8) == 1 { ok = 1 } 661 pass = pass + pj_check(13, ok, "neg-pid-traversal-404" as *u8) 662 663 let rep: *u8 = sys_mmap(256) 664 var z: i64 = 0 665 z = pj_cat(rep, z, "NX-PROJECT-SERVE-GATE pass=" as *u8) 666 z = pj_catn(rep, z, pass) 667 z = pj_cat(rep, z, "/13 verdict=" as *u8) 668 if pass == 13 { z = pj_cat(rep, z, "GREEN\n" as *u8) } else { z = pj_cat(rep, z, "RED\n" as *u8) } 669 sys_write(1, rep, z) 670 if pass == 13 { return 0 } 671 return 1 672} 673 674// ---------------- DAEMON (sockets; the NAS runs this behind the gated proxy row) ---------------- 675func psv_daemon(argc: i64, argv: *i64) -> i64 { 676 if argc < 7 { 677 pj_p("usage: nx_project_serve <port> <keys> <store> <budget> <conf> <idx> [m] [t] [p]\n" as *u8) 678 sys_exit(2) 679 return 2 680 } 681 let port: i64 = pj_atoi(argv[1] as *u8) 682 let keys_path: *u8 = argv[2] as *u8 683 let store_path: *u8 = argv[3] as *u8 684 let budget: i64 = pj_atoi(argv[4] as *u8) 685 let confpath: *u8 = argv[5] as *u8 686 let idxpath: *u8 = argv[6] as *u8 687 var m_cost: i64 = PSV_MAGIC_65536 688 if argc > 7 { m_cost = pj_atoi(argv[7] as *u8) } 689 var t_cost: i64 = 3 690 if argc > 8 { t_cost = pj_atoi(argv[8] as *u8) } 691 var p_cost: i64 = 4 692 if argc > 9 { p_cost = pj_atoi(argv[9] as *u8) } 693 let ctx: *NxAuthContext = sys_mmap(256) as *NxAuthContext 694 if olg_ctx_setup(ctx, keys_path, store_path, "nishi_site_admin" as *u8, 16, "Nishi site admin" as *u8, 16, m_cost, t_cost, p_cost) != 0 { 695 pj_p("CTX-INIT-FAIL\n" as *u8) 696 sys_exit(1) 697 return 1 698 } 699 let addr: *u8 = sys_mmap(16) 700 addr[0] = 2 as u8 701 addr[1] = 0 as u8 702 addr[2] = ((port >> 8) & 255) as u8 703 addr[3] = (port & 255) as u8 704 var zi: i64 = 4 705 while zi < 16 { addr[zi] = 0 as u8; zi = zi + 1 } 706 let lfd: i64 = sys_socket(2, 1, 0) 707 if lfd < 0 { pj_p("SOCKET-FAIL\n" as *u8); sys_exit(1); return 1 } 708 let optv: *u8 = sys_mmap(4) 709 optv[0] = 1 as u8 710 optv[1] = 0 as u8 711 optv[2] = 0 as u8 712 optv[3] = 0 as u8 713 sys_setsockopt(lfd, 1, 2, optv, 4) 714 if sys_bind(lfd, addr, 16) < 0 { pj_p("BIND-FAIL\n" as *u8); sys_exit(1); return 1 } 715 if sys_listen(lfd, 16) < 0 { pj_p("LISTEN-FAIL\n" as *u8); sys_exit(1); return 1 } 716 pj_p("PROJECT-GATEWAY-UP\n" as *u8) 717 let st: *i64 = psv_st_new(ctx, confpath, idxpath) 718 let req: *u8 = sys_mmap(PSV_MAGIC_16400) 719 let resp: *u8 = sys_mmap(PSV_MAGIC_131072) 720 pj_p("PSV-BUFS-OK\n" as *u8) 721 var served: i64 = 0 722 while served < budget { 723 let cfd: i64 = sys_accept(lfd) 724 if cfd >= 0 { 725 sys_set_socket_timeout(cfd, 5) 726 let rn: i64 = sys_read(cfd, req, PSV_MAGIC_16383) 727 if rn > 0 { 728 let n: i64 = psv_handle(st, req, rn, resp) 729 sys_write(cfd, resp, n) 730 } 731 sys_close(cfd) 732 } 733 served = served + 1 734 } 735 sys_close(lfd) 736 sys_exit(0) 737 return 0 738}