nx_torrent_daemon_gate.nx source
↩ module page · 154 lines · 10488 B
1// nx_torrent_daemon_gate.nx -- SOVEREIGN verification of the MULTI-TORRENT /torrent daemon
2// (X-TORRENT-UI-003). Drives the LIVE daemon via nx_http_client; asserts the surface, the login
3// hide-fix, multi-torrent isolation (two DISTINCT magnets -> two independent rows, per-id
4// pause/remove with no clobber), AND the browser urlencoded-form add path (bug#1 regression guard:
5// the daemon's d_form_magnet decode, which the old JSON-only gate never exercised). NO bash/curl.
6// Run: ./_offc/nx_sov_build_run.elf nx_torrent_daemon_gate (daemon must be UP via nx_torrent_up)
7// license_tier: ORIGINAL layer: gate over L6 daemon
8// module: nishi-core.torrent.daemon_gate
9// depends: nishi-core.http_client, nishi-core.syscalls
10import "nx_http_client.nx"
11
12import "nx_connect.nx" // bounded connect: a raw sys_connect hangs ~127s on a black-holed host
13import "nx_gate_verdict.nx"
14func g_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
15func g_putn(v: i64) -> i64 {
16 let bb: *u8 = sys_mmap(28); var m: i64 = v; if m < 0 { m = 0 - m; sys_write(1, "-" as *u8, 1) }
17 let t: *u8 = sys_mmap(28); var k: i64 = 0; if m == 0 { t[0] = 48 as u8; k = 1 }
18 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
19 var i: i64 = 0; while i < k { bb[i] = t[k-1-i]; i = i + 1 }
20 sys_write(1, bb, k); return 0
21}
22func g_strlen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
23func g_contains(buf: *u8, n: i64, s: *u8) -> i64 {
24 let sl: i64 = g_strlen(s); if sl == 0 { return 1 }
25 var i: i64 = 0
26 while i + sl <= n {
27 var j: i64 = 0; var ok: i64 = 1
28 while j < sl { if buf[i+j] != s[j] { ok = 0; j = sl } else { j = j + 1 } }
29 if ok == 1 { return 1 }
30 i = i + 1
31 }
32 return 0
33}
34func g_report(label: *u8, ok: i64) -> i64 {
35 if ok == 1 { g_puts(" [PASS] " as *u8) } else { g_puts(" [FAIL] " as *u8) }
36 g_puts(label); g_puts("\n" as *u8); return 0
37}
38func g_get(path: *u8, buf: *u8, cap: i64) -> i64 {
39 let addr: *u8 = sys_mmap(16); nx_http_client_sockaddr_ipv4(addr, 127, 0, 0, 1, 8097)
40 let v: *i64 = sys_mmap(16) as *i64
41 return nx_http_client_get(addr, path, g_strlen(path), "127.0.0.1" as *u8, 9, buf, cap, v)
42}
43func g_post(path: *u8, body: *u8, blen: i64, buf: *u8, cap: i64) -> i64 {
44 let fd: i64 = sys_socket(AF_INET, SOCK_STREAM, 0); if fd < 0 { return 0 - 1 }
45 let addr: *u8 = sys_mmap(16); nx_http_client_sockaddr_ipv4(addr, 127, 0, 0, 1, 8097)
46 if nx_connect_bounded(fd, addr, 16, NX_CONN_DEFAULT_MS) < 0 { sys_close(fd); return 0 - 1 }
47 sys_set_socket_timeout(fd, 8) // no-hang law: bound the read so a server-side fd leak can't wedge the gate
48 let req: *u8 = sys_mmap(8192)
49 let rl: i64 = nx_http_client_build_request_post(path, g_strlen(path), "127.0.0.1" as *u8, 9,
50 "application/json" as *u8, 16, body, blen, req)
51 sys_write(fd, req, rl)
52 let got: i64 = _drain(fd, buf, cap)
53 sys_close(fd)
54 return got
55}
56// Same as g_post but Content-Type: application/x-www-form-urlencoded -- the EXACT request the browser
57// UI sends for an add (body = magnet=<encodeURIComponent(...)>&area=...). This drives the daemon's
58// d_form_magnet decode path (bug#1), which g_post's JSON path never exercised.
59func g_post_form(path: *u8, body: *u8, blen: i64, buf: *u8, cap: i64) -> i64 {
60 let fd: i64 = sys_socket(AF_INET, SOCK_STREAM, 0); if fd < 0 { return 0 - 1 }
61 let addr: *u8 = sys_mmap(16); nx_http_client_sockaddr_ipv4(addr, 127, 0, 0, 1, 8097)
62 if nx_connect_bounded(fd, addr, 16, NX_CONN_DEFAULT_MS) < 0 { sys_close(fd); return 0 - 1 }
63 sys_set_socket_timeout(fd, 8) // no-hang law: bound the read so a server-side fd leak can't wedge the gate
64 let req: *u8 = sys_mmap(8192)
65 let rl: i64 = nx_http_client_build_request_post(path, g_strlen(path), "127.0.0.1" as *u8, 9,
66 "application/x-www-form-urlencoded" as *u8, 33, body, blen, req)
67 sys_write(fd, req, rl)
68 let got: i64 = _drain(fd, buf, cap)
69 sys_close(fd)
70 return got
71}
72
73func main() -> i64 {
74 let buf: *u8 = sys_mmap(262144)
75 var pass: i64 = 0; var tot: i64 = 0
76
77 // --- surface: the SESSION-2 redesign is one all-inline sovereign page (no external app.js, no
78 // #login element). The old gate asserted /torrent/app.js + #login[hidden]; the redesign removed both,
79 // so those assertions were stale. Assert the redesigned surface AND its sovereignty instead. ---
80 var n: i64 = g_get("/torrent" as *u8, buf, 262144)
81 var ok1: i64 = 0
82 if g_contains(buf, n, "200 OK" as *u8) == 1 { if g_contains(buf, n, "Nishi Media" as *u8) == 1 { if g_contains(buf, n, "id=chips" as *u8) == 1 { ok1 = 1 } } }
83 tot = tot + 1; if ok1 == 1 { pass = pass + 1 } g_report("GET /torrent = 200 + redesigned UI (Nishi Media + batch-add chips)" as *u8, ok1)
84
85 n = g_get("/torrent" as *u8, buf, 262144)
86 var okjs: i64 = 0
87 if g_contains(buf, n, "<script>" as *u8) == 1 { if g_contains(buf, n, "classify(" as *u8) == 1 { if g_contains(buf, n, "app.js" as *u8) == 0 { okjs = 1 } } }
88 tot = tot + 1; if okjs == 1 { pass = pass + 1 } g_report("GET /torrent = sovereign inline client (bare <script> + classify(), zero external app.js)" as *u8, okjs)
89
90 n = g_get("/torrent/api/me" as *u8, buf, 262144)
91 let okme: i64 = g_contains(buf, n, "local" as *u8)
92 tot = tot + 1; if okme == 1 { pass = pass + 1 } g_report("GET /api/me = auth-open" as *u8, okme)
93
94 // --- MULTI-TORRENT: two DISTINCT magnets ---
95 let m1: *u8 = "{\"magnet\":\"magnet:?xt=urn:btih:dd8255ecdc7ca55fb0bbf81323d87062db1f6d1c&dn=Big+Buck+Bunny&tr=udp://open.stealth.si:80/announce\"}" as *u8
96 let m2: *u8 = "{\"magnet\":\"magnet:?xt=urn:btih:08ada5a7a6183aae1e09d831df6748d566095a10&dn=Sintel&tr=udp://open.stealth.si:80/announce\"}" as *u8
97 n = g_post("/torrent/api/add" as *u8, m1, g_strlen(m1), buf, 262144)
98 let oka1: i64 = g_contains(buf, n, "Big_Buck_Bunny" as *u8)
99 tot = tot + 1; if oka1 == 1 { pass = pass + 1 } g_report("POST /add #1 (BBB) -> 200 + dn-derived name id Big_Buck_Bunny" as *u8, oka1)
100 n = g_post("/torrent/api/add" as *u8, m2, g_strlen(m2), buf, 262144)
101 let oka2: i64 = g_contains(buf, n, "Sintel" as *u8)
102 tot = tot + 1; if oka2 == 1 { pass = pass + 1 } g_report("POST /add #2 (Sintel) -> 200 + name id Sintel" as *u8, oka2)
103
104 sys_sleep_ms(2000)
105 n = g_get("/torrent/api/status" as *u8, buf, 262144)
106 var okboth: i64 = 0; if g_contains(buf, n, "Big_Buck_Bunny" as *u8) == 1 { if g_contains(buf, n, "Sintel" as *u8) == 1 { okboth = 1 } }
107 tot = tot + 1; if okboth == 1 { pass = pass + 1 } g_report("GET /status = BOTH torrents at once (no clobber)" as *u8, okboth)
108
109 g_post("/torrent/api/Big_Buck_Bunny/pause" as *u8, "" as *u8, 0, buf, 262144)
110 n = g_get("/torrent/api/status" as *u8, buf, 262144)
111 var okp: i64 = 0; if g_contains(buf, n, "paused" as *u8) == 1 { if g_contains(buf, n, "Sintel" as *u8) == 1 { okp = 1 } }
112 tot = tot + 1; if okp == 1 { pass = pass + 1 } g_report("pause #1 -> Big_Buck_Bunny paused while Sintel keeps running" as *u8, okp)
113
114 g_post("/torrent/api/Big_Buck_Bunny/remove" as *u8, "" as *u8, 0, buf, 262144)
115 n = g_get("/torrent/api/status" as *u8, buf, 262144)
116 var okr1: i64 = 0; if g_contains(buf, n, "Sintel" as *u8) == 1 { if g_contains(buf, n, "Big_Buck_Bunny" as *u8) == 0 { okr1 = 1 } }
117 tot = tot + 1; if okr1 == 1 { pass = pass + 1 } g_report("remove #1 -> only Sintel remains (per-id isolation)" as *u8, okr1)
118
119 g_post("/torrent/api/Sintel/remove" as *u8, "" as *u8, 0, buf, 262144)
120 n = g_get("/torrent/api/status" as *u8, buf, 262144)
121 var okr2: i64 = 0; if g_contains(buf, n, "Sintel" as *u8) == 0 { if g_contains(buf, n, "Big_Buck_Bunny" as *u8) == 0 { okr2 = 1 } }
122 tot = tot + 1; if okr2 == 1 { pass = pass + 1 } g_report("remove #2 -> both test torrents gone (robust to other torrents)" as *u8, okr2)
123
124 // --- BUG#1 REGRESSION GUARD: the BROWSER add path = urlencoded FORM POST. The UI sends
125 // Content-Type: application/x-www-form-urlencoded, body magnet=<encodeURIComponent(magnet)>&area=...
126 // The daemon decodes it in d_form_magnet (keeps the magnet's internal %26, stops at the real & before
127 // area=). bug#1 was that the daemon only scanned for a literal "magnet:" and never url-decoded, so the
128 // browser add 400'd while THIS gate (JSON-only) stayed GREEN -- the classic gate-vs-browser blind spot.
129 // The encoded magnet carries an internal %26 (before dn=) and a real & (before area=) -> exercises both
130 // halves of the d_form_magnet fix. Success = a real id was ASSIGNED (NOT the "no magnet" error bug#1
131 // produced); the id is the dn-name (fresh) or a dedup'd name -- either way the urlencoded decode worked.
132 let fbody: *u8 = "magnet=magnet%3A%3Fxt%3Durn%3Abtih%3Add8255ecdc7ca55fb0bbf81323d87062db1f6d1c%26dn%3DBig_Buck_Bunny&area=media" as *u8
133 n = g_post_form("/torrent/api/add" as *u8, fbody, g_strlen(fbody), buf, 262144)
134 var okform: i64 = 0
135 if g_contains(buf, n, "\"id\":\"" as *u8) == 1 { if g_contains(buf, n, "no magnet" as *u8) == 0 { if g_contains(buf, n, "error" as *u8) == 0 { okform = 1 } } }
136 tot = tot + 1; if okform == 1 { pass = pass + 1 } g_report("POST /add urlencoded FORM (browser path / d_form_magnet) ACCEPTED -- bug#1 guard" as *u8, okform)
137
138 // cleanup: dn=Big_Buck_Bunny -> deterministic id, so remove by name; assert it's gone -- leave no download.
139 g_post("/torrent/api/Big_Buck_Bunny/remove" as *u8, "" as *u8, 0, buf, 262144)
140 n = g_get("/torrent/api/status" as *u8, buf, 262144)
141 var okfc: i64 = 0; if g_contains(buf, n, "Big_Buck_Bunny" as *u8) == 0 { okfc = 1 }
142 tot = tot + 1; if okfc == 1 { pass = pass + 1 } g_report("form-add cleaned up (removed by id, gone from status)" as *u8, okfc)
143
144 g_puts("TORRENT-MULTI-GATE authored=organ pass=" as *u8); g_putn(pass); g_puts("/" as *u8); g_putn(tot)
145 // MIGRATED onto nx_gate_verdict by nx_gate_dry_apply (D001, minimal form): every check
146 // row above is untouched, so the PASS/FAIL vector cannot change; only the hand-rolled
147 // verdict emission is replaced by the ONE shared base class. Proven by nx_gate_migrate verify.
148 let ctr__dry: *i64 = gv_ctr()
149 ctr__dry[0] = pass
150 ctr__dry[1] = tot
151 let rc__dry: i64 = gv_verdict("TORRENT-DAEMON-GATE" as *u8, ctr__dry, "teeth unchanged; verdict emission migrated onto the shared base class" as *u8)
152 sys_exit(rc__dry)
153 return rc__dry
154}