fix(scan): run deep probe/scraper in MAIN world; reject route-template "manifests"
CI / test (pull_request) Successful in 5m48s
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:
1 parent
e3b904783f
commit
7139c5ea6f
2 files changed
+168
-48
No files matched your search
+113
-19
@@ -14,8 +14,8 @@ const viewerSource = fs.readFileSync(
|
||||
);
|
||||
const interceptorSource = fs.readFileSync(
|
||||
path.join(__dirname, '../src/inject/interceptors.js'),
|
||||
'utf-8'
|
||||
);
|
||||
'utf-8'
|
||||
);
|
||||
|
||||
/* ========================================================================== */
|
||||
/* Helpers */
|
||||
@@ -1027,21 +1027,48 @@ describe('HLS QA: iframe and multi-frame scenarios', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should scrape video elements from all frames', () => {
|
||||
const indexSource = fs.readFileSync(
|
||||
path.join(__dirname, '../src/viewer/index.js'),
|
||||
'utf-8'
|
||||
);
|
||||
it('should scrape video elements from all frames', () => {
|
||||
const indexSource = fs.readFileSync(
|
||||
path.join(__dirname, '../src/viewer/index.js'),
|
||||
'utf-8'
|
||||
);
|
||||
|
||||
// Verify scrapeTab runs in all frames
|
||||
const scrapeMatch = indexSource.match(/scrapeTab[\s\S]*?executeScript[\s\S]*?\}/);
|
||||
if (!scrapeMatch) {
|
||||
throw new Error('scrapeTab function not found');
|
||||
}
|
||||
if (!scrapeMatch[0].includes('allFrames: true')) {
|
||||
throw new Error('scrapeTab must use allFrames: true');
|
||||
}
|
||||
});
|
||||
// Verify scrapeTab runs in all frames
|
||||
const scrapeMatch = indexSource.match(/scrapeTab[\s\S]*?executeScript[\s\S]*?\}/);
|
||||
if (!scrapeMatch) {
|
||||
throw new Error('scrapeTab function not found');
|
||||
}
|
||||
if (!scrapeMatch[0].includes('allFrames: true')) {
|
||||
throw new Error('scrapeTab must use allFrames: true');
|
||||
}
|
||||
});
|
||||
|
||||
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)', () => {
|
||||
const manifestSource = fs.readFileSync(
|
||||
@@ -1211,6 +1238,73 @@ describe('HLS QA: Edge cases', () => {
|
||||
|
||||
if (intercepted.length !== 1) {
|
||||
throw new Error(`Expected 1 intercept for /manifest URL, got ${intercepted.length}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/* ========================================================================== */
|
||||
/* 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(', ')}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user