fix(scan): run deep probe/scraper in MAIN world; reject route-template "manifests"
CI / test (pull_request) Successful in 5m48s

The video-detection pipeline was finding nothing on pages that have a bare
<video> element. Two root causes:

1. world: 'MAIN' placement. The deep probe + scraper are injected with
   chrome.scripting.executeScript. Their code (and its own comments) requires
   the MAIN world to see page globals — window.Hls, per-<video> Hls instances,
   video._goon_hls_manifest, and window.__goon_plugin_data (written by the
   main-world interceptors). The calls were missing it, so every main-world
   strategy (A/A2/E/H) found 0. It was also briefly placed inside `target`,
   which chrome.scripting rejects ("Unexpected property: 'world'"); `world` is
   a sibling of `target` on ScriptInjection. Restore it as a sibling of target.

2. fake "manifest" classification. Strategy H was reporting 10 nav links /
   server-side route TEMPLATES as manifests — e.g. /playlists/,
   /playlists/liked/, /playlist/%1%/video/%2%/, /members/%ID%/playlists/ —
   because the old /playlist|/manifest substring check matches nav menus and
   leaves %1%/%ID%/%DIR% template placeholders intact. Those then became the
   stored "video" entries, drowning out the real element.

   isManifestUrl and isValidManifestUrl now reject:
     - dynamic route templates (%NAME% or {name} tokens), using a negative
       lookahead so genuine %-encoded tokens like %2B/%2F are preserved
       (the invariant the codebase explicitly protects),
     - plural collection paths (/playlists, /manifests),
     - trailing-slash directory listings.

   The 'World: ISOLATED' probe log was also misleading (typeof chrome is not a
   world discriminator — the probe provably runs in MAIN); replaced it.

Adds a regression test (hls_qa) reproducing thisvid's pageContext.langUrls
scenario: all 10 templates are rejected while a real .m3u8 carrying a %2B token
is still detected. 90/90 tests pass; 0 lint errors; build OK.
This commit is contained in:
kfj001 committed 2026-08-25 20:27:14 -07:00
1 parent e3b904783f
commit 7139c5ea6f
2 files changed
+126 -6

No files matched your search

+94
View File
@@ -1043,6 +1043,33 @@ describe('HLS QA: iframe and multi-frame scenarios', () => {
} }
}); });
it('deep probe + scrape must run in the MAIN world (see page globals)', () => {
// The deep probe and scraper read page globals (window.Hls, player
// instances, video._goon_hls_manifest) and window.__goon_plugin_data,
// which the main-world interceptors write. The isolated world cannot see
// these, so a missing `world: 'MAIN'` makes every main-world strategy find
// 0 manifests — the "detects nothing on pages with just <video> elements"
// regression. Guard the exact executeScript calls that run each function.
const indexSource = fs.readFileSync(
path.join(__dirname, '../src/viewer/index.js'),
'utf-8'
);
// Capture each call's target + func block (world: 'MAIN' lives on the
// target: line, which precedes func:).
const probeMatch = indexSource.match(/executeScript\(\{[\s\S]*?func: m3u8DeepProbeFn/);
if (!probeMatch) throw new Error('deep probe executeScript call not found');
if (!probeMatch[0].includes("world: 'MAIN'")) {
throw new Error('deep probe (m3u8DeepProbeFn) must use world: "MAIN" to see page globals');
}
const scrapeMatch = indexSource.match(/executeScript\(\{[\s\S]*?func: scrapeFn/);
if (!scrapeMatch) throw new Error('scrape executeScript call not found');
if (!scrapeMatch[0].includes("world: 'MAIN'")) {
throw new Error('scrape (scrapeFn) must use world: "MAIN" to see page globals');
}
});
it('should inject interceptors into all frames (content_scripts.all_frames)', () => { it('should inject interceptors into all frames (content_scripts.all_frames)', () => {
const manifestSource = fs.readFileSync( const manifestSource = fs.readFileSync(
path.join(__dirname, '../manifest.json'), path.join(__dirname, '../manifest.json'),
@@ -1214,3 +1241,70 @@ describe('HLS QA: Edge cases', () => {
} }
}); });
}); });
/* ========================================================================== */
/* Test: deep probe must not report route-template / nav "manifests" */
/* ========================================================================== */
describe('HLS QA: route-template / nav URL rejection', () => {
const probeMatch = indexSource.match(/function m3u8DeepProbeFn\(\)[\s\S]*?^}/m);
if (!probeMatch) throw new Error('Could not find m3u8DeepProbeFn in index.js');
const probeFnSource = probeMatch[0];
function runProbeWin(url, globals) {
const dom = new JSDOM(
'<!DOCTYPE html><html><body><video></video></body></html>',
{ url, runScripts: 'dangerously' });
const win = dom.window;
for (const [k, v] of Object.entries(globals)) win[k] = v;
const result = win.eval(`(${probeFnSource})();`);
if (!Array.isArray(result)) throw new Error('m3u8DeepProbeFn did not return an array');
return result;
}
// Reproduces thisvid.com: pageContext.langUrls holds server-side route
// TEMPLATES / nav links (e.g. /playlists/, /playlist/%1%/video/%2%/) that the
// old /playlist|/manifest substring check misclassified as "manifests".
const TEMPLATE_URLS = [
'https://thisvid.com/playlists/',
'https://thisvid.com/playlists/liked/',
'https://thisvid.com/playlists/popular/',
'https://thisvid.com/playlists/commented/',
'https://thisvid.com/playlists/updated/',
'https://thisvid.com/playlists/largest/',
'https://thisvid.com/playlist/%1%/video/%2%/',
'https://thisvid.com/playlists/?category=%DIR%',
'https://thisvid.com/members/%ID%/playlists/',
'https://thisvid.com/my/playlists/%ID%/',
];
it('route templates / nav links are NOT reported as manifests', () => {
const result = runProbeWin('https://thisvid.com/videos/redneck/', {
pageContext: { langUrls: TEMPLATE_URLS },
kvsplayer: { config: { langUrls: TEMPLATE_URLS } },
__goon_plugin_data: { manifests: [], sources: [] },
});
const urls = result.map(m => m.url);
const leaked = TEMPLATE_URLS.filter(u => urls.includes(u));
if (leaked.length > 0) {
throw new Error(`template/nav URLs leaked as manifests: ${leaked.join(', ')}`);
}
});
it('a real .m3u8 with a %2B token is still found (no over-rejection)', () => {
// %2B is a valid percent-encoded token, not a placeholder — it must survive.
const real = 'https://cdn.example.com/live/master.m3u8?h=a%2Bb%2Fc&e=1234';
const result = runProbeWin('https://thisvid.com/videos/redneck/', {
hls: { url: real },
pageContext: { langUrls: TEMPLATE_URLS },
});
if (!result.some(m => m.url === real)) {
throw new Error(`real manifest with %2B token was dropped: ${real} — got: ${result.map(m => m.url).join(', ')}`);
}
// And the templates still must not leak in the same run.
const leaked = TEMPLATE_URLS.filter(u => result.some(m => m.url === u));
if (leaked.length > 0) {
throw new Error(`templates leaked alongside real manifest: ${leaked.join(', ')}`);
}
});
});
+32 -6
View File
@@ -122,7 +122,11 @@ function m3u8DeepProbeFn() {
function probeLog(...args) { function probeLog(...args) {
try { console.log('[goon-probe]', ...args); } catch(e) {} try { console.log('[goon-probe]', ...args); } catch(e) {}
} }
probeLog('Deep probe started on:', window.location.href, 'World:', typeof chrome !== 'undefined' ? 'ISOLATED' : 'MAIN'); // NOTE: the probe is injected with chrome.scripting's `world: 'MAIN'`, so it
// runs in the page's main world and can see page globals. `typeof chrome` is
// NOT a reliable world discriminator (extensions expose `chrome` in the
// isolated world too), so don't infer it that way.
probeLog('Deep probe started on:', window.location.href, '(main world)');
// Helpers available in the MAIN world context // Helpers available in the MAIN world context
function resolveUrl(url) { function resolveUrl(url) {
@@ -137,10 +141,19 @@ function m3u8DeepProbeFn() {
function isManifestUrl(url) { function isManifestUrl(url) {
if (!url) return false; if (!url) return false;
const u = url.toLowerCase(); const u = url.toLowerCase();
return u.includes('.m3u8') || u.includes('.hlsvod') || // Dynamic route TEMPLATES (e.g. /playlist/%1%/ or /videos/{id}/) are nav
(u.includes('/hls/') && u.includes('http')) || // links, not fetchable manifests. A placeholder is a `%` that is NOT the
(u.includes('/playlist') && u.includes('http')) || // start of a valid %-encoded hex pair (so real tokens like %2B, %2F are
(u.includes('/manifest') && u.includes('http')); // preserved) or a {name} brace token — so genuine %-encoded auth tokens are
// never mistaken for template placeholders.
if (/%(?!([0-9A-Fa-f]{2}))/i.test(u) || /\{[a-z0-9_]+\}/i.test(u)) return false;
// .m3u8 / .hlsvod / /hls/ are concrete HLS signals.
if (u.includes('.m3u8') || u.includes('.hlsvod') || u.includes('/hls/')) return true;
// Plural collection paths are nav menus, not manifests.
if (u.includes('/playlists') || u.includes('/manifests')) return false;
// A singular manifest-ish path segment with no concrete file may still be a
// real (rare, extensionless) endpoint, so allow it when it carries http.
return (u.includes('/manifest') || u.includes('/playlist')) && u.includes('http');
} }
function walk(obj, path, depth) { function walk(obj, path, depth) {
@@ -666,6 +679,7 @@ async function probeHlsManifests(tabId) {
log('probeHlsManifests: executing deep probe on tab', tabId); log('probeHlsManifests: executing deep probe on tab', tabId);
const raw = await chrome.scripting.executeScript({ const raw = await chrome.scripting.executeScript({
target: { tabId, allFrames: true }, target: { tabId, allFrames: true },
world: 'MAIN',
func: m3u8DeepProbeFn, func: m3u8DeepProbeFn,
}); });
// When allFrames is true, destroyed / sandboxed frames may return null entries. // When allFrames is true, destroyed / sandboxed frames may return null entries.
@@ -782,6 +796,7 @@ async function scrapeTab(tabId, manifests) {
try { try {
const raw = await chrome.scripting.executeScript({ const raw = await chrome.scripting.executeScript({
target: { tabId, allFrames: true }, target: { tabId, allFrames: true },
world: 'MAIN',
func: scrapeFn, func: scrapeFn,
args: [manifests], args: [manifests],
}); });
@@ -873,6 +888,10 @@ function filterManifests(manifests) {
*/ */
function isValidManifestUrl(url) { function isValidManifestUrl(url) {
if (!url || typeof url !== 'string') return false; if (!url || typeof url !== 'string') return false;
// Dynamic route templates / nav links are not fetchable manifests. A
// placeholder is a `%` not followed by two hex digits (real %2B etc. are
// kept) or a {name} brace token.
if (/%(?!([0-9A-Fa-f]{2}))/i.test(url) || /\{[a-z0-9_]+\}/i.test(url)) return false;
let urlObj; let urlObj;
try { try {
urlObj = new URL(url); urlObj = new URL(url);
@@ -880,9 +899,15 @@ function filterManifests(manifests) {
return false; return false;
} }
const path = urlObj.pathname.toLowerCase(); const path = urlObj.pathname.toLowerCase();
// Plural collection paths and trailing-slash directory listings are nav menus,
// not manifest files (e.g. /playlists/, /playlists/liked/, /playlist/%id%/).
if (path.includes('/playlists') || path.includes('/manifests') || path.endsWith('/')) {
return false;
}
const isManifestLike = path.includes('.m3u8') || path.includes('.hlsvod') || const isManifestLike = path.includes('.m3u8') || path.includes('.hlsvod') ||
(path.includes('/master') && path.includes('.m3u')) ||
(path.includes('/playlist') && !path.endsWith('.ts')) || (path.includes('/playlist') && !path.endsWith('.ts')) ||
(path.includes('/master') && path.includes('.m3u')); path.includes('/manifest');
// Not a manifest path — allow it (it may be a direct media source, not HLS). // Not a manifest path — allow it (it may be a direct media source, not HLS).
if (!isManifestLike) return true; if (!isManifestLike) return true;
// A manifest path must sit in a directory, not be a bare top-level file. // A manifest path must sit in a directory, not be a bare top-level file.
@@ -1150,6 +1175,7 @@ findBtn.addEventListener('click', async () => {
try { try {
await chrome.scripting.executeScript({ await chrome.scripting.executeScript({
target: { tabId: activeTab.id }, target: { tabId: activeTab.id },
world: 'MAIN',
func: () => { func: () => {
const diag = []; const diag = [];
// Check interceptor data // Check interceptor data