code wiki / _hdl_build / nx_torrent_gw.nx

nx_torrent_gw.nx source

↩ module page · 212 lines · 12723 B

1// nx_torrent_gw.nx -- the MEDIA/TORRENT acquirer's OPAQUE auth gateway (loopback HTTP, :18793). 2// 3// Sits behind sites_daemon's fail-closed /torrent reverse-proxy (mirrors the /hub -> hub_gw topology). Per 4// request it resolves the OPAQUE no-cookie X-Nishi-Session token against the SITE-ADMIN realm (== the 5// operator/owner; the same keys/store the :9091 login daemon mints from), and acts FAIL-CLOSED: 6// valid session -> inject the TRUSTED internal header `X-Nishi-Level: 3` (OWNER) + reverse-proxy to the 7// torrent daemon (127.0.0.1:8097); the daemon trusts that header (Cardinal 12). 8// no token -> serve the no-cookie bootstrap (JS reads sessionStorage -> re-fetch WITH the header). 9// invalid token -> 302 /login. 10// ISOLATED from the daemon + the login daemon ON PURPOSE: a bug here can never regress them. The injected 11// X-Nishi-Level is PREPENDED right after the request line, so it is the FIRST one the daemon's d_trusted_level 12// sees -> a client-spoofed X-Nishi-Level (necessarily later) is ignored. Sovereign, no 3rd party. ORIGINAL. 13import "nx_opaque_login.nx" // olg_ctx_setup / olg_whoami + NxAuthContext + NX_MAUTH_OK 14import "nx_hub_gw_decide.nx" // hgw_hdr_val (reused header parser) 15import "nx_http_client.nx" // nx_http_client_sockaddr_ipv4 (+ socket syscalls via nx_syscalls) 16import "nx_torrent_access.nx" // NX_TACCESS_OWNER (== 3) 17import "nx_connect.nx" // bounded connect 18const TGW_MAGIC_65536: i64 = 65536 19const TGW_MAGIC_262144: i64 = 262144 20const TGW_MAGIC_18793: i64 = 18793 21const TGW_MAGIC_14400: i64 = 14400 22const TGW_MAGIC_16400: i64 = 16400 23const TGW_MAGIC_1000000000: i64 = 1000000000 24const TGW_MAGIC_16383: i64 = 16383 25 26const TGW_KEYS: *u8 = "/volume1/homes/elderwesto/nishihost/opaque_keys.bin" 27const TGW_STORE: *u8 = "/volume1/homes/elderwesto/nishihost/opaque_store.log" 28const TGW_BACKEND_PORT: i64 = 8097 29const TGW_BOOTSTRAP: *u8 = "<!DOCTYPE html><html lang='en'><head><meta charset='utf-8'><meta name='viewport' content='width=device-width, initial-scale=1'><title>Nishi Media</title></head><body><script>(function(){var t=localStorage.getItem('nsess')||sessionStorage.getItem('nsess');if(t){localStorage.setItem('nsess',t)}if(!t){location='/login?return=/torrent';return}fetch(location.pathname,{headers:{'X-Nishi-Session':t}}).then(function(r){return r.ok?r.text():null}).then(function(x){if(x){document.open();document.write(x);document.close()}else{localStorage.removeItem('nsess');sessionStorage.removeItem('nsess');location='/login?return=/torrent'}})})();</script></body></html>" 30 31func tgw_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 } 32func tgw_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 } 33func tgw_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 } 34func tgw_slen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n } 35func tgw_w(s: *u8) -> i64 { sys_write(1, s, tgw_slen(s)); return 0 } 36 37// ---- session-in-query for <video> streams: a <video src> can't send the X-Nishi-Session HEADER, so the UI 38// puts the token in ?nsess=<encodeURIComponent(token)>. Extract+URL-decode it, and STRIP the ?query before 39// forwarding so the daemon's path parser (path-until-space) sees a clean /torrent/stream/<id>. ---- 40func tgw_hexv(c: i64) -> i64 { 41 if c >= 48 { if c <= 57 { return c - 48 } } 42 if c >= 97 { if c <= 102 { return c - 97 + 10 } } 43 if c >= 65 { if c <= 70 { return c - 65 + 10 } } 44 return 0 - 1 45} 46func tgw_query_nsess(req: *u8, rn: i64, out: *u8, cap: i64) -> i64 { 47 var lim: i64 = rn; var li: i64 = 0 48 while li < rn { if req[li] == (13 as u8) { lim = li; li = rn } else { li = li + 1 } } // request line ends at first \r 49 let key: *u8 = "nsess=" as *u8 50 var i: i64 = 0; var st: i64 = 0 - 1 51 while i + 6 <= lim { 52 var j: i64 = 0; var m: i64 = 1 53 while j < 6 { if req[i+j] != key[j] { m = 0; j = 6 } else { j = j + 1 } } 54 if m == 1 { st = i + 6; i = lim } else { i = i + 1 } 55 } 56 if st < 0 { return 0 } 57 var o: i64 = 0; var p: i64 = st; var go: i64 = 1 58 while go == 1 { 59 if p >= lim { go = 0 } else { 60 let c: i64 = req[p] as i64 61 if c == 38 { go = 0 } else { if c == 32 { go = 0 } else { if o < cap { out[o] = c as u8; o = o + 1 } p = p + 1 } } 62 } 63 } 64 return o 65} 66// copy req[0..rl) into fwd@o, dropping any ?query (from the first '?' to the last space before HTTP/1.1). Returns new offset. 67func tgw_reqline_noq(fwd: *u8, o: i64, req: *u8, rl: i64) -> i64 { 68 var q: i64 = 0 - 1; var sp: i64 = 0 - 1; var i: i64 = 0 69 while i < rl { 70 if req[i] == (63 as u8) { if q < 0 { q = i } } 71 if req[i] == (32 as u8) { sp = i } 72 i = i + 1 73 } 74 var no: i64 = o 75 var strip: i64 = 0 76 if q >= 0 { if sp > q { strip = 1 } } 77 if strip == 1 { 78 var j: i64 = 0 79 while j < q { fwd[no] = req[j]; no = no + 1; j = j + 1 } 80 j = sp 81 while j < rl { fwd[no] = req[j]; no = no + 1; j = j + 1 } 82 } else { 83 var k: i64 = 0 84 while k < rl { fwd[no] = req[k]; no = no + 1; k = k + 1 } 85 } 86 return no 87} 88// does req[0..rn) contain the (space-prefixed) path needle? routes GET /torrent/streamtkt. 89func tgw_req_has(req: *u8, rn: i64, needle: *u8, nl: i64) -> i64 { 90 var i: i64 = 0 91 while i + nl <= rn { 92 var j: i64 = 0; var m: i64 = 1 93 while j < nl { if req[i+j] != needle[j] { m = 0; j = nl } else { j = j + 1 } } 94 if m == 1 { return 1 } 95 i = i + 1 96 } 97 return 0 98} 99 100func tgw_send(cfd: i64, scratch: *u8, status: *u8, ctype: *u8, body: *u8, blen: i64) -> i64 { 101 var o: i64=0 102 o=tgw_cat(scratch,o,"HTTP/1.1 " as *u8); o=tgw_cat(scratch,o,status) 103 o=tgw_cat(scratch,o,"\r\nContent-Type: " as *u8); o=tgw_cat(scratch,o,ctype) 104 o=tgw_cat(scratch,o,"\r\nContent-Length: " as *u8); o=tgw_itoa(scratch,o,blen) 105 o=tgw_cat(scratch,o,"\r\nConnection: close\r\nCache-Control: no-store\r\n\r\n" as *u8) 106 var i: i64=0; while i<blen { scratch[o]=body[i]; o=o+1; i=i+1 } 107 sys_write(cfd, scratch, o); return o 108} 109func tgw_redirect(cfd: i64, scratch: *u8, loc: *u8) -> i64 { 110 var o: i64=0 111 o=tgw_cat(scratch,o,"HTTP/1.1 302 Found\r\nLocation: " as *u8); o=tgw_cat(scratch,o,loc) 112 o=tgw_cat(scratch,o,"\r\nContent-Length: 0\r\nConnection: close\r\nCache-Control: no-store\r\n\r\n" as *u8) 113 sys_write(cfd, scratch, o); return o 114} 115 116// reverse-proxy: inject "X-Nishi-Level: <level>" after the request line, forward req[0..rn) to the daemon 117// (127.0.0.1:8097), relay the response (chunked, to EOF) to cfd. 118func tgw_proxy(cfd: i64, req: *u8, rn: i64, level: i64) -> i64 { 119 var rl: i64 = 0; var done: i64 = 0 120 while done == 0 { 121 if rl + 1 >= rn { done = 1 } 122 else { if req[rl] == (13 as u8) { if req[rl+1] == (10 as u8) { done = 1 } else { rl = rl + 1 } } else { rl = rl + 1 } } 123 } 124 var hascrlf: i64 = 0; if rl + 1 < rn { if req[rl] == (13 as u8) { if req[rl+1] == (10 as u8) { hascrlf = 1 } } } 125 let fwd: *u8 = sys_mmap(TGW_MAGIC_65536); var o: i64 = 0 126 if hascrlf == 1 { 127 o = tgw_reqline_noq(fwd, o, req, rl); fwd[o] = 13 as u8; o = o + 1; fwd[o] = 10 as u8; o = o + 1 128 o = tgw_cat(fwd, o, "X-Nishi-Level: " as *u8); o = tgw_itoa(fwd, o, level); fwd[o] = 13 as u8; o = o + 1; fwd[o] = 10 as u8; o = o + 1 129 var i: i64 = rl + 2; while i < rn { fwd[o] = req[i]; o = o + 1; i = i + 1 } 130 } else { 131 var i: i64 = 0; while i < rn { fwd[o] = req[i]; o = o + 1; i = i + 1 } 132 } 133 let bfd: i64 = sys_socket(AF_INET, SOCK_STREAM, 0); if bfd < 0 { return 0 - 1 } 134 let addr: *u8 = sys_mmap(16); nx_http_client_sockaddr_ipv4(addr, 127, 0, 0, 1, TGW_BACKEND_PORT) 135 if nx_connect_bounded(bfd, addr, 16, NX_CONN_DEFAULT_MS) < 0 { sys_close(bfd); return 0 - 1 } 136 sys_set_socket_timeout(bfd, 20) 137 sys_write(bfd, fwd, o) 138 let buf: *u8 = sys_mmap(TGW_MAGIC_262144); var go: i64 = 1 139 while go == 1 { let r: i64 = sys_read(bfd, buf, TGW_MAGIC_262144); if r <= 0 { go = 0 } else { sys_write(cfd, buf, r) } } 140 sys_close(bfd); return 0 141} 142 143func main(argc: i64, argv: *i64) -> i64 { 144 var port: i64 = TGW_MAGIC_18793 145 var keys: *u8 = TGW_KEYS; var store: *u8 = TGW_STORE 146 var m_cost: i64 = TGW_MAGIC_65536; var t_cost: i64 = 3; var p_cost: i64 = 4 147 if argc > 1 { port = tgw_atoi(argv[1] as *u8) } 148 if argc > 2 { keys = argv[2] as *u8 } 149 if argc > 3 { store = argv[3] as *u8 } 150 if argc > 5 { m_cost = tgw_atoi(argv[5] as *u8) } 151 if argc > 6 { t_cost = tgw_atoi(argv[6] as *u8) } 152 if argc > 7 { p_cost = tgw_atoi(argv[7] as *u8) } 153 154 let ctx: *NxAuthContext = sys_mmap(256) as *NxAuthContext 155 if olg_ctx_setup(ctx, keys, store, "nishi_site_admin" as *u8, 16, "Nishi site admin" as *u8, 16, m_cost, t_cost, p_cost) != 0 { 156 tgw_w("CTX-INIT-FAIL\n" as *u8); sys_exit(1); return 1 157 } 158 // short-TTL (4h) context: mints BRIEF stream tokens from the operator's session so the long-lived session 159 // token NEVER lands in a stream URL (no client-side history of the identity token). Same keys/realm. 160 let ctx_stream: *NxAuthContext = sys_mmap(256) as *NxAuthContext 161 olg_ctx_setup_ttl(ctx_stream, keys, store, "nishi_site_admin" as *u8, 16, "Nishi site admin" as *u8, 16, TGW_MAGIC_14400, m_cost, t_cost, p_cost) 162 let addr: *u8 = sys_mmap(16); nx_http_client_sockaddr_ipv4(addr, 127, 0, 0, 1, port) 163 let lfd: i64 = sys_socket(AF_INET, SOCK_STREAM, 0) 164 if lfd < 0 { tgw_w("SOCKET-FAIL\n" as *u8); sys_exit(1); return 1 } 165 let optv: *u8 = sys_mmap(4); optv[0] = 1 as u8; optv[1] = 0 as u8; optv[2] = 0 as u8; optv[3] = 0 as u8 166 sys_setsockopt(lfd, 1, 2, optv, 4) 167 if sys_bind(lfd, addr, 16) < 0 { tgw_w("BIND-FAIL\n" as *u8); sys_exit(1); return 1 } 168 if sys_listen(lfd, 16) < 0 { tgw_w("LISTEN-FAIL\n" as *u8); sys_exit(1); return 1 } 169 // FD_CLOEXEC on the LISTENER (debt 1785529579). Binds by RAW SYSCALL, never calls 170 // nx_http_server_listen, so the helper's CLOEXEC fix cannot reach it -- rebuilding alone changes 171 // nothing. Measured live by nx_livecloexec_gate: this pid held a listener on fd=4 with the bit CLEAR. 172 // A forked+exec'd child that inherits a listening fd holds the port FOREVER; restarting the victim 173 // can never free it. 72 = SYS_FCNTL, 2 = F_SETFD, 1 = FD_CLOEXEC. 174 __syscall(72, lfd, 2, 1, 0, 0, 0) 175 tgw_w("TORRENT-GATEWAY-UP\n" as *u8) 176 177 let req: *u8 = sys_mmap(TGW_MAGIC_16400); let scratch: *u8 = sys_mmap(TGW_MAGIC_65536) 178 let tok: *u8 = sys_mmap(512); let uid: *u8 = sys_mmap(64); let uidn: *i64 = sys_mmap(16) as *i64 179 var served: i64 = 0 180 while served < TGW_MAGIC_1000000000 { 181 let cfd: i64 = sys_accept(lfd) 182 if cfd >= 0 { 183 sys_set_socket_timeout(cfd, 15) 184 let rn: i64 = sys_read(cfd, req, TGW_MAGIC_16383) 185 if rn > 0 { 186 let now: i64 = sys_now_realtime_sec() 187 var tl: i64 = hgw_hdr_val(req, rn, "X-Nishi-Session:" as *u8, 16, tok, 512) 188 if tl <= 0 { tl = tgw_query_nsess(req, rn, tok, 512) } // <video> streams carry the session as ?nsess=<token> 189 if tgw_req_has(req, rn, " /torrent/streamtkt" as *u8, 19) == 1 { 190 var minted: i64 = 0 191 if tl > 0 { 192 let stok: *u8 = sys_mmap(512); let stokn: *i64 = sys_mmap(16) as *i64 193 if olg_refresh(ctx_stream, tok, tl, now, stok, 512, stokn) == NX_MAUTH_OK { 194 tgw_send(cfd, scratch, "200 OK" as *u8, "text/plain; charset=utf-8" as *u8, stok, stokn[0]); minted = 1 195 } 196 } 197 if minted == 0 { tgw_send(cfd, scratch, "401 Unauthorized" as *u8, "text/plain" as *u8, "no\n" as *u8, 3) } 198 } else { 199 if tl <= 0 { 200 tgw_send(cfd, scratch, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, TGW_BOOTSTRAP, tgw_slen(TGW_BOOTSTRAP)) 201 } else { 202 if olg_whoami(ctx, tok, tl, now, uid, 64, uidn) == NX_MAUTH_OK { tgw_proxy(cfd, req, rn, NX_TACCESS_OWNER) } 203 else { tgw_redirect(cfd, scratch, "/login" as *u8) } 204 } 205 } 206 } 207 sys_close(cfd) 208 } 209 served = served + 1 210 } 211 sys_close(lfd); sys_exit(0); return 0 212}