code wiki / _hdl_build / nx_gen_gateway.nx
nx_gen_gateway.nx source
↩ module page · 310 lines · 25279 B
1// nx_gallery_gateway.nx -- OPAQUE-gated reverse proxy for the NSFW gallery, mounted under /gen/*.
2// Auth = the gallery's OWN OPAQUE realm (own keys+store via argv -> isolated NSFW realm) + the
3// Service-Worker header-injection model (the SW adds X-Nishi-Session to every /gen/* request incl.
4// media -> NO cookie, cardinal C1 preserved). The gateway validates X-Nishi-Session per request via
5// olg_whoami, then reverse-proxies the request (prefix-stripped) to the gallery backend. No valid
6// session -> 401 (never a public byte). Bootstrap: GET /gen/login (page) registers the SW.
7// argv: [1]=listen_port [2]=keys_path [3]=store_path [4]=budget [5]=backend_port
8// [6]=allow_register(0|1; PROD=0) [7]=m_cost(opt 65536) [8]=t(opt 3) [9]=p(opt 4)
9// R1 = loopback proof (no TLS yet; TLS termination + path-route into nishifamily.com = R3).
10import "nx_opaque_login.nx" // olg_ctx_setup / olg_register / olg_login / olg_whoami + NxAuthContext + NX_MAUTH_*
11import "nx_http_form.nx" // nx_http_form_get_field
12import "nx_hr_entitle.nx" // he_has_access -- per-area approval authZ (single nishifamily login + approve-by-area)
13import "nx_connect.nx" // bounded connect
14const GGW_MAGIC_262144: i64 = 262144
15const GGW_MAGIC_6291456: i64 = 6291456
16const GGW_MAGIC_131072: i64 = 131072
17const GGW_MAGIC_131071: i64 = 131071
18const GGW_MAGIC_8192: i64 = 8192
19
20const GGW_PROD_M: i64 = 65536
21// Session lifetime for this LOW-RISK single-operator NSFW media realm. 86400 = 24h = the auth lib's HARD CAP
22// (NX_MAUTH_HARD_MAX_TTL_S; nx_auth_context_init REJECTS anything larger with BAD_INPUT -> a bigger value crashes
23// the gateway at CTX-INIT-FAIL, which it did at 2592000). 24h still kills the 15-min re-login (96x longer);
24// "days" = sliding refresh (nx_modern_auth_refresh_session re-issues on activity), a later rung. argv[10] overrides.
25const GGW_SESSION_TTL: i64 = 86400
26// the sovereign entitlement seg_store the NAS login daemon reads (OLGD_ENT_PATH). A user reaches /gen only if
27// he_has_access(handle,"/gen")==1 -> the operator's "approve access to new areas" gate. argv[11] overrides for tests.
28const GEN_ENT_PATH: *u8 = "/volume1/homes/elderwesto/nishihost/nishi_entitlements-" as *u8
29// HR SSOT seg_store (same one the login daemon checks): the OWNER is superadmin -> auto-access every area, so the
30// operator reaches /gen with NO entitlement-store write. Others need an explicit he_ent_put grant (the approval).
31const GEN_HR_PATH: *u8 = "/volume1/homes/elderwesto/nishihost/nishi_hr-" as *u8
32
33func gw_slen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
34func gw_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 }
35func gw_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 }
36func gw_find(buf: *u8, n: i64, needle: *u8, nl: i64) -> i64 {
37 if nl==0 { return 0 }
38 var i: i64=0
39 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 }
40 return 0-1
41}
42func gw_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 }
43func gw_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 }
44func gw_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 }
45
46// send a full HTTP/1.1 response (single write). status e.g. "200 OK", ctype e.g. "text/html".
47func gw_send(cfd: i64, status: *u8, ctype: *u8, body: *u8, blen: i64) -> i64 {
48 let buf: *u8 = sys_mmap(GGW_MAGIC_262144); var o: i64 = 0
49 o = gw_cat(buf, o, "HTTP/1.1 " as *u8); o = gw_cat(buf, o, status)
50 o = gw_cat(buf, o, "\r\nContent-Type: " as *u8); o = gw_cat(buf, o, ctype)
51 o = gw_cat(buf, o, "\r\nContent-Length: " as *u8); o = gw_itoa(buf, o, blen)
52 o = gw_cat(buf, o, "\r\nConnection: close\r\nCache-Control: no-store\r\n\r\n" as *u8)
53 o = gw_catb(buf, o, body, blen)
54 sys_write(cfd, buf, o); return 0
55}
56func gw_401(cfd: i64) -> i64 {
57 let b: *u8 = "{\"error\":\"login required\"}" as *u8
58 gw_send(cfd, "401 Unauthorized" as *u8, "application/json" as *u8, b, gw_slen(b)); return 0
59}
60// authenticated but NOT approved for this area (valid session, not an operator handle) -> deny-by-default.
61func gw_403(cfd: i64) -> i64 {
62 let b: *u8 = "{\"error\":\"not approved for this area\"}" as *u8
63 gw_send(cfd, "403 Forbidden" as *u8, "application/json" as *u8, b, gw_slen(b)); return 0
64}
65// exact compare: handle bytes a[0..an) vs NUL-terminated b. 1 iff same length AND same bytes.
66func gw_eqs(a: *u8, an: i64, b: *u8) -> i64 { var i: i64=0; while b[i]!=(0 as u8){ if i>=an {return 0} if a[i]!=b[i]{return 0} i=i+1 } if i!=an {return 0} return 1 }
67// /gen approval (first cut, live-compatible, NO seg_store dep): operator handles only. The seg_store
68// entitlement layer (he_has_access) is the later generalization once nishi_entitlements- is deployed live.
69func gw_is_op(h: *u8, n: i64) -> i64 { if gw_eqs(h, n, "elderwesto" as *u8)==1 {return 1} if gw_eqs(h, n, "elder" as *u8)==1 {return 1} return 0 }
70// 1 iff this looks like a top-level page navigation (so an expired session should bounce to the login page,
71// not flash a raw 401/blank grid). Sec-Fetch-Mode: navigate is the browser-set, spoof-irrelevant signal.
72func gw_is_nav(req: *u8, n: i64) -> i64 { if gw_find(req, n, "Sec-Fetch-Mode: navigate" as *u8, 24) >= 0 { return 1 } return 0 }
73// 302 to the login page (graceful re-login). Body-less; no-store so the redirect itself is never cached.
74func gw_302_login(cfd: i64) -> i64 {
75 let b: *u8 = "HTTP/1.1 302 Found\r\nLocation: /gen/login\r\nContent-Length: 0\r\nCache-Control: no-store\r\nConnection: close\r\n\r\n" as *u8
76 sys_write(cfd, b, gw_slen(b)); return 0
77}
78// 200 response that ALSO sets the session as an HttpOnly cookie -> the browser sends it on every /gen/*
79// request (page navigation, <img>, <video>) so the page load itself authenticates, no Service-Worker timing
80// dependency. HttpOnly = JS cannot read it; Secure = HTTPS only; SameSite=Strict = no cross-site send.
81func gw_send_ck(cfd: i64, ctype: *u8, body: *u8, blen: i64, ckval: *u8, ckvallen: i64, ttl: i64) -> i64 {
82 let buf: *u8 = sys_mmap(GGW_MAGIC_262144); var o: i64 = 0
83 o = gw_cat(buf, o, "HTTP/1.1 200 OK\r\nContent-Type: " as *u8); o = gw_cat(buf, o, ctype)
84 o = gw_cat(buf, o, "\r\nSet-Cookie: ngs=" as *u8); o = gw_catb(buf, o, ckval, ckvallen)
85 o = gw_cat(buf, o, "; HttpOnly; Secure; SameSite=Strict; Path=/gen; Max-Age=" as *u8); o = gw_itoa(buf, o, ttl)
86 o = gw_cat(buf, o, "\r\nContent-Length: " as *u8); o = gw_itoa(buf, o, blen)
87 o = gw_cat(buf, o, "\r\nConnection: close\r\nCache-Control: no-store\r\n\r\n" as *u8)
88 o = gw_catb(buf, o, body, blen)
89 sys_write(cfd, buf, o); return 0
90}
91// pull the session token from the `ngs=` cookie in req[0..hend] -> len into out (NUL-term).
92func gw_cookie_val(req: *u8, hend: i64, out: *u8, cap: i64) -> i64 {
93 let p: i64 = gw_find(req, hend, "ngs=" as *u8, 4)
94 if p < 0 { out[0]=0 as u8; return 0 }
95 var i: i64 = p + 4; var o: i64 = 0
96 while i < hend { let c: u8 = req[i]; if c==(59 as u8){i=hend} else { if c==(13 as u8){i=hend} else { if c==(10 as u8){i=hend} else { if c==(32 as u8){i=hend} else { if o<cap-1 {out[o]=c; o=o+1} i=i+1 } } } } }
97 out[o]=0 as u8; return o
98}
99// request header value for `name` (incl trailing ':') over req[0..hend] -> len into out (NUL-term)
100func gw_hdr_val(req: *u8, hend: i64, name: *u8, nl: i64, out: *u8, cap: i64) -> i64 {
101 let p: i64 = gw_find(req, hend, name, nl)
102 if p < 0 { out[0]=0 as u8; return 0 }
103 var i: i64 = p + nl
104 if i < hend { if req[i]==(32 as u8) { i=i+1 } }
105 var o: i64 = 0
106 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 } } }
107 out[o]=0 as u8; return o
108}
109// request-target path (between first space and next space) -> len into out (NUL-term)
110func gw_reqpath(req: *u8, rn: i64, out: *u8, cap: i64) -> i64 {
111 var s1: i64 = 0-1; var i: i64 = 0
112 while i < rn { if req[i]==(32 as u8) { s1=i; i=rn } else { i=i+1 } }
113 if s1 < 0 { out[0]=0 as u8; return 0 }
114 var p: i64 = s1+1; var o: i64 = 0
115 while p < rn { let c: u8 = req[p]; if c==(32 as u8) { p=rn } else { if o<cap-1 { out[o]=c; o=o+1 } p=p+1 } }
116 out[o]=0 as u8; return o
117}
118// reverse-proxy: connect 127.0.0.1:bport, forward method + backend_path (+ body), relay response to cfd.
119// buffered relay (R1: API/images); streaming/range hardening = R4. Returns bytes relayed (or negative).
120// emit "Range: <rng>\r\n" verbatim (pass-through for suffix/multi-range/unparseable forms).
121func gw_emit_range_raw(rq: *u8, o0: i64, rng: *u8, rngn: i64) -> i64 {
122 var o: i64 = gw_cat(rq, o0, "Range: " as *u8); o = gw_catb(rq, o, rng, rngn); o = gw_cat(rq, o, "\r\n" as *u8); return o
123}
124// Cap an OPEN-ENDED or oversized byte-range to a CHUNK window so the whole 206 fits the buffered front proxy
125// (sites_v2 reads the backend response fully into an 8MB buffer before sending; a 206 claiming the full file
126// would be truncated -> the browser rejects the malformed partial). "bytes=START-" / span>CHUNK becomes
127// "bytes=START-(START+CHUNK-1)"; the <video> element fetches the next window as it plays/seeks. Small specific
128// ranges, suffix ranges (bytes=-N), and multi-ranges pass through unchanged.
129func gw_cap_range(rq: *u8, o0: i64, rng: *u8, rngn: i64) -> i64 {
130 let CHUNK: i64 = GGW_MAGIC_6291456
131 var eq: i64 = 0 - 1; var comma: i64 = 0; var k: i64 = 0
132 while k < rngn { if rng[k]==(61 as u8) { if eq<0 { eq=k } } if rng[k]==(44 as u8) { comma=1 } k=k+1 }
133 if eq < 0 { return gw_emit_range_raw(rq, o0, rng, rngn) }
134 if comma == 1 { return gw_emit_range_raw(rq, o0, rng, rngn) }
135 var p: i64 = eq + 1; var start: i64 = 0; var sany: i64 = 0
136 while p < rngn { let c: i64 = rng[p] as i64; if c>=48 { if c<=57 { start=start*10+(c-48); sany=1; p=p+1 } else { p=rngn } } else { p=rngn } }
137 if sany == 0 { return gw_emit_range_raw(rq, o0, rng, rngn) }
138 var dash: i64 = 0 - 1; var d: i64 = eq+1
139 while d < rngn { if rng[d]==(45 as u8) { dash=d; d=rngn } else { d=d+1 } }
140 var end: i64 = 0; var eany: i64 = 0
141 if dash >= 0 { var t: i64 = dash+1; while t < rngn { let c2: i64 = rng[t] as i64; if c2>=48 { if c2<=57 { end=end*10+(c2-48); eany=1; t=t+1 } else { t=rngn } } else { t=rngn } } }
142 if eany == 0 { end = start + CHUNK - 1 } else { if (end - start + 1) > CHUNK { end = start + CHUNK - 1 } }
143 var o: i64 = gw_cat(rq, o0, "Range: bytes=" as *u8)
144 o = gw_itoa(rq, o, start); rq[o]=45 as u8; o=o+1; o = gw_itoa(rq, o, end)
145 o = gw_cat(rq, o, "\r\n" as *u8)
146 return o
147}
148
149func gw_proxy(cfd: i64, bport: i64, method: *u8, mlen: i64, bpath: *u8, bplen: i64, body: *u8, blen: i64, oreq: *u8, ohe: i64) -> i64 {
150 let fd: i64 = sys_socket(2, 1, 0); if fd < 0 { return 0-1 }
151 sys_set_socket_timeout(fd, 20)
152 let a: *u8 = sys_mmap(16)
153 a[0]=2 as u8; a[1]=0 as u8; a[2]=((bport>>8)&0xff) as u8; a[3]=(bport&0xff) as u8
154 a[4]=127 as u8; a[5]=0 as u8; a[6]=0 as u8; a[7]=1 as u8
155 var zi: i64=8; while zi<16 { a[zi]=0 as u8; zi=zi+1 }
156 if nx_connect_bounded(fd, a, 16, NX_CONN_DEFAULT_MS) != 0 { sys_close(fd); return 0-2 }
157 let rq: *u8 = sys_mmap(GGW_MAGIC_131072); var o: i64 = 0
158 o = gw_catb(rq, o, method, mlen); rq[o]=32 as u8; o=o+1
159 o = gw_catb(rq, o, bpath, bplen)
160 o = gw_cat(rq, o, " HTTP/1.1\r\nHost: 127.0.0.1\r\nAccept: */*\r\nConnection: close\r\n" as *u8)
161 // Forward the client's Range header (anchored on a header-line start so "If-Range:" can't false-match)
162 // so the backend answers 206 Partial Content + Content-Range + Accept-Ranges -> native <video> seeks and
163 // plays (esp. iOS/Safari, which refuse a 200 full-file response). No Range present -> byte-identical req.
164 let rng: *u8 = sys_mmap(256)
165 let rngn: i64 = gw_hdr_val(oreq, ohe, "\r\nRange:" as *u8, 8, rng, 256)
166 if rngn > 0 { o = gw_cap_range(rq, o, rng, rngn) }
167 if blen > 0 {
168 o = gw_cat(rq, o, "Content-Type: application/x-www-form-urlencoded\r\nContent-Length: " as *u8)
169 o = gw_itoa(rq, o, blen); o = gw_cat(rq, o, "\r\n\r\n" as *u8)
170 o = gw_catb(rq, o, body, blen)
171 } else {
172 o = gw_cat(rq, o, "\r\n" as *u8)
173 }
174 sys_write(fd, rq, o)
175 let buf: *u8 = sys_mmap(GGW_MAGIC_262144)
176 var total: i64 = 0; var go: i64 = 1
177 while go==1 { let r: i64 = sys_read(fd, buf, GGW_MAGIC_262144); if r<=0 {go=0} else { sys_write(cfd, buf, r); total=total+r } }
178 sys_close(fd)
179 return total
180}
181
182// The Service Worker (served at /gen/sw.js, scope /gen/): injects X-Nishi-Session (from IndexedDB)
183// into every /gen/* request EXCEPT the auth + sw.js + login bootstrap. No cookie. (Browser-tested in R4.)
184const GGW_SW_JS: *u8 = "var DBN='nishi_gen',ST='auth';function tok(){return new Promise(function(res){try{var r=indexedDB.open(DBN,1);r.onupgradeneeded=function(e){e.target.result.createObjectStore(ST)};r.onsuccess=function(e){var db=e.target.result;try{var g=db.transaction(ST,'readonly').objectStore(ST).get('nsess');g.onsuccess=function(){res(g.result||'')};g.onerror=function(){res('')}}catch(x){res('')}};r.onerror=function(){res('')}}catch(x){res('')}})}self.addEventListener('install',function(e){self.skipWaiting()});self.addEventListener('activate',function(e){e.waitUntil(self.clients.claim())});self.addEventListener('fetch',function(e){var u;try{u=new URL(e.request.url)}catch(x){return}if(u.origin!==self.location.origin){return}var pn=u.pathname;if(pn.indexOf('/gen/')!==0){return}if(pn.indexOf('/gen/auth/')===0||pn==='/gen/sw.js'||pn==='/gen/login'){return}e.respondWith(tok().then(function(t){var h=new Headers(e.request.headers);if(t){h.set('X-Nishi-Session',t)}if(e.request.method==='GET'){return fetch(new Request(u.href,{headers:h}))}var rq;try{rq=new Request(e.request,{headers:h})}catch(x){rq=e.request}return fetch(rq)}).catch(function(){return fetch(e.request)}))});" as *u8
185
186// The login + SW-bootstrap page (served at /gen/login). OPAQUE login -> store token in IndexedDB ->
187// register the SW (scope /gen/) -> go to /gen/. No passphrase ever leaves as anything but OPAQUE.
188const GGW_LOGIN_HTML: *u8 = "<!doctype html><html><head><meta charset=utf-8><meta name=viewport content=\"width=device-width,initial-scale=1\"><title>Nishi Gen</title><style>body{font-family:system-ui,sans-serif;max-width:420px;margin:8vh auto;padding:0 18px;color:#cdd7e6;background:#0b1019}h1{font-size:1.2rem;color:#e8eef7}p{color:#7c8aa5;font-size:.86rem}input{width:100%;padding:9px;margin:5px 0;box-sizing:border-box;border:1px solid #2a3550;border-radius:5px;background:#121a28;color:#e8eef7}button{padding:9px 16px;margin:6px 6px 0 0;background:#2d6cdf;color:#fff;border:0;border-radius:5px;cursor:pointer}#m{margin:14px 0;padding:12px;background:#121a28;border-left:3px solid #2d6cdf;color:#cdd7e6;word-break:break-all;min-height:1.2em}</style></head><body><h1>Nishi Gen — private</h1><p>Full OPAQUE aPAKE (RFC 9807). The passphrase never leaves your browser as anything crackable; the session rides a Service Worker, not a cookie.</p><div id=m>Log in to view the gallery.</div><input id=h placeholder=handle autocomplete=username><input id=p type=password placeholder=passphrase autocomplete=current-password><button onclick=login()>Login</button> <button onclick=reg()>Register</button><script>function $(i){return document.getElementById(i)} function M(t){$('m').textContent=t} function setTok(t){return new Promise(function(res){var r=indexedDB.open('nishi_gen',1);r.onupgradeneeded=function(e){e.target.result.createObjectStore('auth')};r.onsuccess=function(e){var db=e.target.result;var tx=db.transaction('auth','readwrite').objectStore('auth').put(t,'nsess');tx.onsuccess=function(){res()};tx.onerror=function(){res()}};r.onerror=function(){res()}})} async function reg(){M('Registering (memory-hard, a moment)...');try{var r=await fetch('/gen/auth/register',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'handle='+encodeURIComponent($('h').value)+'&pw='+encodeURIComponent($('p').value)});var j=await r.json();M(r.ok?('Registered. SAVE THIS RECOVERY MNEMONIC: '+j.mnemonic):('Register failed: '+(j.error||r.status)))}catch(e){M('error: '+e)}} async function login(){M('Logging in...');try{var r=await fetch('/gen/auth/login',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'handle='+encodeURIComponent($('h').value)+'&pw='+encodeURIComponent($('p').value)});var j=await r.json();if(!r.ok){M('Login failed: '+(j.error||r.status));return}await setTok(j.token);if('serviceWorker' in navigator){try{await navigator.serviceWorker.register('/gen/sw.js',{scope:'/gen/'});await navigator.serviceWorker.ready}catch(e){}}M('Logged in. Opening gallery...');location.href='/gen/'}catch(e){M('error: '+e)}}</script></body></html>" as *u8
189
190// Read the FULL request: loop until end-of-headers, then until Content-Length bytes of body are present.
191// A single sys_read can return only the headers (or a partial body) when the client splits the POST across
192// TCP segments -> the login body (handle/pw) arrives truncated -> olg_login fails -> intermittent 401.
193// This is the root cause of the flaky gallery login; GET requests (no body) were unaffected by it.
194func gw_read_full(cfd: i64, req: *u8, cap: i64) -> i64 {
195 var total: i64 = 0
196 var he: i64 = 0 - 1
197 while he < 0 {
198 if total >= cap { return total }
199 let r: i64 = sys_read(cfd, ((req as i64) + total) as *u8, cap - total)
200 if r <= 0 { return total }
201 total = total + r
202 he = gw_find(req, total, "\r\n\r\n" as *u8, 4)
203 }
204 let clbuf: *u8 = sys_mmap(32)
205 let cln: i64 = gw_hdr_val(req, he, "\r\nContent-Length:" as *u8, 17, clbuf, 32)
206 var need: i64 = he + 4
207 if cln > 0 { need = he + 4 + gw_atoi(clbuf) }
208 while total < need {
209 if total >= cap { return total }
210 let r2: i64 = sys_read(cfd, ((req as i64) + total) as *u8, cap - total)
211 if r2 <= 0 { return total }
212 total = total + r2
213 }
214 return total
215}
216
217// handle ONE connection: read request, route (OPAQUE auth + reverse-proxy /gen/* to backend bport).
218// Extracted from the daemon loop so the integration gate can compose it in-process. ctx = the
219// shared OPAQUE auth context (family realm nishi_site_admin).
220func gw_serve_conn(cfd: i64, ctx: *NxAuthContext, allow_reg: i64, bport: i64, session_ttl: i64, hr_path: *u8, ent_path: *u8) -> i64 {
221 sys_set_socket_timeout(cfd, 20)
222 let req: *u8 = sys_mmap(GGW_MAGIC_131072)
223 let rn: i64 = gw_read_full(cfd, req, GGW_MAGIC_131071)
224 if rn > 0 {
225 let he: i64 = gw_find(req, rn, "\r\n\r\n" as *u8, 4)
226 var body: *u8 = req; var bn: i64 = 0
227 if he >= 0 { body = ((req as i64) + he + 4) as *u8; bn = rn - he - 4 }
228 let now: i64 = sys_now_realtime_sec()
229 let path: *u8 = sys_mmap(GGW_MAGIC_8192)
230 let plen: i64 = gw_reqpath(req, rn, path, GGW_MAGIC_8192)
231 let resp: *u8 = sys_mmap(GGW_MAGIC_8192)
232
233 if gw_starts(req, rn, "POST /gen/auth/register" as *u8) == 1 {
234 if allow_reg == 1 {
235 let hbuf: *u8 = sys_mmap(128); let hl: *i64 = sys_mmap(16) as *i64
236 let pbuf: *u8 = sys_mmap(320); let pl: *i64 = sys_mmap(16) as *i64
237 nx_http_form_get_field(body, bn, "handle" as *u8, 6, hbuf, 127, hl)
238 nx_http_form_get_field(body, bn, "pw" as *u8, 2, pbuf, 319, pl)
239 let mn: *u8 = sys_mmap(512); let mnn: *i64 = sys_mmap(16) as *i64
240 if olg_register(ctx, hbuf, hl[0], pbuf, pl[0], mn, 512, mnn) == NX_MAUTH_OK {
241 var o: i64 = gw_cat(resp, 0, "{\"mnemonic\":\"" as *u8); o = gw_catb(resp, o, mn, mnn[0]); o = gw_cat(resp, o, "\"}" as *u8)
242 gw_send(cfd, "200 OK" as *u8, "application/json" as *u8, resp, o)
243 } else {
244 let o: i64 = gw_cat(resp, 0, "{\"error\":\"register failed\"}" as *u8)
245 gw_send(cfd, "400 Bad Request" as *u8, "application/json" as *u8, resp, o)
246 }
247 } else {
248 let o: i64 = gw_cat(resp, 0, "{\"error\":\"registration closed\"}" as *u8)
249 gw_send(cfd, "403 Forbidden" as *u8, "application/json" as *u8, resp, o)
250 }
251 } else { if gw_starts(req, rn, "POST /gen/auth/login" as *u8) == 1 {
252 let hbuf: *u8 = sys_mmap(128); let hl: *i64 = sys_mmap(16) as *i64
253 let pbuf: *u8 = sys_mmap(320); let pl: *i64 = sys_mmap(16) as *i64
254 nx_http_form_get_field(body, bn, "handle" as *u8, 6, hbuf, 127, hl)
255 nx_http_form_get_field(body, bn, "pw" as *u8, 2, pbuf, 319, pl)
256 let b64: *u8 = sys_mmap(512); let b64n: *i64 = sys_mmap(16) as *i64
257 if olg_login(ctx, hbuf, hl[0], pbuf, pl[0], b64, 512, b64n) == NX_MAUTH_OK {
258 var o: i64 = gw_cat(resp, 0, "{\"token\":\"" as *u8); o = gw_catb(resp, o, b64, b64n[0]); o = gw_cat(resp, o, "\"}" as *u8)
259 gw_send_ck(cfd, "application/json" as *u8, resp, o, b64, b64n[0], session_ttl)
260 } else {
261 let o: i64 = gw_cat(resp, 0, "{\"error\":\"invalid credentials\"}" as *u8)
262 gw_send(cfd, "401 Unauthorized" as *u8, "application/json" as *u8, resp, o)
263 }
264 } else { if gw_starts(req, rn, "GET /gen/auth/whoami" as *u8) == 1 {
265 let tb: *u8 = sys_mmap(512)
266 var tl: i64 = gw_hdr_val(req, he, "X-Nishi-Session:" as *u8, 16, tb, 512)
267 if tl == 0 { tl = gw_cookie_val(req, he, tb, 512) }
268 let uh: *u8 = sys_mmap(64); let uhn: *i64 = sys_mmap(16) as *i64
269 if olg_whoami(ctx, tb, tl, now, uh, 64, uhn) == NX_MAUTH_OK {
270 let o: i64 = gw_cat(resp, 0, "{\"ok\":1}" as *u8)
271 gw_send(cfd, "200 OK" as *u8, "application/json" as *u8, resp, o)
272 } else {
273 gw_401(cfd)
274 }
275 } else { if gw_starts(req, rn, "GET /gen/sw.js" as *u8) == 1 {
276 gw_send(cfd, "200 OK" as *u8, "application/javascript" as *u8, GGW_SW_JS, gw_slen(GGW_SW_JS))
277 } else { if gw_starts(req, rn, "GET /gen/login" as *u8) == 1 {
278 gw_send(cfd, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, GGW_LOGIN_HTML, gw_slen(GGW_LOGIN_HTML))
279 } else {
280 if gw_starts(path, plen, "/gen" as *u8) == 1 {
281 let tb: *u8 = sys_mmap(512)
282 var tl: i64 = gw_hdr_val(req, he, "X-Nishi-Session:" as *u8, 16, tb, 512)
283 if tl == 0 { tl = gw_cookie_val(req, he, tb, 512) }
284 let uh: *u8 = sys_mmap(64); let uhn: *i64 = sys_mmap(16) as *i64
285 if olg_whoami(ctx, tb, tl, now, uh, 64, uhn) == NX_MAUTH_OK {
286 // olg_whoami returns the RAW 32-byte uid; the HR store keys by HEX(uid) = the cred_id.
287 // hex-encode FIRST (do NOT pass the uid back through hr_cred_id, which re-hashes it).
288 // Owner = HR superadmin -> auto /gen; others need an explicit he_ent_put(<cred_id_hex>,/gen).
289 let cidhex: *u8 = sys_mmap(96)
290 let chl: i64 = hr_hexenc(uh, uhn[0], cidhex); cidhex[chl] = 0 as u8
291 let isuper: i64 = hra_is_superadmin(hr_path, cidhex, chl)
292 if he_has_access(isuper, cidhex, chl, ent_path, "/gen" as *u8) != 1 { gw_403(cfd) } else {
293 var bp: *u8 = ((path as i64) + 4) as *u8
294 var bpl: i64 = plen - 4
295 if bpl <= 0 { bp = "/" as *u8; bpl = 1 }
296 let msp: i64 = gw_find(req, rn, " " as *u8, 1)
297 gw_proxy(cfd, bport, req, msp, bp, bpl, body, bn, req, he)
298 }
299 } else {
300 if gw_is_nav(req, rn) == 1 { gw_302_login(cfd) } else { gw_401(cfd) }
301 }
302 } else {
303 let o: i64 = gw_cat(resp, 0, "not found" as *u8)
304 gw_send(cfd, "404 Not Found" as *u8, "text/plain" as *u8, resp, o)
305 }
306 } } } } }
307 }
308 sys_close(cfd)
309 return 0
310}