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}