code wiki / _hdl_build / nx_status_daemon.nx

nx_status_daemon.nx source

↩ module page · 289 lines · 15140 B

1// nx_status_daemon.nx -- THE ACCESS WALL for nishifamily.com/status, on the CANONICAL Modern Auth (the 2// SINGLE ecosystem-wide auth: OPAQUE-3DH + Argon2id KSF + BIP39 recovery + NO-COOKIE Ed25519 session). 3// Operator 2026-06-16: "lets have that be how all the sites work." Composes the ONE shared gate 4// (nx_sa_validate) -- no bespoke crypto, no cookies (the prior cms_pw + Set-Cookie build violated the 5// charter's C1 no-cookie cardinal and is gone). A loopback HTTP daemon the sites daemon reverse-proxies to. 6// 7// The router is a PURE FUNCTION sd_handle(ctx, req_bytes, n, statusfile, out) -> out_n -- request bytes in, 8// response bytes out, no socket. The socket loop is a thin shell over it; the gate drives it IN-PROCESS 9// (sovereign Nishi test, no curl/shell). Routes: 10// POST /status/login -> handle=<h>&passphrase=<p> (url-decoded) -> nx_modern_auth_login 11// -> 200 {"token":"<base64 152B>"} | 401 {"error":"unauthorized"} (same for 12// wrong-pw / unknown-handle: client-enumeration defense) 13// GET /status/content -> X-Nishi-Session header validates (nx_sa_validate) ? serve <statusfile> : 401 14// GET /status (+ any) -> the no-cookie SPA shell: login form + fetch() that stores the token in 15// sessionStorage and sends it as X-Nishi-Session (NEVER a cookie) 16// Context armed at startup via nx_uas_server_keys_load_or_init + nx_auth_context_init; the admin is 17// provisioned out-of-band by nx_modauth_arm (this daemon never registers). 18// argv: [1]=port [2]=keysfile [3]=storefile [4]=realm [5]=statusfile [6]=request-budget. 19// Sovereign: nx_site_auth + nx_modern_auth_flow + nx_http_server + nx_base64 + nx_syscalls. license_tier: ORIGINAL 20import "nx_syscalls.nx" 21import "nx_http_server.nx" 22import "nx_site_auth.nx" 23import "hub/nx_modern_auth_flow.nx" 24import "nx_base64.nx" 25const SD_MAGIC_8192: i64 = 8192 26 27const SD_REQCAP: i64 = 65536 28const SD_OUTCAP: i64 = 524288 29 30func sd_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != 0 as u8 { n = n + 1 } return n } 31func sd_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != 0 as u8 { d[o + i] = s[i]; i = i + 1 } return o + i } 32func sd_atoi(s: *u8) -> i64 { var v: i64 = 0; var i: i64 = 0; while s[i] != 0 as u8 { let c: i64 = s[i] as i64; if c >= 48 { if c <= 57 { v = v * 10 + (c - 48) } } i = i + 1 } return v } 33func sd_catn(d: *u8, o: i64, v: i64) -> i64 { 34 let t: *u8 = sys_mmap(24); var m: i64 = v; var k: i64 = 0 35 var w: i64 = o 36 if m < 0 { d[w] = 45 as u8; w = w + 1; m = 0 - m } 37 if m == 0 { t[0] = 48 as u8; k = 1 } 38 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 39 var i: i64 = 0 40 while i < k { d[w] = t[k - 1 - i]; w = w + 1; i = i + 1 } 41 return w 42} 43func sd_starts(req: *u8, n: i64, s: *u8) -> i64 { 44 let sl: i64 = sd_len(s) 45 if n < sl { return 0 } 46 var i: i64 = 0 47 while i < sl { if req[i] != s[i] { return 0 } i = i + 1 } 48 return 1 49} 50 51// ---- request parsing (pure; the gate drives these directly) -------------------------------------- 52 53// path = the slice between the 1st and 2nd ASCII space of "METHOD PATH HTTP/1.1". sets off/len, 1/0. 54func sd_find_path(req: *u8, n: i64, off_box: *i64, len_box: *i64) -> i64 { 55 var sp1: i64 = 0 56 var f1: i64 = 0 57 while f1 == 0 { if sp1 >= n { f1 = 1 } else { if (req[sp1] as i64) == 32 { f1 = 1 } else { sp1 = sp1 + 1 } } } 58 let start: i64 = sp1 + 1 59 var e: i64 = start 60 var f2: i64 = 0 61 while f2 == 0 { if e >= n { f2 = 1 } else { if (req[e] as i64) == 32 { f2 = 1 } else { e = e + 1 } } } 62 off_box[0] = start 63 len_box[0] = e - start 64 if e <= start { return 0 } 65 return 1 66} 67 68// byte offset just past the CRLFCRLF header terminator, or n if there is no body. 69func sd_body_off(req: *u8, n: i64) -> i64 { 70 var i: i64 = 0 71 while i + 4 <= n { 72 if (req[i] as i64) == 13 { if (req[i + 1] as i64) == 10 { if (req[i + 2] as i64) == 13 { if (req[i + 3] as i64) == 10 { return i + 4 } } } } 73 i = i + 1 74 } 75 return n 76} 77 78func sd_hexnib(c: i64) -> i64 { 79 if c >= 48 { if c <= 57 { return c - 48 } } 80 if c >= 97 { if c <= 102 { return c - 87 } } 81 if c >= 65 { if c <= 70 { return c - 55 } } 82 return 0 - 1 83} 84 85// urldecode src[0..n) into out (cap out_cap): '+'->space, %XX->byte. one output byte per loop. -1 on error. 86func sd_urldecode(src: *u8, n: i64, out: *u8, out_cap: i64) -> i64 { 87 var i: i64 = 0 88 var o: i64 = 0 89 while i < n { 90 if o >= out_cap { return 0 - 1 } 91 let c: i64 = src[i] as i64 92 if c == 43 { out[o] = 32 as u8; i = i + 1 } 93 if c == 37 { 94 if i + 3 > n { return 0 - 1 } 95 let hi: i64 = sd_hexnib(src[i + 1] as i64) 96 let lo: i64 = sd_hexnib(src[i + 2] as i64) 97 if hi < 0 { return 0 - 1 } 98 if lo < 0 { return 0 - 1 } 99 out[o] = ((hi << 4) | lo) as u8 100 i = i + 3 101 } 102 if c != 43 { if c != 37 { out[o] = c as u8; i = i + 1 } } 103 o = o + 1 104 } 105 return o 106} 107 108// find "name=" in body[0..body_n); on hit set out_off/out_n to the raw (still-encoded) value slice. 1/0. 109// Authorization callers require one occurrence; legacy first-match lookup stays unchanged. 110func sd_form_field_unique(body:*u8,body_n:i64,name:*u8,name_n:i64,out_off:*i64,out_n:*i64)->i64{ 111 if (body as i64)<=0||(name as i64)<=0||(out_off as i64)<=0||(out_n as i64)<=0{return -1} 112 out_off[0]=0;out_n[0]=0 113 if body_n<0||name_n<=0{return -1};if name_n>=body_n{return 0} 114 let found:i64=sd_form_field(body,body_n,name,name_n,out_off,out_n) 115 if found!=1{return found} 116 let end:i64=out_off[0]+out_n[0] 117 if end<body_n{ 118 let next:i64=end+1;var other_off:i64=0;var other_n:i64=0 119 if sd_form_field(((body as i64)+next) as *u8,body_n-next,name,name_n,&other_off,&other_n)==1{ 120 out_off[0]=0;out_n[0]=0;return -1 121 } 122 } 123 return 1 124} 125func sd_form_field(body: *u8, body_n: i64, name: *u8, name_n: i64, out_off: *i64, out_n: *i64) -> i64 { 126 var pos: i64 = 0 127 while pos < body_n { 128 var m: i64 = 1 129 if pos + name_n + 1 > body_n { m = 0 } 130 if m == 1 { 131 var i: i64 = 0 132 while i < name_n { 133 if (body[pos + i] as i64) != (name[i] as i64) { m = 0; i = name_n } 134 if i < name_n { i = i + 1 } 135 } 136 } 137 if m == 1 { if (body[pos + name_n] as i64) != 61 { m = 0 } } 138 var vend: i64 = pos 139 var scan: i64 = 1 140 while scan == 1 { 141 if vend >= body_n { scan = 0 } 142 if scan == 1 { if (body[vend] as i64) == 38 { scan = 0 } } 143 if scan == 1 { vend = vend + 1 } 144 } 145 if m == 1 { 146 out_off[0] = pos + name_n + 1 147 out_n[0] = vend - (pos + name_n + 1) 148 return 1 149 } 150 pos = vend + 1 151 } 152 return 0 153} 154 155// the no-cookie SPA shell: login -> token in sessionStorage -> fetch content with X-Nishi-Session. (public UI) 156func sd_shell(out: *u8) -> i64 { 157 var o: i64 = 0 158 o = sd_cat(out, o, "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nConnection: close\r\n\r\n" as *u8) 159 o = sd_cat(out, o, "<!DOCTYPE html><meta charset=utf-8><meta name=viewport content=\"width=device-width,initial-scale=1\"><title>Nishi status</title>" as *u8) 160 o = sd_cat(out, o, "<style>body{font-family:-apple-system,Segoe UI,sans-serif;max-width:820px;margin:7vh auto;padding:0 20px;color:#1c1c1e}#login{max-width:330px}input{width:100%;padding:10px;margin:.45rem 0;box-sizing:border-box;border:1px solid #ccc;border-radius:7px}button{padding:10px 18px;border:0;border-radius:7px;background:#0a6;color:#fff;font-size:1rem}.e{color:#b00;min-height:1.2em}</style>" as *u8) 161 o = sd_cat(out, o, "<div id=login><h2>&#128274; Nishi admin</h2><input id=h placeholder=\"handle\" autocomplete=username autofocus><input id=p type=password placeholder=\"passphrase\" autocomplete=current-password><button id=b>Sign in</button><p id=e class=e></p></div><div id=content hidden></div>" as *u8) 162 o = sd_cat(out, o, "<script>var L=document.getElementById('login'),C=document.getElementById('content'),E=document.getElementById('e');function show(){fetch('/status/content',{headers:{'X-Nishi-Session':sessionStorage.nx_sess||''}}).then(function(r){if(r.ok){return r.text()}throw 0}).then(function(t){C.innerHTML=t;L.hidden=true;C.hidden=false}).catch(function(){sessionStorage.removeItem('nx_sess')})}" as *u8) 163 o = sd_cat(out, o, "document.getElementById('b').onclick=function(){E.textContent='';var b='handle='+encodeURIComponent(document.getElementById('h').value)+'&passphrase='+encodeURIComponent(document.getElementById('p').value);fetch('/status/login',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:b}).then(function(r){if(r.ok){return r.json()}throw 0}).then(function(j){sessionStorage.nx_sess=j.token;show()}).catch(function(){E.textContent='Wrong handle or passphrase.'})};if(sessionStorage.nx_sess){show()}</script>" as *u8) 164 return o 165} 166 167func sd_emit_401_json(out: *u8) -> i64 { 168 return sd_cat(out, 0, "HTTP/1.1 401 Unauthorized\r\nContent-Type: application/json\r\nConnection: close\r\nContent-Length: 24\r\n\r\n{\"error\":\"unauthorized\"}" as *u8) 169} 170 171// ---- THE ROUTER: pure function, request bytes -> response bytes (no socket). gate drives this directly. -- 172func sd_handle(ctx: *NxAuthContext, req: *u8, req_n: i64, statusfile: *u8, out: *u8) -> i64 { 173 let poff: *i64 = sys_mmap(8) as *i64 174 let plen: *i64 = sys_mmap(8) as *i64 175 poff[0] = 0 176 plen[0] = 0 177 sd_find_path(req, req_n, poff, plen) 178 let path: *u8 = ((req as i64) + poff[0]) as *u8 179 let pn: i64 = plen[0] 180 let is_post: i64 = (req[0] == 80 as u8) as i64 181 var o: i64 = 0 182 if sd_starts(path, pn, "/status/login" as *u8) == 1 { 183 if is_post == 1 { 184 let body_off: i64 = sd_body_off(req, req_n) 185 let body: *u8 = ((req as i64) + body_off) as *u8 186 let body_n: i64 = req_n - body_off 187 let hoff: *i64 = sys_mmap(8) as *i64 188 let hn: *i64 = sys_mmap(8) as *i64 189 let poff2: *i64 = sys_mmap(8) as *i64 190 let pnn: *i64 = sys_mmap(8) as *i64 191 var got: i64 = 0 192 if sd_form_field(body, body_n, "handle" as *u8, 6, hoff, hn) == 1 { 193 if sd_form_field(body, body_n, "passphrase" as *u8, 10, poff2, pnn) == 1 { got = 1 } 194 } 195 var ok: i64 = 0 196 if got == 1 { 197 let hbuf: *u8 = sys_mmap(256) 198 let pbuf: *u8 = sys_mmap(512) 199 let h_dec: i64 = sd_urldecode(((body as i64) + hoff[0]) as *u8, hn[0], hbuf, 255) 200 let p_dec: i64 = sd_urldecode(((body as i64) + poff2[0]) as *u8, pnn[0], pbuf, 511) 201 if h_dec > 0 { if p_dec > 0 { 202 let tok: *u8 = sys_mmap(NX_MAUTH_SESSION_TOKEN_BYTES) 203 let tok_n: *i64 = sys_mmap(8) as *i64 204 tok_n[0] = 0 205 if nx_modern_auth_login(ctx, hbuf, h_dec, pbuf, p_dec, tok, NX_MAUTH_SESSION_TOKEN_BYTES, tok_n) == NX_MAUTH_OK { 206 let b64: *u8 = sys_mmap(256) 207 let b64_n: i64 = b64_encode(tok, NX_MAUTH_SESSION_TOKEN_BYTES, b64) 208 o = sd_cat(out, o, "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nConnection: close\r\nContent-Length: " as *u8) 209 o = sd_catn(out, o, 12 + b64_n) 210 o = sd_cat(out, o, "\r\n\r\n{\"token\":\"" as *u8) 211 var z: i64 = 0 212 while z < b64_n { out[o] = b64[z]; o = o + 1; z = z + 1 } 213 o = sd_cat(out, o, "\"}" as *u8) 214 ok = 1 215 } 216 } } 217 } 218 if ok == 0 { o = sd_emit_401_json(out) } 219 } else { o = sd_shell(out) } 220 } else { if sd_starts(path, pn, "/status/content" as *u8) == 1 { 221 let now_s: i64 = sys_now_realtime_sec() 222 if nx_sa_validate(ctx, req, req_n, now_s) == NX_MAUTH_OK { 223 let sbox: *i64 = sys_mmap(16) as *i64; sbox[0] = 0 224 let page: *u8 = sys_read_file(statusfile, sbox) 225 if (page as i64) != 0 { 226 o = sd_cat(out, o, "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nConnection: close\r\nContent-Length: " as *u8) 227 o = sd_catn(out, o, sbox[0]) 228 o = sd_cat(out, o, "\r\n\r\n" as *u8) 229 var z2: i64 = 0 230 while z2 < sbox[0] { out[o] = page[z2]; o = o + 1; z2 = z2 + 1 } 231 } else { o = sd_cat(out, o, "HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\nContent-Length: 0\r\n\r\n" as *u8) } 232 } else { o = sd_emit_401_json(out) } 233 } else { o = sd_shell(out) } } 234 return o 235} 236 237func main(argc: i64, argv: *i64) -> i64 { 238 if argc < 7 { sys_write(2, "usage: nx_status_daemon <port> <keysfile> <storefile> <realm> <statusfile> <budget>\n" as *u8, 83); return 1 } 239 let port: i64 = sd_atoi(argv[1] as *u8) 240 let keysfile: *u8 = argv[2] as *u8 241 let storefile: *u8 = argv[3] as *u8 242 let realm: *u8 = argv[4] as *u8 243 let statusfile: *u8 = argv[5] as *u8 244 let budget: i64 = sd_atoi(argv[6] as *u8) 245 let realm_n: i64 = sd_len(realm) 246 247 // ---- fail-fast: arm the realm context at startup (Rule 20) ---- 248 let oprf_seed: *u8 = sys_mmap(32) 249 let akp: *u8 = sys_mmap(32) 250 let akb: *u8 = sys_mmap(33) 251 let edp: *u8 = sys_mmap(32) 252 let edb: *u8 = sys_mmap(32) 253 if nx_uas_server_keys_load_or_init(keysfile, oprf_seed, akp, akb, edp, edb) != NX_UAS_OK { sys_write(2, "FATAL: server-key bundle\n" as *u8, 24); return 2 } 254 let ctx: *NxAuthContext = sys_mmap(256) as *NxAuthContext 255 if nx_auth_context_init(ctx, realm, realm_n, realm, realm_n, storefile as i64, oprf_seed, edp, edb, 900, SD_MAGIC_8192, 1, 1, 5, 1) != NX_MAUTH_OK { sys_write(2, "FATAL: context init\n" as *u8, 20); return 3 } 256 257 let addr: *u8 = sys_mmap(16) 258 if nx_http_server_addr_loopback(addr, port) != 16 { return 4 } 259 let lv: *i64 = sys_mmap(8) as *i64 260 let lfd: i64 = nx_http_server_listen(addr, 64, lv) 261 if lfd < 0 { return 4 } 262 263 let req: *u8 = sys_mmap(SD_REQCAP) 264 let out: *u8 = sys_mmap(SD_OUTCAP) 265 var served: i64 = 0 266 267 while served < budget { 268 let av: *i64 = sys_mmap(8) as *i64 269 let cfd: i64 = nx_http_server_accept_one(lfd, av) 270 if cfd < 0 { served = served + 1 } 271 if cfd >= 0 { 272 let om: *i64 = sys_mmap(8) as *i64 273 let opo: *i64 = sys_mmap(8) as *i64 274 let opl: *i64 = sys_mmap(8) as *i64 275 let ocl: *i64 = sys_mmap(8) as *i64 276 let obo: *i64 = sys_mmap(8) as *i64 277 let orn: *i64 = sys_mmap(8) as *i64 278 let rrc: i64 = nx_http_server_read_request(cfd, req, SD_REQCAP, om, opo, opl, ocl, obo, orn) 279 if rrc == NXS_OK { 280 let o: i64 = sd_handle(ctx, req, orn[0], statusfile, out) 281 nx_http_server_send_response_nokeep_close(cfd, out, o) 282 } 283 sys_close(cfd) 284 served = served + 1 285 } 286 } 287 sys_close(lfd) 288 return 0 289}