code wiki / _hdl_build / nx_gallery_gateway.nx
nx_gallery_gateway.nx source
↩ module page · 451 lines · 31130 B
1// nx_gallery_gateway.nx -- OPAQUE-gated reverse proxy for the NSFW gallery, mounted under /gallery/*.
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 /gallery/* 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 /gallery/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_connect.nx" // bounded connect
13const GGW_MAGIC_262144: i64 = 262144
14const GGW_MAGIC_6291456: i64 = 6291456
15const GGW_MAGIC_131072: i64 = 131072
16const GGW_MAGIC_4194304: i64 = 4194304
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
27func gw_slen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
28func 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 }
29func 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 }
30func gw_find(buf: *u8, n: i64, needle: *u8, nl: i64) -> i64 {
31 if nl==0 { return 0 }
32 var i: i64=0
33 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 }
34 return 0-1
35}
36func 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 }
37func 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 }
38func 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 }
39
40// send a full HTTP/1.1 response (single write). status e.g. "200 OK", ctype e.g. "text/html".
41func gw_send(cfd: i64, status: *u8, ctype: *u8, body: *u8, blen: i64) -> i64 {
42 let buf: *u8 = sys_mmap(GGW_MAGIC_262144); var o: i64 = 0
43 o = gw_cat(buf, o, "HTTP/1.1 " as *u8); o = gw_cat(buf, o, status)
44 o = gw_cat(buf, o, "\r\nContent-Type: " as *u8); o = gw_cat(buf, o, ctype)
45 o = gw_cat(buf, o, "\r\nContent-Length: " as *u8); o = gw_itoa(buf, o, blen)
46 o = gw_cat(buf, o, "\r\nConnection: close\r\nCache-Control: no-store\r\n\r\n" as *u8)
47 o = gw_catb(buf, o, body, blen)
48 sys_write(cfd, buf, o); return 0
49}
50func gw_401(cfd: i64) -> i64 {
51 let b: *u8 = "{\"error\":\"login required\"}" as *u8
52 gw_send(cfd, "401 Unauthorized" as *u8, "application/json" as *u8, b, gw_slen(b)); return 0
53}
54// 1 iff this looks like a top-level page navigation (so an expired session should bounce to the login page,
55// not flash a raw 401/blank grid). Sec-Fetch-Mode: navigate is the browser-set, spoof-irrelevant signal.
56func 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 }
57// 302 to the login page (graceful re-login). Body-less; no-store so the redirect itself is never cached.
58func gw_302_login(cfd: i64) -> i64 {
59 let b: *u8 = "HTTP/1.1 302 Found\r\nLocation: /gallery/login\r\nContent-Length: 0\r\nCache-Control: no-store\r\nConnection: close\r\n\r\n" as *u8
60 sys_write(cfd, b, gw_slen(b)); return 0
61}
62// 200 response that ALSO sets the session as an HttpOnly cookie -> the browser sends it on every /gallery/*
63// request (page navigation, <img>, <video>) so the page load itself authenticates, no Service-Worker timing
64// dependency. HttpOnly = JS cannot read it; Secure = HTTPS only; SameSite=Strict = no cross-site send.
65func gw_send_ck(cfd: i64, ctype: *u8, body: *u8, blen: i64, ckval: *u8, ckvallen: i64, ttl: i64) -> i64 {
66 let buf: *u8 = sys_mmap(GGW_MAGIC_262144); var o: i64 = 0
67 o = gw_cat(buf, o, "HTTP/1.1 200 OK\r\nContent-Type: " as *u8); o = gw_cat(buf, o, ctype)
68 o = gw_cat(buf, o, "\r\nSet-Cookie: ngs=" as *u8); o = gw_catb(buf, o, ckval, ckvallen)
69 o = gw_cat(buf, o, "; HttpOnly; Secure; SameSite=Strict; Path=/gallery; Max-Age=" as *u8); o = gw_itoa(buf, o, ttl)
70 o = gw_cat(buf, o, "\r\nContent-Length: " as *u8); o = gw_itoa(buf, o, blen)
71 o = gw_cat(buf, o, "\r\nConnection: close\r\nCache-Control: no-store\r\n\r\n" as *u8)
72 o = gw_catb(buf, o, body, blen)
73 sys_write(cfd, buf, o); return 0
74}
75// pull the session token from the `ngs=` cookie in req[0..hend] -> len into out (NUL-term).
76func gw_cookie_val(req: *u8, hend: i64, out: *u8, cap: i64) -> i64 {
77 let p: i64 = gw_find(req, hend, "ngs=" as *u8, 4)
78 if p < 0 { out[0]=0 as u8; return 0 }
79 var i: i64 = p + 4; var o: i64 = 0
80 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 } } } } }
81 out[o]=0 as u8; return o
82}
83// request header value for `name` (incl trailing ':') over req[0..hend] -> len into out (NUL-term)
84func gw_hdr_val(req: *u8, hend: i64, name: *u8, nl: i64, out: *u8, cap: i64) -> i64 {
85 let p: i64 = gw_find(req, hend, name, nl)
86 if p < 0 { out[0]=0 as u8; return 0 }
87 var i: i64 = p + nl
88 if i < hend { if req[i]==(32 as u8) { i=i+1 } }
89 var o: i64 = 0
90 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 } } }
91 out[o]=0 as u8; return o
92}
93// request-target path (between first space and next space) -> len into out (NUL-term)
94func gw_reqpath(req: *u8, rn: i64, out: *u8, cap: i64) -> i64 {
95 var s1: i64 = 0-1; var i: i64 = 0
96 while i < rn { if req[i]==(32 as u8) { s1=i; i=rn } else { i=i+1 } }
97 if s1 < 0 { out[0]=0 as u8; return 0 }
98 var p: i64 = s1+1; var o: i64 = 0
99 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 } }
100 out[o]=0 as u8; return o
101}
102// reverse-proxy: connect 127.0.0.1:bport, forward method + backend_path (+ body), relay response to cfd.
103// buffered relay (R1: API/images); streaming/range hardening = R4. Returns bytes relayed (or negative).
104// emit "Range: <rng>\r\n" verbatim (pass-through for suffix/multi-range/unparseable forms).
105func gw_emit_range_raw(rq: *u8, o0: i64, rng: *u8, rngn: i64) -> i64 {
106 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
107}
108// Cap an OPEN-ENDED or oversized byte-range to a CHUNK window so the whole 206 fits the buffered front proxy
109// (sites_v2 reads the backend response fully into an 8MB buffer before sending; a 206 claiming the full file
110// would be truncated -> the browser rejects the malformed partial). "bytes=START-" / span>CHUNK becomes
111// "bytes=START-(START+CHUNK-1)"; the <video> element fetches the next window as it plays/seeks. Small specific
112// ranges, suffix ranges (bytes=-N), and multi-ranges pass through unchanged.
113func gw_cap_range(rq: *u8, o0: i64, rng: *u8, rngn: i64) -> i64 {
114 let CHUNK: i64 = GGW_MAGIC_6291456
115 var eq: i64 = 0 - 1; var comma: i64 = 0; var k: i64 = 0
116 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 }
117 if eq < 0 { return gw_emit_range_raw(rq, o0, rng, rngn) }
118 if comma == 1 { return gw_emit_range_raw(rq, o0, rng, rngn) }
119 var p: i64 = eq + 1; var start: i64 = 0; var sany: i64 = 0
120 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 } }
121 if sany == 0 { return gw_emit_range_raw(rq, o0, rng, rngn) }
122 var dash: i64 = 0 - 1; var d: i64 = eq+1
123 while d < rngn { if rng[d]==(45 as u8) { dash=d; d=rngn } else { d=d+1 } }
124 var end: i64 = 0; var eany: i64 = 0
125 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 } } }
126 if eany == 0 { end = start + CHUNK - 1 } else { if (end - start + 1) > CHUNK { end = start + CHUNK - 1 } }
127 var o: i64 = gw_cat(rq, o0, "Range: bytes=" as *u8)
128 o = gw_itoa(rq, o, start); rq[o]=45 as u8; o=o+1; o = gw_itoa(rq, o, end)
129 o = gw_cat(rq, o, "\r\n" as *u8)
130 return o
131}
132
133func gw_proxy(cfd: i64, bport: i64, method: *u8, mlen: i64, bpath: *u8, bplen: i64, body: *u8, blen: i64, oreq: *u8, ohe: i64) -> i64 {
134 let fd: i64 = sys_socket(2, 1, 0); if fd < 0 { return 0-1 }
135 sys_set_socket_timeout(fd, 20)
136 let a: *u8 = sys_mmap(16)
137 a[0]=2 as u8; a[1]=0 as u8; a[2]=((bport>>8)&0xff) as u8; a[3]=(bport&0xff) as u8
138 a[4]=127 as u8; a[5]=0 as u8; a[6]=0 as u8; a[7]=1 as u8
139 var zi: i64=8; while zi<16 { a[zi]=0 as u8; zi=zi+1 }
140 if nx_connect_bounded(fd, a, 16, NX_CONN_DEFAULT_MS) != 0 { sys_close(fd); return 0-2 }
141 let rq: *u8 = sys_mmap(GGW_MAGIC_131072); var o: i64 = 0
142 o = gw_catb(rq, o, method, mlen); rq[o]=32 as u8; o=o+1
143 o = gw_catb(rq, o, bpath, bplen)
144 o = gw_cat(rq, o, " HTTP/1.1\r\nHost: 127.0.0.1\r\nAccept: */*\r\nConnection: close\r\n" as *u8)
145 // Forward the client's Range header (anchored on a header-line start so "If-Range:" can't false-match)
146 // so the backend answers 206 Partial Content + Content-Range + Accept-Ranges -> native <video> seeks and
147 // plays (esp. iOS/Safari, which refuse a 200 full-file response). No Range present -> byte-identical req.
148 let rng: *u8 = sys_mmap(256)
149 let rngn: i64 = gw_hdr_val(oreq, ohe, "\r\nRange:" as *u8, 8, rng, 256)
150 if rngn > 0 { o = gw_cap_range(rq, o, rng, rngn) }
151 if blen > 0 {
152 o = gw_cat(rq, o, "Content-Type: application/x-www-form-urlencoded\r\nContent-Length: " as *u8)
153 o = gw_itoa(rq, o, blen); o = gw_cat(rq, o, "\r\n\r\n" as *u8)
154 o = gw_catb(rq, o, body, blen)
155 } else {
156 o = gw_cat(rq, o, "\r\n" as *u8)
157 }
158 sys_write(fd, rq, o)
159 let buf: *u8 = sys_mmap(GGW_MAGIC_262144)
160 var total: i64 = 0; var go: i64 = 1
161 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 } }
162 sys_close(fd)
163 return total
164}
165
166// The Service Worker (served at /gallery/sw.js, scope /gallery/): injects X-Nishi-Session (from IndexedDB)
167// into every /gallery/* request EXCEPT the auth + sw.js + login bootstrap. No cookie. (Browser-tested in R4.)
168const GGW_SW_JS: *u8 = "var DBN='nishi_gallery',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('/gallery/')!==0){return}if(pn.indexOf('/gallery/auth/')===0||pn==='/gallery/sw.js'||pn==='/gallery/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
169
170// The login + SW-bootstrap page (served at /gallery/login). OPAQUE login -> store token in IndexedDB ->
171// register the SW (scope /gallery/) -> go to /gallery/. No passphrase ever leaves as anything but OPAQUE.
172const GGW_LOGIN_HTML: *u8 = "<!doctype html><html><head><meta charset=utf-8><meta name=viewport content=\"width=device-width,initial-scale=1\"><title>Nishi Gallery</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 Gallery — 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_gallery',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('/gallery/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{if('serviceWorker' in navigator){try{var rs=await navigator.serviceWorker.getRegistrations();for(var i=0;i<rs.length;i++){await rs[i].unregister()}}catch(e){}}var r=await fetch('/gallery/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('/gallery/sw.js',{scope:'/gallery/'});await navigator.serviceWorker.ready}catch(e){}}M('Logged in. Opening gallery...');location.href='/gallery/'}catch(e){M('error: '+e)}}</script></body></html>" as *u8
173
174// Read the FULL request: loop until end-of-headers, then until Content-Length bytes of body are present.
175// A single sys_read can return only the headers (or a partial body) when the client splits the POST across
176// TCP segments -> the login body (handle/pw) arrives truncated -> olg_login fails -> intermittent 401.
177// This is the root cause of the flaky gallery login; GET requests (no body) were unaffected by it.
178func gw_read_full(cfd: i64, req: *u8, cap: i64) -> i64 {
179 var total: i64 = 0
180 var he: i64 = 0 - 1
181 while he < 0 {
182 if total >= cap { return total }
183 let r: i64 = sys_read(cfd, ((req as i64) + total) as *u8, cap - total)
184 if r <= 0 { return total }
185 total = total + r
186 he = gw_find(req, total, "\r\n\r\n" as *u8, 4)
187 }
188 let clbuf: *u8 = sys_mmap(32)
189 let cln: i64 = gw_hdr_val(req, he, "\r\nContent-Length:" as *u8, 17, clbuf, 32)
190 var need: i64 = he + 4
191 if cln > 0 { need = he + 4 + gw_atoi(clbuf) }
192 while total < need {
193 if total >= cap { return total }
194 let r2: i64 = sys_read(cfd, ((req as i64) + total) as *u8, cap - total)
195 if r2 <= 0 { return total }
196 total = total + r2
197 }
198 return total
199}
200
201// GET /gallery/curator -> the interactive data-curator catalog surface (sort/filter across every collection).
202// GATED by the SAME opaque session as the gallery proper: the file lists NSFW collection/handle names, so it
203// must NEVER be a public byte (cardinal C1). Reads the pre-generated static HTML from disk and streams it
204// (header then body). Fork-per-request (the accept loop) frees the read buffer BY CONSTRUCTION on sys_exit.
205const GGW_CURATOR_PATH: *u8 = "/volume1/homes/elderwesto/nishihost/curator.html" as *u8
206func gw_serve_curator(cfd: i64) -> i64 {
207 let fd: i64 = sys_openat_rd(GGW_CURATOR_PATH)
208 if fd < 0 {
209 let b: *u8 = "curator report not generated yet" as *u8
210 gw_send(cfd, "503 Service Unavailable" as *u8, "text/plain" as *u8, b, gw_slen(b)); return 0
211 }
212 let cap: i64 = GGW_MAGIC_4194304
213 let buf: *u8 = sys_mmap(cap)
214 var total: i64 = 0; var go: i64 = 1
215 while go == 1 {
216 let r: i64 = sys_read(fd, ((buf as i64) + total) as *u8, cap - total)
217 if r <= 0 { go = 0 } else { total = total + r; if total >= cap { go = 0 } }
218 }
219 sys_close(fd)
220 let hdr: *u8 = sys_mmap(512); var o: i64 = 0
221 o = gw_cat(hdr, o, "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: " as *u8)
222 o = gw_itoa(hdr, o, total)
223 o = gw_cat(hdr, o, "\r\nConnection: close\r\nCache-Control: no-store\r\n\r\n" as *u8)
224 sys_write(cfd, hdr, o)
225 sys_write(cfd, buf, total)
226 return 0
227}
228
229// ---- corpus contact sheets (entity media gather), served ONLY to an authenticated session ----
230// These are per-entity visual validation sheets rendered by nx_contactsheet from knowledge/media/<entity>/.
231// They live in their OWN directory and are NOT part of the gallery's cid corpus -- nothing here touches the
232// existing library index.
233const GGW_SHEETS_DIR: *u8 = "/volume1/homes/elderwesto/nishihost/knowledge/media/_sheets/" as *u8
234
235// Serve /gallery/sheets/<slug>.png. TRAVERSAL-IMPOSSIBLE BY CONSTRUCTION: the slug is REBUILT from
236// characters we validated as [a-z0-9_-], and WE append the ".png" -- no '.', '/', '\\' or NUL from the
237// request can survive into the filename, so there is no path to escape the directory.
238func gw_serve_sheet(cfd: i64, path: *u8, plen: i64) -> i64 {
239 let pre: i64 = 16 // len("/gallery/sheets/")
240 let slug: *u8 = sys_mmap(320)
241 var so: i64 = 0
242 var ok: i64 = 1
243 var done: i64 = 0
244 var i: i64 = pre
245 while i < plen {
246 let c: i64 = path[i] as i64
247 if done == 0 {
248 if c == 46 { done = 1 }
249 if done == 0 {
250 var good: i64 = 0
251 if c >= 97 { if c <= 122 { good = 1 } }
252 if c >= 48 { if c <= 57 { good = 1 } }
253 if c == 95 { good = 1 }
254 if c == 45 { good = 1 }
255 if good == 0 { ok = 0 }
256 if good == 1 { if so < 200 { slug[so] = c as u8; so = so + 1 } }
257 }
258 }
259 i = i + 1
260 }
261 if ok == 0 { let b: *u8 = "bad sheet name" as *u8; gw_send(cfd, "400 Bad Request" as *u8, "text/plain" as *u8, b, gw_slen(b)); return 0 }
262 if so <= 0 { let b: *u8 = "bad sheet name" as *u8; gw_send(cfd, "400 Bad Request" as *u8, "text/plain" as *u8, b, gw_slen(b)); return 0 }
263
264 let fp: *u8 = sys_mmap(512)
265 var fo: i64 = gw_cat(fp, 0, GGW_SHEETS_DIR)
266 fo = gw_catb(fp, fo, slug, so)
267 fo = gw_cat(fp, fo, ".png" as *u8)
268 fp[fo] = 0 as u8
269
270 let fd: i64 = sys_openat_rd(fp)
271 if fd < 0 { let b: *u8 = "no such sheet" as *u8; gw_send(cfd, "404 Not Found" as *u8, "text/plain" as *u8, b, gw_slen(b)); return 0 }
272 let cap: i64 = GGW_MAGIC_6291456
273 let buf: *u8 = sys_mmap(cap)
274 var total: i64 = 0
275 var go: i64 = 1
276 while go == 1 {
277 let r: i64 = sys_read(fd, ((buf as i64) + total) as *u8, cap - total)
278 if r <= 0 { go = 0 } else { total = total + r; if total >= cap { go = 0 } }
279 }
280 sys_close(fd)
281 let hdr: *u8 = sys_mmap(512)
282 var o: i64 = 0
283 o = gw_cat(hdr, o, "HTTP/1.1 200 OK\r\nContent-Type: image/png\r\nContent-Length: " as *u8)
284 o = gw_itoa(hdr, o, total)
285 o = gw_cat(hdr, o, "\r\nConnection: close\r\nCache-Control: no-store\r\nX-Content-Type-Options: nosniff\r\n\r\n" as *u8)
286 sys_write(cfd, hdr, o)
287 sys_write(cfd, buf, total)
288 return 0
289}
290
291func main(argc: i64, argv: *i64) -> i64 {
292 if argc < 6 {
293 sys_write(1, "usage: nx_gallery_gateway <port> <keys> <store> <budget> <backend_port> [allow_register] [m] [t] [p]\n" as *u8, 100)
294 sys_exit(2); return 2
295 }
296 let port: i64 = gw_atoi(argv[1] as *u8)
297 let keys_path: *u8 = argv[2] as *u8
298 let store_path: *u8 = argv[3] as *u8
299 let budget: i64 = gw_atoi(argv[4] as *u8)
300 let bport: i64 = gw_atoi(argv[5] as *u8)
301 var allow_reg: i64 = 0
302 if argc > 6 { allow_reg = gw_atoi(argv[6] as *u8) }
303 var m_cost: i64 = GGW_PROD_M
304 if argc > 7 { m_cost = gw_atoi(argv[7] as *u8) }
305 var t_cost: i64 = 3
306 if argc > 8 { t_cost = gw_atoi(argv[8] as *u8) }
307 var p_cost: i64 = 4
308 if argc > 9 { p_cost = gw_atoi(argv[9] as *u8) }
309 var session_ttl: i64 = GGW_SESSION_TTL
310 if argc > 10 { session_ttl = gw_atoi(argv[10] as *u8) }
311
312 let ctx: *NxAuthContext = sys_mmap(256) as *NxAuthContext
313 if olg_ctx_setup_ttl(ctx, keys_path, store_path, "nishi_gallery" as *u8, 13, "Nishi Gallery" as *u8, 13, session_ttl, m_cost, t_cost, p_cost) != 0 {
314 sys_write(1, "CTX-INIT-FAIL\n" as *u8, 14); sys_exit(1); return 1
315 }
316
317 let addr: *u8 = sys_mmap(16)
318 addr[0]=2 as u8; addr[1]=0 as u8
319 addr[2]=((port>>8)&0xff) as u8; addr[3]=(port&0xff) as u8
320 addr[4]=0 as u8; addr[5]=0 as u8; addr[6]=0 as u8; addr[7]=0 as u8
321 var zi: i64=8; while zi<16 { addr[zi]=0 as u8; zi=zi+1 }
322 let lfd: i64 = sys_socket(2, 1, 0)
323 if lfd < 0 { sys_write(1, "SOCKET-FAIL\n" as *u8, 12); sys_exit(1); return 1 }
324 let optv: *u8 = sys_mmap(4); optv[0]=1 as u8
325 sys_setsockopt(lfd, 1, 2, optv, 4)
326 if sys_bind(lfd, addr, 16) < 0 { sys_write(1, "BIND-FAIL\n" as *u8, 10); sys_exit(1); return 1 }
327 if sys_listen(lfd, 16) < 0 { sys_write(1, "LISTEN-FAIL\n" as *u8, 12); sys_exit(1); return 1 }
328 sys_write(1, "GALLERY-GATEWAY-UP\n" as *u8, 19)
329
330 let st: *i64 = sys_mmap(16) as *i64
331 var served: i64 = 0
332 while served < budget {
333 let cfd: i64 = sys_accept(lfd)
334 if cfd >= 0 {
335 let pid: i64 = sys_fork()
336 if pid == 0 {
337 sys_close(lfd)
338 sys_set_socket_timeout(cfd, 20)
339 let req: *u8 = sys_mmap(GGW_MAGIC_131072)
340 let rn: i64 = gw_read_full(cfd, req, GGW_MAGIC_131071)
341 if rn > 0 {
342 let he: i64 = gw_find(req, rn, "\r\n\r\n" as *u8, 4)
343 var body: *u8 = req; var bn: i64 = 0
344 if he >= 0 { body = ((req as i64) + he + 4) as *u8; bn = rn - he - 4 }
345 let now: i64 = sys_now_realtime_sec()
346 let path: *u8 = sys_mmap(GGW_MAGIC_8192)
347 let plen: i64 = gw_reqpath(req, rn, path, GGW_MAGIC_8192)
348 let resp: *u8 = sys_mmap(GGW_MAGIC_8192)
349
350 if gw_starts(req, rn, "POST /gallery/auth/register" as *u8) == 1 {
351 if allow_reg == 1 {
352 let hbuf: *u8 = sys_mmap(128); let hl: *i64 = sys_mmap(16) as *i64
353 let pbuf: *u8 = sys_mmap(320); let pl: *i64 = sys_mmap(16) as *i64
354 nx_http_form_get_field(body, bn, "handle" as *u8, 6, hbuf, 127, hl)
355 nx_http_form_get_field(body, bn, "pw" as *u8, 2, pbuf, 319, pl)
356 let mn: *u8 = sys_mmap(512); let mnn: *i64 = sys_mmap(16) as *i64
357 if olg_register(ctx, hbuf, hl[0], pbuf, pl[0], mn, 512, mnn) == NX_MAUTH_OK {
358 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)
359 gw_send(cfd, "200 OK" as *u8, "application/json" as *u8, resp, o)
360 } else {
361 let o: i64 = gw_cat(resp, 0, "{\"error\":\"register failed\"}" as *u8)
362 gw_send(cfd, "400 Bad Request" as *u8, "application/json" as *u8, resp, o)
363 }
364 } else {
365 let o: i64 = gw_cat(resp, 0, "{\"error\":\"registration closed\"}" as *u8)
366 gw_send(cfd, "403 Forbidden" as *u8, "application/json" as *u8, resp, o)
367 }
368 } else { if gw_starts(req, rn, "POST /gallery/auth/login" as *u8) == 1 {
369 let hbuf: *u8 = sys_mmap(128); let hl: *i64 = sys_mmap(16) as *i64
370 let pbuf: *u8 = sys_mmap(320); let pl: *i64 = sys_mmap(16) as *i64
371 nx_http_form_get_field(body, bn, "handle" as *u8, 6, hbuf, 127, hl)
372 nx_http_form_get_field(body, bn, "pw" as *u8, 2, pbuf, 319, pl)
373 let b64: *u8 = sys_mmap(512); let b64n: *i64 = sys_mmap(16) as *i64
374 if olg_login(ctx, hbuf, hl[0], pbuf, pl[0], b64, 512, b64n) == NX_MAUTH_OK {
375 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)
376 gw_send_ck(cfd, "application/json" as *u8, resp, o, b64, b64n[0], session_ttl)
377 } else {
378 let o: i64 = gw_cat(resp, 0, "{\"error\":\"invalid credentials\"}" as *u8)
379 gw_send(cfd, "401 Unauthorized" as *u8, "application/json" as *u8, resp, o)
380 }
381 } else { if gw_starts(req, rn, "GET /gallery/auth/whoami" as *u8) == 1 {
382 let tb: *u8 = sys_mmap(512)
383 var tl: i64 = gw_hdr_val(req, he, "X-Nishi-Session:" as *u8, 16, tb, 512)
384 if tl == 0 { tl = gw_cookie_val(req, he, tb, 512) }
385 let uh: *u8 = sys_mmap(64); let uhn: *i64 = sys_mmap(16) as *i64
386 if olg_whoami(ctx, tb, tl, now, uh, 64, uhn) == NX_MAUTH_OK {
387 let o: i64 = gw_cat(resp, 0, "{\"ok\":1}" as *u8)
388 gw_send(cfd, "200 OK" as *u8, "application/json" as *u8, resp, o)
389 } else {
390 gw_401(cfd)
391 }
392 } else { if gw_starts(req, rn, "GET /gallery/sw.js" as *u8) == 1 {
393 gw_send(cfd, "200 OK" as *u8, "application/javascript" as *u8, GGW_SW_JS, gw_slen(GGW_SW_JS))
394 } else { if gw_starts(req, rn, "GET /gallery/login" as *u8) == 1 {
395 gw_send(cfd, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, GGW_LOGIN_HTML, gw_slen(GGW_LOGIN_HTML))
396 } else { if gw_starts(req, rn, "GET /gallery/curator" as *u8) == 1 {
397 let tb2: *u8 = sys_mmap(512)
398 var tl2: i64 = gw_hdr_val(req, he, "X-Nishi-Session:" as *u8, 16, tb2, 512)
399 if tl2 == 0 { tl2 = gw_cookie_val(req, he, tb2, 512) }
400 let uh2: *u8 = sys_mmap(64); let uhn2: *i64 = sys_mmap(16) as *i64
401 if olg_whoami(ctx, tb2, tl2, now, uh2, 64, uhn2) == NX_MAUTH_OK {
402 gw_serve_curator(cfd)
403 } else {
404 if gw_is_nav(req, rn) == 1 { gw_302_login(cfd) } else { gw_401(cfd) }
405 }
406 } else {
407 // corpus contact sheets -- SAME session check as every other /gallery/* route, and it is
408 // checked BEFORE the proxy branch so it never reaches the library backend.
409 if gw_starts(req, rn, "GET /gallery/sheets/" as *u8) == 1 {
410 let tbs: *u8 = sys_mmap(512)
411 var tls: i64 = gw_hdr_val(req, he, "X-Nishi-Session:" as *u8, 16, tbs, 512)
412 if tls == 0 { tls = gw_cookie_val(req, he, tbs, 512) }
413 let uhs: *u8 = sys_mmap(64); let uhns: *i64 = sys_mmap(16) as *i64
414 if olg_whoami(ctx, tbs, tls, now, uhs, 64, uhns) == NX_MAUTH_OK {
415 gw_serve_sheet(cfd, path, plen)
416 } else {
417 if gw_is_nav(req, rn) == 1 { gw_302_login(cfd) } else { gw_401(cfd) }
418 }
419 } else {
420 if gw_starts(path, plen, "/gallery" as *u8) == 1 {
421 let tb: *u8 = sys_mmap(512)
422 var tl: i64 = gw_hdr_val(req, he, "X-Nishi-Session:" as *u8, 16, tb, 512)
423 if tl == 0 { tl = gw_cookie_val(req, he, tb, 512) }
424 let uh: *u8 = sys_mmap(64); let uhn: *i64 = sys_mmap(16) as *i64
425 if olg_whoami(ctx, tb, tl, now, uh, 64, uhn) == NX_MAUTH_OK {
426 var bp: *u8 = ((path as i64) + 8) as *u8
427 var bpl: i64 = plen - 8
428 if bpl <= 0 { bp = "/" as *u8; bpl = 1 }
429 let msp: i64 = gw_find(req, rn, " " as *u8, 1)
430 gw_proxy(cfd, bport, req, msp, bp, bpl, body, bn, req, he)
431 } else {
432 if gw_is_nav(req, rn) == 1 { gw_302_login(cfd) } else { gw_401(cfd) }
433 }
434 } else {
435 let o: i64 = gw_cat(resp, 0, "not found" as *u8)
436 gw_send(cfd, "404 Not Found" as *u8, "text/plain" as *u8, resp, o)
437 }
438 }
439 } } } } } }
440 }
441 sys_close(cfd)
442 sys_exit(0)
443 }
444 sys_close(cfd)
445 var reaped: i64 = 1
446 while reaped > 0 { reaped = sys_wait4(0 - 1, st, 1) }
447 }
448 served = served + 1
449 }
450 sys_close(lfd); sys_exit(0); return 0
451}