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 — 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 — 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 · 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}