code wiki / _hdl_build / nx_studio_gate.nx

nx_studio_gate.nx source

↩ module page · 165 lines · 14332 B

1// nx_studio_gate.nx -- OPAQUE-gated "Nishi Studio" surface (OPERATOR-ONLY) mounted at PATH /studio on 2// nishifamily.com (sites_v2 path-proxies /studio/* here; it forwards the full path, so routes are 3// /studio-prefixed). Faithful clone of the proven nx_opaque_login_daemon: RFC 9807 OPAQUE + no-cookie 4// X-Nishi-Session security INHERITED VERBATIM, OWN ISOLATED realm "nishi_studio". GET /studio/app serves 5// the studio / search-by-image surface ONLY to a valid session; NO valid token -> 401 (never a content 6// leak). The ONE clean public-behind-OPAQUE surface ([[feedback-one-gated-live-surface-no-dev-sprawl]]). 7// argv: [1]=port [2]=keys-path [3]=store-path [4]=budget [5]=argon2_m(opt;64MiB prod) [6]=t [7]=p [8]=allow_register(opt;default 1) 8// routes: GET /studio | /studio/ -> login page ; POST /studio/login ; POST /studio/register ; GET /studio/whoami ; GET /studio/app (PROTECTED) 9// license_tier: ORIGINAL 10import "nx_opaque_login.nx" 11import "nx_http_form.nx" 12 13const SG_PROD_M: i64 = 65536 14 15func sg_slen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n } 16func sg_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{return v} if c>57{return v} v=v*10+(c-48); i=i+1 } return v } 17func sg_starts(buf: *u8, n: i64, pre: *u8) -> i64 { var i: i64=0; while pre[i]!=(0 as u8){ if i>=n {return 0} if buf[i]!=pre[i]{return 0} i=i+1 } return 1 } 18func sg_find(buf: *u8, n: i64, needle: *u8, nl: i64) -> i64 { 19 if nl==0 { return 0 } 20 var i: i64=0 21 while i+nl<=n { var j: i64=0; var ok: i64=1; while j<nl { if buf[i+j]!=needle[j]{ok=0; j=nl} else {j=j+1} } if ok==1 {return i} i=i+1 } 22 return 0-1 23} 24func sg_cat(dst: *u8, off: i64, s: *u8) -> i64 { var o: i64=off; var i: i64=0; while s[i]!=(0 as u8){dst[o]=s[i]; o=o+1; i=i+1} return o } 25func sg_catb(dst: *u8, off: i64, src: *u8, n: i64) -> i64 { var o: i64=off; var i: i64=0; while i<n {dst[o]=src[i]; o=o+1; i=i+1} return o } 26func sg_itoa(dst: *u8, off: i64, v: i64) -> i64 { let t: *u8=sys_mmap(28); var m: i64=v; var k: i64=0; if m==0{t[0]=48 as u8;k=1} while m>0{t[k]=(48+(m%10)) as u8;m=m/10;k=k+1} var o: i64=off; var q: i64=k-1; while q>=0{dst[o]=t[q];o=o+1;q=q-1} return o } 27func sg_hex(dst: *u8, off: i64, src: *u8, n: i64) -> i64 { let hx: *u8="0123456789abcdef" as *u8; var o: i64=off; var i: i64=0; while i<n { let c: i64=(src[i] as i64)&0xff; dst[o]=hx[(c>>4)&15]; dst[o+1]=hx[c&15]; o=o+2; i=i+1 } return o } 28 29func sg_send(cfd: i64, status: *u8, ctype: *u8, body: *u8, blen: i64) -> i64 { 30 let buf: *u8 = sys_mmap(65536); var o: i64 = 0 31 o = sg_cat(buf, o, "HTTP/1.1 " as *u8); o = sg_cat(buf, o, status) 32 o = sg_cat(buf, o, "\r\nContent-Type: " as *u8); o = sg_cat(buf, o, ctype) 33 o = sg_cat(buf, o, "\r\nContent-Length: " as *u8); o = sg_itoa(buf, o, blen) 34 o = sg_cat(buf, o, "\r\nConnection: close\r\nCache-Control: no-store\r\n\r\n" as *u8) 35 o = sg_catb(buf, o, body, blen) 36 sys_write(cfd, buf, o); return 0 37} 38func sg_hdr_val(req: *u8, hend: i64, name: *u8, nl: i64, out: *u8, cap: i64) -> i64 { 39 let p: i64 = sg_find(req, hend, name, nl) 40 if p < 0 { return 0 } 41 var i: i64 = p + nl 42 if i < hend { if req[i]==(32 as u8) { i=i+1 } } 43 var o: i64 = 0 44 while i < hend { let c: u8 = req[i]; if c==(13 as u8){i=hend} else { if c==(10 as u8){i=hend} else { if o<cap-1 {out[o]=c; o=o+1} i=i+1 } } } 45 out[o]=0 as u8; return o 46} 47 48const SG_LOGIN_HTML: *u8 = "<!doctype html><html><head><meta charset=utf-8><meta name=viewport content=\"width=device-width,initial-scale=1\"><title>Nishi Studio &mdash; login</title><style>body{font-family:system-ui,sans-serif;max-width:460px;margin:6vh auto;padding:0 18px;background:#0d1117;color:#e6edf3}h1{font-size:1.25rem}h3{margin:18px 0 4px}input{width:100%;padding:9px;margin:5px 0;box-sizing:border-box;border:1px solid #30363d;border-radius:6px;background:#161b22;color:#e6edf3}button{padding:9px 16px;margin:6px 6px 0 0;background:#238636;color:#fff;border:0;border-radius:6px;cursor:pointer}#m{margin:14px 0;padding:12px;background:#161b22;border-left:3px solid #238636;word-break:break-all;min-height:1.2em}</style></head><body><h1>Nishi Studio</h1><p style=\"color:#8b949e;font-size:.9rem\">Operator-only. Full OPAQUE (RFC 9807); no-cookie session.</p><div id=m>Register, then log in to enter the studio.</div><h3>Register</h3><input id=rh placeholder=handle><input id=rp type=password placeholder=passphrase><button onclick=reg()>Register</button><h3>Login</h3><input id=lh placeholder=handle><input id=lp type=password placeholder=passphrase><button onclick=login()>Login</button> <button onclick=enter()>Enter Studio</button><script>function $(i){return document.getElementById(i)}function M(t){$('m').textContent=t}async function P(p,b){var r=await fetch(p,{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:b});var j={};try{j=await r.json()}catch(e){}return{ok:r.ok,s:r.status,j:j}}async function reg(){M('Registering\\u2026');var r=await P('/studio/register','handle='+encodeURIComponent($('rh').value)+'&pw='+encodeURIComponent($('rp').value));M(r.ok?('Registered. SAVE RECOVERY: '+r.j.mnemonic):('Register failed '+r.s))}async function login(){M('Logging in\\u2026');var r=await P('/studio/login','handle='+encodeURIComponent($('lh').value)+'&pw='+encodeURIComponent($('lp').value));if(r.ok){sessionStorage.setItem('nsess',r.j.token);M('Logged in. Click Enter Studio.')}else{M('Login failed '+r.s)}}async function enter(){var t=sessionStorage.getItem('nsess');if(!t){M('Log in first.');return}var r=await fetch('/studio/app',{headers:{'X-Nishi-Session':t}});if(r.ok){var h=await r.text();document.open();document.write(h);document.close()}else{M('Denied '+r.status+' \\u2014 login required.')}}</script></body></html>" as *u8 49 50const SG_STUDIO_HTML: *u8 = "<!doctype html><html><head><meta charset=utf-8><meta name=viewport content=\"width=device-width,initial-scale=1\"><title>Nishi Studio &mdash; Search by Image</title><style>body{font-family:system-ui,sans-serif;max-width:820px;margin:4vh auto;padding:0 18px;background:#0d1117;color:#e6edf3}h1{font-size:1.3rem}.tag{color:#2ea043;font-size:.8rem;border:1px solid #2ea043;border-radius:20px;padding:2px 10px;display:inline-block;vertical-align:middle}.card{background:#161b22;border:1px solid #30363d;border-radius:10px;padding:18px;margin:16px 0}.sub{color:#8b949e;font-size:.9rem}button{padding:9px 16px;background:#238636;color:#fff;border:0;border-radius:6px;cursor:pointer}</style></head><body><h1>Nishi Studio <span class=tag>operator-only &middot; OPAQUE-gated</span></h1><p class=sub>You are authenticated. This surface is reachable only with your session \\u2014 not the internet, not others.</p><div class=card><h3>Search by Image</h3><p class=sub>Sovereign reverse-image search (perceptual dHash, brightness-invariant, per-site isolated). Pick an image to find its matches across the corpus.</p><input type=file id=qimg accept=image/*> <button onclick=go()>Search</button><div id=res style=\"margin-top:14px\" class=sub>Ready.</div></div><script>function go(){var f=document.getElementById('qimg').files[0];var d=document.getElementById('res');if(!f){d.textContent='Choose an image first.';return}d.textContent='Searching '+f.name+'\\u2026 (live search API wires in the next rung)'}</script></body></html>" as *u8 51 52func main(argc: i64, argv: *i64) -> i64 { 53 if argc < 5 { 54 sys_write(1, "usage: nx_studio_gate <port> <keys-path> <store-path> <budget> [m] [t] [p] [allow_register]\n" as *u8, 91) 55 sys_exit(2); return 2 56 } 57 let port: i64 = sg_atoi(argv[1] as *u8) 58 let keys_path: *u8 = argv[2] as *u8 59 let store_path: *u8 = argv[3] as *u8 60 let budget: i64 = sg_atoi(argv[4] as *u8) 61 var m_cost: i64 = SG_PROD_M 62 if argc > 5 { m_cost = sg_atoi(argv[5] as *u8) } 63 var t_cost: i64 = 3 64 if argc > 6 { t_cost = sg_atoi(argv[6] as *u8) } 65 var p_cost: i64 = 4 66 if argc > 7 { p_cost = sg_atoi(argv[7] as *u8) } 67 var allow_reg: i64 = 1 68 if argc > 8 { allow_reg = sg_atoi(argv[8] as *u8) } 69 70 let ctx: *NxAuthContext = sys_mmap(256) as *NxAuthContext 71 if olg_ctx_setup(ctx, keys_path, store_path, "nishi_studio" as *u8, 12, "Nishi Studio" as *u8, 12, m_cost, t_cost, p_cost) != 0 { 72 sys_write(1, "CTX-INIT-FAIL\n" as *u8, 14); sys_exit(1); return 1 73 } 74 75 let addr: *u8 = sys_mmap(16) 76 addr[0]=2 as u8; addr[1]=0 as u8 77 addr[2]=((port>>8)&0xff) as u8; addr[3]=(port&0xff) as u8 78 addr[4]=0 as u8; addr[5]=0 as u8; addr[6]=0 as u8; addr[7]=0 as u8 79 var zi: i64=8; while zi<16 { addr[zi]=0 as u8; zi=zi+1 } 80 let lfd: i64 = sys_socket(2, 1, 0) 81 if lfd < 0 { sys_write(1, "SOCKET-FAIL\n" as *u8, 12); sys_exit(1); return 1 } 82 let optv: *u8 = sys_mmap(4); optv[0]=1 as u8 83 sys_setsockopt(lfd, 1, 2, optv, 4) 84 if sys_bind(lfd, addr, 16) < 0 { sys_write(1, "BIND-FAIL\n" as *u8, 10); sys_exit(1); return 1 } 85 if sys_listen(lfd, 16) < 0 { sys_write(1, "LISTEN-FAIL\n" as *u8, 12); sys_exit(1); return 1 } 86 sys_write(1, "STUDIO-GATE-UP\n" as *u8, 15) 87 88 var served: i64 = 0 89 while served < budget { 90 let cfd: i64 = sys_accept(lfd) 91 if cfd >= 0 { 92 sys_set_socket_timeout(cfd, 5) 93 let req: *u8 = sys_mmap(16384) 94 let rn: i64 = sys_read(cfd, req, 16383) 95 if rn > 0 { 96 let he: i64 = sg_find(req, rn, "\r\n\r\n" as *u8, 4) 97 var body: *u8 = req; var bn: i64 = 0 98 if he >= 0 { body = ((req as i64) + he + 4) as *u8; bn = rn - he - 4 } 99 let now: i64 = sys_now_realtime_sec() 100 let resp: *u8 = sys_mmap(8192) 101 102 if sg_starts(req, rn, "POST /studio/login" as *u8) == 1 { 103 let hbuf: *u8 = sys_mmap(128); let hl: *i64 = sys_mmap(16) as *i64 104 let pbuf: *u8 = sys_mmap(320); let pl: *i64 = sys_mmap(16) as *i64 105 nx_http_form_get_field(body, bn, "handle" as *u8, 6, hbuf, 127, hl) 106 nx_http_form_get_field(body, bn, "pw" as *u8, 2, pbuf, 319, pl) 107 let b64: *u8 = sys_mmap(512); let b64n: *i64 = sys_mmap(16) as *i64 108 if olg_login(ctx, hbuf, hl[0], pbuf, pl[0], b64, 512, b64n) == NX_MAUTH_OK { 109 var o: i64 = sg_cat(resp, 0, "{\"token\":\"" as *u8); o = sg_catb(resp, o, b64, b64n[0]); o = sg_cat(resp, o, "\"}" as *u8) 110 sg_send(cfd, "200 OK" as *u8, "application/json" as *u8, resp, o) 111 } else { 112 let o: i64 = sg_cat(resp, 0, "{\"error\":\"invalid credentials\"}" as *u8) 113 sg_send(cfd, "401 Unauthorized" as *u8, "application/json" as *u8, resp, o) 114 } 115 } else { if sg_starts(req, rn, "GET /studio/whoami" as *u8) == 1 { 116 let tb: *u8 = sys_mmap(512) 117 let tl: i64 = sg_hdr_val(req, he, "X-Nishi-Session:" as *u8, 16, tb, 512) 118 let uh: *u8 = sys_mmap(64); let uhn: *i64 = sys_mmap(16) as *i64 119 if olg_whoami(ctx, tb, tl, now, uh, 64, uhn) == NX_MAUTH_OK { 120 var o: i64 = sg_cat(resp, 0, "{\"uid\":\"" as *u8); o = sg_hex(resp, o, uh, uhn[0]); o = sg_cat(resp, o, "\"}" as *u8) 121 sg_send(cfd, "200 OK" as *u8, "application/json" as *u8, resp, o) 122 } else { 123 let o: i64 = sg_cat(resp, 0, "{\"error\":\"session rejected\"}" as *u8) 124 sg_send(cfd, "401 Unauthorized" as *u8, "application/json" as *u8, resp, o) 125 } 126 } else { if sg_starts(req, rn, "GET /studio/app" as *u8) == 1 { 127 // PROTECTED: studio / search-by-image surface, served ONLY to a valid OPAQUE session. 128 // No valid X-Nishi-Session -> 401 (never a public content leak). THE GATE. 129 let tb: *u8 = sys_mmap(512) 130 let tl: i64 = sg_hdr_val(req, he, "X-Nishi-Session:" as *u8, 16, tb, 512) 131 let uh: *u8 = sys_mmap(64); let uhn: *i64 = sys_mmap(16) as *i64 132 if olg_whoami(ctx, tb, tl, now, uh, 64, uhn) == NX_MAUTH_OK { 133 sg_send(cfd, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, SG_STUDIO_HTML, sg_slen(SG_STUDIO_HTML)) 134 } else { 135 let o: i64 = sg_cat(resp, 0, "{\"error\":\"login required\"}" as *u8) 136 sg_send(cfd, "401 Unauthorized" as *u8, "application/json" as *u8, resp, o) 137 } 138 } else { if sg_starts(req, rn, "POST /studio/register" as *u8) == 1 { 139 if allow_reg == 0 { 140 let o: i64 = sg_cat(resp, 0, "{\"error\":\"registration closed\"}" as *u8) 141 sg_send(cfd, "403 Forbidden" as *u8, "application/json" as *u8, resp, o) 142 } else { 143 let hbuf: *u8 = sys_mmap(128); let hl: *i64 = sys_mmap(16) as *i64 144 let pbuf: *u8 = sys_mmap(320); let pl: *i64 = sys_mmap(16) as *i64 145 nx_http_form_get_field(body, bn, "handle" as *u8, 6, hbuf, 127, hl) 146 nx_http_form_get_field(body, bn, "pw" as *u8, 2, pbuf, 319, pl) 147 let mn: *u8 = sys_mmap(512); let mnn: *i64 = sys_mmap(16) as *i64 148 if olg_register(ctx, hbuf, hl[0], pbuf, pl[0], mn, 512, mnn) == NX_MAUTH_OK { 149 var o: i64 = sg_cat(resp, 0, "{\"mnemonic\":\"" as *u8); o = sg_catb(resp, o, mn, mnn[0]); o = sg_cat(resp, o, "\"}" as *u8) 150 sg_send(cfd, "200 OK" as *u8, "application/json" as *u8, resp, o) 151 } else { 152 let o: i64 = sg_cat(resp, 0, "{\"error\":\"register failed\"}" as *u8) 153 sg_send(cfd, "400 Bad Request" as *u8, "application/json" as *u8, resp, o) 154 } 155 } 156 } else { 157 sg_send(cfd, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, SG_LOGIN_HTML, sg_slen(SG_LOGIN_HTML)) 158 } } } } 159 } 160 sys_close(cfd) 161 } 162 served = served + 1 163 } 164 sys_close(lfd); sys_exit(0); return 0 165}