code wiki / _hdl_build / nx_torrent_gateway.nx
nx_torrent_gateway.nx source
↩ module page · 206 lines · 13995 B
1// nx_torrent_gateway.nx -- R3c: the FRONT GATEWAY that puts /torrent on the hub, GATED. Mirrors the proven
2// nx_gallery_gateway: OPAQUE login (full RFC-9807 aPAKE, no plaintext at rest) + cookie/SW token transport,
3// then per request it resolves the viewer's LEVEL via nx_torrent_authn (tau_level) and reverse-proxies to the
4// lean torrent daemon (127.0.0.1:bport) -- INJECTING the level as the trusted internal header X-Nishi-Level.
5// SECURITY KEYSTONE: the backend request is built FRESH (method + path + OUR X-Nishi-Level + Range + body),
6// so a client-supplied X-Nishi-Level is DROPPED -- the level is authoritative from the validated session, never
7// from client input. No valid session on a gated path -> 401 (no content). The daemon binds localhost behind
8// this gateway (Cardinal 12). The realm keys/store + roles registry are the HUB realm (operator-provisioned).
9// license_tier: ORIGINAL
10import "nx_torrent_authn.nx" // tau_level / tau_token / tau_find + olg_* + NxAuthContext + NX_MAUTH_OK (transitively)
11import "nx_opaque_login.nx"
12import "nx_connect.nx" // bounded connect: a raw sys_connect hangs ~127s on a black-holed host
13import "nx_syscalls.nx"
14const TGW_MAGIC_1024: i64 = 1024
15const TGW_MAGIC_262144: i64 = 262144
16const TGW_MAGIC_65536: i64 = 65536
17const TGW_MAGIC_131072: i64 = 131072
18const TGW_MAGIC_2048: i64 = 2048
19const TGW_MAGIC_1280: i64 = 1280
20
21func tg_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 {i=i+1} else { if c>57 {i=i+1} else { v=v*10+(c-48); i=i+1 } } } return v }
22func tg_cat(dst: *u8, o: i64, s: *u8) -> i64 { var i: i64=0; while s[i]!=(0 as u8){ dst[o+i]=s[i]; i=i+1 } return o+i }
23func tg_catb(dst: *u8, o: i64, s: *u8, n: i64) -> i64 { var i: i64=0; while i<n { dst[o+i]=s[i]; i=i+1 } return o+i }
24func tg_itoa(dst: *u8, o: i64, v: i64) -> i64 {
25 if v==0 { dst[o]=48 as u8; return o+1 }
26 var m: i64=v; if m<0{m=0-m}
27 let t: *u8=sys_mmap(32); var k: i64=0
28 while m>0 { t[k]=(48+(m%10)) as u8; m=m/10; k=k+1 }
29 var j: i64=0; while j<k { dst[o+j]=t[k-1-j]; j=j+1 }
30 return o+k
31}
32// request-target path (between the first two spaces) -> out (NUL-term). returns len.
33func tg_reqpath(req: *u8, n: i64, out: *u8, cap: i64) -> i64 {
34 var s1: i64 = 0-1; var i: i64 = 0
35 while i < n { if req[i]==(32 as u8) { s1=i; i=n } else { i=i+1 } }
36 if s1 < 0 { out[0]=0 as u8; return 0 }
37 var p: i64 = s1+1; var o: i64 = 0
38 while p < n { let c: u8 = req[p]; if c==(32 as u8){p=n} else { if o<cap-1 { out[o]=c; o=o+1 } p=p+1 } }
39 out[o]=0 as u8; return o
40}
41// the HTTP method (up to the first space) -> out. returns len.
42func tg_method(req: *u8, n: i64, out: *u8, cap: i64) -> i64 {
43 var o: i64 = 0
44 while o < n { let c: u8 = req[o]; if c==(32 as u8){ out[o]=0 as u8; return o } if o<cap-1 { out[o]=c } o=o+1 }
45 out[0]=0 as u8; return 0
46}
47
48// THE SECURITY KEYSTONE: build the backend request FRESH. method + path + the AUTHORITATIVE X-Nishi-Level +
49// (forwarded) Range + body. Because it's built from scratch, any client-supplied X-Nishi-Level is DROPPED.
50func tg_backend_req(method: *u8, mlen: i64, path: *u8, plen: i64, level: i64, rng: *u8, rngn: i64, body: *u8, blen: i64, out: *u8) -> i64 {
51 var o: i64 = 0
52 o = tg_catb(out, o, method, mlen); out[o]=32 as u8; o=o+1
53 o = tg_catb(out, o, path, plen)
54 o = tg_cat(out, o, " HTTP/1.1\r\nHost: 127.0.0.1\r\nAccept: */*\r\nConnection: close\r\n" as *u8)
55 o = tg_cat(out, o, "X-Nishi-Level: " as *u8); o = tg_itoa(out, o, level); o = tg_cat(out, o, "\r\n" as *u8)
56 if rngn > 0 { o = tg_cat(out, o, "Range: " as *u8); o = tg_catb(out, o, rng, rngn); o = tg_cat(out, o, "\r\n" as *u8) }
57 if blen > 0 {
58 o = tg_cat(out, o, "Content-Type: application/x-www-form-urlencoded\r\nContent-Length: " as *u8)
59 o = tg_itoa(out, o, blen); o = tg_cat(out, o, "\r\n\r\n" as *u8)
60 o = tg_catb(out, o, body, blen)
61 } else {
62 o = tg_cat(out, o, "\r\n" as *u8)
63 }
64 return o
65}
66
67// gate decision for a path at a level. A gated /torrent/* path needs level>=1 (FAMILY); level 0 -> DENY (401).
68// The auth/login/sw bootstrap paths are PUBLIC (you must reach them to log in). returns 1 ALLOW, 0 DENY.
69func tg_path_public(path: *u8, plen: i64) -> i64 {
70 if tau_find(path, plen, "/torrent/auth/" as *u8, 14) == 0 { return 1 }
71 if tau_find(path, plen, "/torrent/login" as *u8, 14) == 0 { return 1 }
72 if tau_find(path, plen, "/torrent/sw.js" as *u8, 14) == 0 { return 1 }
73 return 0
74}
75func tg_gate_decision(path: *u8, plen: i64, level: i64) -> i64 {
76 if tg_path_public(path, plen) == 1 { return 1 } // login bootstrap is reachable
77 if level >= 1 { return 1 } // any logged-in member+ may reach the gated app (area enforced downstream)
78 return 0 // no session -> DENY (401)
79}
80
81// send a small response (status, ctype, body) + close.
82func tg_send(cfd: i64, status: *u8, ctype: *u8, body: *u8, blen: i64) -> i64 {
83 let h: *u8 = sys_mmap(TGW_MAGIC_1024); var o: i64 = 0
84 o = tg_cat(h, o, "HTTP/1.1 " as *u8); o = tg_cat(h, o, status)
85 o = tg_cat(h, o, "\r\nContent-Type: " as *u8); o = tg_cat(h, o, ctype)
86 o = tg_cat(h, o, "\r\nContent-Length: " as *u8); o = tg_itoa(h, o, blen)
87 o = tg_cat(h, o, "\r\nConnection: close\r\n\r\n" as *u8)
88 sys_write(cfd, h, o)
89 if blen > 0 { sys_write(cfd, body, blen) }
90 sys_close(cfd)
91 return 0
92}
93func tg_slen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
94
95// reverse-proxy: connect 127.0.0.1:bport, write the fresh backend request, relay the response to cfd.
96func tg_proxy(cfd: i64, bport: i64, breq: *u8, breqn: i64) -> i64 {
97 let fd: i64 = sys_socket(2, 1, 0); if fd < 0 { return 0-1 }
98 sys_set_socket_timeout(fd, 20)
99 let a: *u8 = sys_mmap(16)
100 a[0]=2 as u8; a[1]=0 as u8; a[2]=((bport>>8)&0xff) as u8; a[3]=(bport&0xff) as u8
101 a[4]=127 as u8; a[5]=0 as u8; a[6]=0 as u8; a[7]=1 as u8
102 var zi: i64=8; while zi<16 { a[zi]=0 as u8; zi=zi+1 }
103 if nx_connect_bounded(fd, a, 16, NX_CONN_DEFAULT_MS) != 0 { sys_close(fd); return 0-2 }
104 sys_write(fd, breq, breqn)
105 let buf: *u8 = sys_mmap(TGW_MAGIC_262144); var total: i64 = 0; var go: i64 = 1
106 while go==1 { let r: i64 = sys_read(fd, buf, TGW_MAGIC_262144); if r<=0 {go=0} else { sys_write(cfd, buf, r); total=total+r } }
107 sys_close(fd)
108 return total
109}
110
111const TGW_LOGIN_HTML: *u8 = "<!doctype html><html><head><meta charset=utf-8><meta name=viewport content=\"width=device-width,initial-scale=1\"><title>Nishi Torrent</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}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;word-break:break-all;min-height:1.2em}</style></head><body><h1>Nishi Torrent — sign in</h1><div id=m>Log in to manage downloads.</div><input id=h placeholder=handle autocomplete=username><input id=p type=password placeholder=passphrase autocomplete=current-password><button onclick=login()>Login</button><script>function $(i){return document.getElementById(i)}function M(t){$('m').textContent=t}async function login(){M('Logging in...');try{var r=await fetch('/torrent/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}M('Logged in. Opening...');location.href='/torrent/'}catch(e){M('error: '+e)}}</script></body></html>" as *u8
112
113func main(argc: i64, argv: *i64) -> i64 {
114 if argc < 8 {
115 sys_write(1, "usage: nx_torrent_gateway <port> <keys> <store> <roles> <budget> <backend_port> <realm> [allow_register] [m] [t] [p]\n" as *u8, 113)
116 sys_exit(2); return 2
117 }
118 let port: i64 = tg_atoi(argv[1] as *u8)
119 let keys_path: *u8 = argv[2] as *u8
120 let store_path: *u8 = argv[3] as *u8
121 let roles_path: *u8 = argv[4] as *u8
122 let budget: i64 = tg_atoi(argv[5] as *u8)
123 let bport: i64 = tg_atoi(argv[6] as *u8)
124 let realm: *u8 = argv[7] as *u8; let realm_n: i64 = tg_slen(realm)
125 var allow_reg: i64 = 0
126 if argc > 8 { allow_reg = tg_atoi(argv[8] as *u8) }
127 var m_cost: i64 = TGW_MAGIC_65536
128 if argc > 9 { m_cost = tg_atoi(argv[9] as *u8) }
129 var t_cost: i64 = 3
130 if argc > 10 { t_cost = tg_atoi(argv[10] as *u8) }
131 var p_cost: i64 = 4
132 if argc > 11 { p_cost = tg_atoi(argv[11] as *u8) }
133
134 let ctx: *NxAuthContext = sys_mmap(256) as *NxAuthContext
135 if olg_ctx_setup(ctx, keys_path, store_path, realm, realm_n, realm, realm_n, m_cost, t_cost, p_cost) != 0 {
136 sys_write(1, "CTX-INIT-FAIL\n" as *u8, 14); sys_exit(1); return 1
137 }
138 let addr: *u8 = sys_mmap(16)
139 addr[0]=2 as u8; addr[1]=0 as u8; addr[2]=((port>>8)&0xff) as u8; addr[3]=(port&0xff) as u8
140 var zi: i64=4; while zi<16 { addr[zi]=0 as u8; zi=zi+1 }
141 let lfd: i64 = sys_socket(2, 1, 0)
142 if lfd < 0 { sys_write(1, "SOCKET-FAIL\n" as *u8, 12); sys_exit(1); return 1 }
143 let optv: *u8 = sys_mmap(4); optv[0]=1 as u8; sys_setsockopt(lfd, 1, 2, optv, 4)
144 if sys_bind(lfd, addr, 16) < 0 { sys_write(1, "BIND-FAIL\n" as *u8, 10); sys_exit(1); return 1 }
145 if sys_listen(lfd, 16) < 0 { sys_write(1, "LISTEN-FAIL\n" as *u8, 12); sys_exit(1); return 1 }
146 sys_write(1, "TORRENT-GATEWAY-UP\n" as *u8, 19)
147
148 let st: *i64 = sys_mmap(16) as *i64
149 var served: i64 = 0
150 while served < budget {
151 let cfd: i64 = sys_accept(lfd)
152 if cfd >= 0 {
153 let pid: i64 = sys_fork()
154 if pid == 0 {
155 sys_close(lfd)
156 sys_set_socket_timeout(cfd, 15)
157 let req: *u8 = sys_mmap(TGW_MAGIC_131072)
158 var n: i64 = 0; var he: i64 = 0-1
159 while he < 0 { if n >= TGW_MAGIC_131072 { he = n } else { let r: i64 = sys_read(cfd, ((req as i64)+n) as *u8, TGW_MAGIC_131072-n); if r<=0 { he = n } else { n = n + r; he = tau_find(req, n, "\r\n\r\n" as *u8, 4) } } }
160 let path: *u8 = sys_mmap(TGW_MAGIC_2048); let plen: i64 = tg_reqpath(req, n, path, TGW_MAGIC_2048)
161 let meth: *u8 = sys_mmap(16); let mlen: i64 = tg_method(req, n, meth, 16)
162 // login: OPAQUE -> token + Set-Cookie ngs.
163 if tau_find(path, plen, "/torrent/auth/login" as *u8, 19) == 0 {
164 let hb: *u8 = sys_mmap(128); let pb: *u8 = sys_mmap(256)
165 var hN: i64 = tau_hdr_val(req, n, "handle=" as *u8, 7, hb, 128) // urlencoded body: handle=..&pw=..
166 var pN: i64 = tau_hdr_val(req, n, "pw=" as *u8, 3, pb, 256)
167 var hh: i64 = 0; while hh < hN { if hb[hh]==(38 as u8) { hN = hh } else { hh = hh + 1 } } // strip "&pw=.." from handle
168 var pp: i64 = 0; while pp < pN { if pb[pp]==(38 as u8) { pN = pp } else { pp = pp + 1 } } // strip a trailing field from pw
169 let b64: *u8 = sys_mmap(512); let b64n: *i64 = sys_mmap(16) as *i64; b64n[0]=0
170 if olg_login(ctx, hb, hN, pb, pN, b64, 512, b64n) == NX_MAUTH_OK {
171 b64[b64n[0]] = 0 as u8
172 let rb: *u8 = sys_mmap(TGW_MAGIC_1024); var ro: i64 = tg_cat(rb, 0, "{\"token\":\"" as *u8); ro = tg_catb(rb, ro, b64, b64n[0]); ro = tg_cat(rb, ro, "\"}" as *u8)
173 let hh2: *u8 = sys_mmap(TGW_MAGIC_1280); var ho: i64 = 0
174 ho = tg_cat(hh2, ho, "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nSet-Cookie: ngs=" as *u8); ho = tg_catb(hh2, ho, b64, b64n[0])
175 ho = tg_cat(hh2, ho, "; HttpOnly; Secure; SameSite=Strict; Path=/torrent; Max-Age=900\r\nContent-Length: " as *u8); ho = tg_itoa(hh2, ho, ro)
176 ho = tg_cat(hh2, ho, "\r\nConnection: close\r\n\r\n" as *u8); ho = tg_catb(hh2, ho, rb, ro)
177 sys_write(cfd, hh2, ho); sys_close(cfd)
178 } else {
179 tg_send(cfd, "401 Unauthorized" as *u8, "application/json" as *u8, "{\"error\":\"login failed\"}" as *u8, 24)
180 }
181 sys_exit(0)
182 }
183 if tau_find(path, plen, "/torrent/login" as *u8, 14) == 0 {
184 tg_send(cfd, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, TGW_LOGIN_HTML, tg_slen(TGW_LOGIN_HTML)); sys_exit(0)
185 }
186 // everything else under /torrent: resolve level, gate, then proxy with X-Nishi-Level injected.
187 let lv: i64 = tau_level_now(req, n, keys_path, store_path, roles_path, realm, realm_n)
188 if tg_gate_decision(path, plen, lv) == 0 {
189 tg_send(cfd, "401 Unauthorized" as *u8, "text/plain" as *u8, "login required\n" as *u8, 15); sys_exit(0)
190 }
191 let rng: *u8 = sys_mmap(256); let rngn: i64 = tau_hdr_val(req, n, "\r\nRange:" as *u8, 8, rng, 256)
192 var bN: i64 = 0; let bo2: i64 = tau_find(req, n, "\r\n\r\n" as *u8, 4); var bodyp: *u8 = req
193 if bo2 >= 0 { bodyp = ((req as i64) + bo2 + 4) as *u8; bN = n - (bo2 + 4) }
194 let breq: *u8 = sys_mmap(TGW_MAGIC_131072); let breqn: i64 = tg_backend_req(meth, mlen, path, plen, lv, rng, rngn, bodyp, bN, breq)
195 tg_proxy(cfd, bport, breq, breqn)
196 sys_close(cfd)
197 sys_exit(0)
198 }
199 sys_close(cfd)
200 var reap: i64 = 1
201 while reap == 1 { let w: i64 = sys_wait4(0-1, st, WNOHANG); if w <= 0 { reap = 0 } }
202 }
203 served = served + 1
204 }
205 return 0
206}