nx_media_jsexec.nx source
↩ module page · 136 lines · 6879 B
1// nx_media_jsexec.nx -- JS-EXECUTION media harvester. Runs a page's JavaScript on the sovereign engine
2// and collects the media URLs its code CONSTRUCTS AT RUNTIME (via functions, template literals, string
3// concatenation, class methods) and hands to fetch()/XHR. These are exactly the URLs a STATIC byte/urlfold
4// scan MISSES -- e.g. `fetch(cdn(id)+'/master.m3u8')` where the path is computed, not a literal.
5//
6// SAFE BY CONSTRUCTION for "don't get blacklisted / don't hang on a hostile page":
7// * Fetches PEND -- the engine records the URL and returns a pending promise; it makes NO network call.
8// So running the page cannot hit a rate limit, cannot be fingerprinted by request pattern, cannot leak.
9// * We read the pending URLs and STOP -- we do NOT service them (no fetch storm, no recursive crawl here).
10// * ⚠ REMAINING SAFETY (documented, not yet wired): the interpreter has NO instruction/loop budget, so a
11// hostile `while(true){}` in page JS would hang. LIVE-CRAWLER wiring is BLOCKED on a fuel budget in the
12// eval loop (a coordinated shared-core change -- see coordination.tsv; do NOT wire live until it lands).
13// This module + its gate run only WELL-BEHAVED input, so they are safe today.
14import "nx_syscalls.nx"
15import "nx_js_eval.nx"
16const K_MAGIC_2048: i64 = 2048
17const K_MAGIC_4096: i64 = 4096
18
19// substring search: 1 if `needle` (len nl) occurs anywhere in hay[0..hl), else 0.
20func mjx_contains(hay: *u8, hl: i64, needle: *u8, nl: i64) -> i64 {
21 if nl == 0 { return 1 }
22 if nl > hl { return 0 }
23 var i: i64 = 0
24 let last: i64 = hl - nl
25 while i <= last {
26 var j: i64 = 0
27 var m: i64 = 1
28 while j < nl { if (hay[i + j] & 0xff) != (needle[j] & 0xff) { m = 0; j = nl } else { j = j + 1 } }
29 if m == 1 { return 1 }
30 i = i + 1
31 }
32 return 0
33}
34
35// does this URL look like a MEDIA resource? substring match on known extensions/manifest markers so a
36// trailing ?query / #frag doesn't defeat it. Covers HLS/DASH manifests, video/audio segments, images.
37func mjx_is_media_url(url: *u8, ul: i64) -> i64 {
38 if mjx_contains(url, ul, ".m3u8" as *u8, 5) == 1 { return 1 } // HLS manifest
39 if mjx_contains(url, ul, ".mpd" as *u8, 4) == 1 { return 1 } // DASH manifest
40 if mjx_contains(url, ul, ".mp4" as *u8, 4) == 1 { return 1 }
41 if mjx_contains(url, ul, ".m4s" as *u8, 4) == 1 { return 1 } // fMP4 segment
42 if mjx_contains(url, ul, ".ts" as *u8, 3) == 1 { return 1 } // MPEG-TS segment
43 if mjx_contains(url, ul, ".webm" as *u8, 5) == 1 { return 1 }
44 if mjx_contains(url, ul, ".mkv" as *u8, 4) == 1 { return 1 }
45 if mjx_contains(url, ul, ".mov" as *u8, 4) == 1 { return 1 }
46 if mjx_contains(url, ul, ".mp3" as *u8, 4) == 1 { return 1 }
47 if mjx_contains(url, ul, ".m4a" as *u8, 4) == 1 { return 1 }
48 if mjx_contains(url, ul, ".aac" as *u8, 4) == 1 { return 1 }
49 if mjx_contains(url, ul, ".jpg" as *u8, 4) == 1 { return 1 }
50 if mjx_contains(url, ul, ".jpeg" as *u8, 5) == 1 { return 1 }
51 if mjx_contains(url, ul, ".png" as *u8, 4) == 1 { return 1 }
52 if mjx_contains(url, ul, ".gif" as *u8, 4) == 1 { return 1 }
53 if mjx_contains(url, ul, ".webp" as *u8, 5) == 1 { return 1 }
54 if mjx_contains(url, ul, ".avif" as *u8, 5) == 1 { return 1 }
55 return 0
56}
57
58// Run page JS `src` and COLLECT the media URLs its code hands to fetch(). Writes them newline-separated
59// (each NUL-safe, '\n' delimited) into `outbuf` (cap bytes); returns the COUNT of media URLs found.
60// De-dups exact repeats. `outbox` (>=2 i64) receives [ctx, genv] so a caller can inspect further.
61func mjx_harvest_js(src: *u8, srclen: i64, outbuf: *u8, cap: i64, outbox: *i64) -> i64 {
62 outbuf[0] = 0 as u8
63 let rc: i64 = js_run_source_keep(src, srclen, outbox)
64 if rc != 0 { return 0 } // parse error -> genv is null; do NOT deref
65 let genv: *i64 = (outbox[1]) as *i64
66 if (genv as i64) == 0 { return 0 }
67 let ubuf: *u8 = sys_mmap(K_MAGIC_2048)
68 var found: i64 = 0
69 var wp: i64 = 0
70 // Enumerate pending entries by index: js_pending_url returns 0 once `i` is past the end (its own
71 // range check is the terminator). Hard-capped at 4096 so a bad count can never spin -- kept as
72 // defense-in-depth. (js_pending_total's inline-cast miscompile that first bit us here is now FIXED
73 // in nx_js_eval, two-step cast; this terminator loop doesn't depend on it either way.)
74 var i: i64 = 0
75 var go: i64 = 1
76 while go == 1 {
77 if i >= K_MAGIC_4096 { go = 0 }
78 else {
79 let ul: i64 = js_pending_url(genv, i, ubuf, K_MAGIC_2048)
80 if ul == 0 { go = 0 }
81 else {
82 if mjx_is_media_url(ubuf, ul) == 1 {
83 if mjx_contains(outbuf, wp, ubuf, ul) == 0 { // de-dup exact repeats
84 if (wp + ul + 1) < cap {
85 var k: i64 = 0
86 while k < ul { outbuf[wp] = ubuf[k]; wp = wp + 1; k = k + 1 }
87 outbuf[wp] = 10 as u8; wp = wp + 1 // '\n'
88 found = found + 1
89 }
90 }
91 }
92 i = i + 1
93 }
94 }
95 }
96 outbuf[wp] = 0 as u8
97 return found
98}
99
100// mjx_harvest_js + DOM: identical harvest, but seeds the page HTML as the DOM (js_run_source_keep_doc) so page
101// JS that reads document.getElementById/querySelector (e.g. a player plugin reading a data-* stream token off
102// its container element) resolves the real elements FIRST -- then the stream URL it constructs is captured.
103func mjx_harvest_js_dom(src: *u8, srclen: i64, dochtml: *u8, doclen: i64, outbuf: *u8, cap: i64, outbox: *i64) -> i64 {
104 outbuf[0] = 0 as u8
105 let rc: i64 = js_run_source_keep_doc(src, srclen, dochtml, doclen, outbox)
106 if rc != 0 { return 0 }
107 let genv: *i64 = (outbox[1]) as *i64
108 if (genv as i64) == 0 { return 0 }
109 let ubuf: *u8 = sys_mmap(K_MAGIC_2048)
110 var found: i64 = 0
111 var wp: i64 = 0
112 var i: i64 = 0
113 var go: i64 = 1
114 while go == 1 {
115 if i >= K_MAGIC_4096 { go = 0 }
116 else {
117 let ul: i64 = js_pending_url(genv, i, ubuf, K_MAGIC_2048)
118 if ul == 0 { go = 0 }
119 else {
120 if mjx_is_media_url(ubuf, ul) == 1 {
121 if mjx_contains(outbuf, wp, ubuf, ul) == 0 {
122 if (wp + ul + 1) < cap {
123 var k: i64 = 0
124 while k < ul { outbuf[wp] = ubuf[k]; wp = wp + 1; k = k + 1 }
125 outbuf[wp] = 10 as u8; wp = wp + 1
126 found = found + 1
127 }
128 }
129 }
130 i = i + 1
131 }
132 }
133 }
134 outbuf[wp] = 0 as u8
135 return found
136}