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 &mdash; 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}