fix: real-time HLS interception via content script (architectural overhaul)
CI / test (pull_request) Canceled after 0s

Previous approach (DOM snapshot via executeScript) failed on Pornhub because:
- Video player loads asynchronously via JavaScript
- HLS manifest arrives via XHR/fetch AFTER page render
- <video> element only shows blob: URL, not the actual manifest

New approach:
- Content script injects interceptor into MAIN world at document_start
- Interceptor patches XMLHttpRequest and fetch() to capture .m3u8 URLs
- Periodic DOM scans detect HLS player instances on <video> elements
- Global config object scanning for embedded stream URLs
- Popup queries content script via chrome.tabs.sendMessage for real-time data
- If no DOM videos found but content script has data, create entries from intercepted sources

Changes:
- content.js: Full rewrite with main-world injection + XHR/fetch interception
- index.js: Query content scripts via messages, merge with DOM snapshot
- manifest.json: all_frames + document_start for early injection
This commit is contained in:
hermes committed 2026-08-10 18:53:27 +00:00
1 parent db52db7799
commit c22631d1f1
3 files changed
+354 -72

No files matched your search

+280 -10
View File
@@ -1,19 +1,289 @@
/**
* content.js — Content script injected into matching pages.
* Communicates with the background service worker via chrome.runtime.
* Use cautiously: runs in the page's DOM context, so isolate from page globals.
* Runs in the ISOLATED world. Coordinates scanning by injecting a script
* into the MAIN world (which CAN intercept XHR/fetch), then collects results
* via postMessage.
*
* Architecture:
* 1. Inject interceptor.js into MAIN world (intercepts network + DOM)
* 2. Interceptor posts results back via window.postMessage
* 3. This script collects results and forwards to the popup
*/
(function () {
'use strict';
console.log('[goon-plugin] Content script loaded');
// Collect results from injected scripts
let collectedData = {
hlsManifests: new Set(),
dashManifests: new Set(),
videoSources: [],
posterUrl: null,
};
// Example: inject a small element into the page.
// Uncomment and customize as needed.
//
// const badge = document.createElement('div');
// badge.textContent = 'Goon Plugin Active';
// badge.style.cssText = 'position:fixed;bottom:8px;right:8px;padding:4px 8px;...';
// document.body.appendChild(badge);
let pollingActive = false;
let pollTimer = null;
/**
* Inject the interceptor script into the MAIN world to intercept XHR/fetch.
*/
function injectInterceptor() {
const interceptorCode = `
(function() {
// Already injected
if (window.__goonInterceptor) return;
window.__goonInterceptor = true;
const manifests = { hls: new Set(), dash: new Set() };
let scanCount = 0;
// --- XHR interception ---
const origOpen = XMLHttpRequest.prototype.open;
const origSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function(method, url) {
this.__goonUrl = typeof url === 'string' ? url : url.toString();
return origOpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function() {
const url = this.__goonUrl || '';
if (url.includes('.m3u8') || url.includes('/hls/')) {
manifests.hls.add(url);
}
if (url.includes('.mpd') || url.includes('/dash/')) {
manifests.dash.add(url);
}
this.addEventListener('load', function() {
try {
const resp = this.responseText;
if (resp && typeof resp === 'string') {
const m3u8Re = /https?:[^"'\\s]+\\.m3u8[^"'\\s]*/gi;
let m;
while ((m = m3u8Re.exec(resp)) !== null) {
const cleaned = m[0].replace(/[;,]$/, '');
if (!cleaned.includes('/subtitle') && !cleaned.includes('/captions') && !cleaned.includes('.m3u8x')) {
manifests.hls.add(cleaned);
}
}
}
} catch(e) {}
});
return origSend.apply(this, arguments);
};
// --- Fetch interception ---
const origFetch = window.fetch;
window.fetch = function(input, init) {
const url = typeof input === 'string' ? input : (input && input.url ? input.url.toString() : '');
if (url.includes('.m3u8') || url.includes('/hls/')) manifests.hls.add(url);
if (url.includes('.mpd') || url.includes('/dash/')) manifests.dash.add(url);
return origFetch.apply(this, arguments).then(function(resp) {
if (resp && resp.clone) {
resp.clone().text().then(function(text) {
if (text && typeof text === 'string') {
const m3u8Re = /https?:[^"'\\s]+\\.m3u8[^"'\\s]*/gi;
let m;
while ((m = m3u8Re.exec(text)) !== null) {
const cleaned = m[0].replace(/[;,]$/, '');
if (!cleaned.includes('/subtitle') && !cleaned.includes('/captions') && !cleaned.includes('.m3u8x')) {
manifests.hls.add(cleaned);
}
}
}
}).catch(function(){});
}
return resp;
}).catch(function(e) { return origFetch(input, init); });
};
// --- Scan for HLS instances on video elements ---
function scanVideos() {
scanCount++;
const videos = document.querySelectorAll('video');
const sources = [];
const hlsProps = ['hls', 'hlsInstance', '_hls', '__hls', 'player', '_player', '__player__'];
for (const v of videos) {
let src = v.currentSrc || v.src || '';
let poster = v.poster || '';
let hlsUrl = null;
// Check HLS instances
for (const prop of hlsProps) {
if (v[prop] && typeof v[prop] === 'object') {
const obj = v[prop];
if (obj.url) hlsUrl = obj.url;
else if (obj.opts && obj.opts.url) hlsUrl = obj.opts.url;
else if (obj.config && obj.config.url) hlsUrl = obj.config.url;
else if (obj.levels && obj.levels.length > 0) hlsUrl = obj.levels[0].uri;
else if (obj.sourceLevels && obj.sourceLevels.length > 0) hlsUrl = obj.sourceLevels[0].uri;
if (hlsUrl) break;
}
}
if (hlsUrl) manifests.hls.add(hlsUrl);
sources.push({
src: src,
poster: poster,
width: v.clientWidth || 0,
height: v.clientHeight || 0,
isBlob: src.startsWith('blob:'),
});
}
// --- Scan inline scripts for embedded URLs ---
const m3u8Re = /https?:[^"'\\s]+\\.m3u8[^"'\\s]*/gi;
for (const script of document.querySelectorAll('script')) {
if (script.src) continue;
const text = script.textContent;
if (!text) continue;
let m;
while ((m = m3u8Re.exec(text)) !== null) {
const cleaned = m[0].replace(/[;,]$/, '');
if (!cleaned.includes('/subtitle') && !cleaned.includes('/captions') && !cleaned.includes('.m3u8x')) {
manifests.hls.add(cleaned);
}
}
}
// --- Scan global config objects ---
const configVars = ['hlsConfig', 'hls_player', 'hlsOptions', 'playerConfig', 'streamConfig',
'videoConfig', 'playerOpts', 'phPlayer', 'playerData', 'videoData', 'streamData',
'cloudflareConfig', 'cfPlayer', 'cfStream', 'videoSource', '__videojs__', 'videojs'];
for (const vname of configVars) {
try {
const obj = window[vname];
if (obj && typeof obj === 'object') {
function deepFind(o, depth) {
if (!o || typeof o !== 'object' || depth > 4) return;
for (const k of ['src', 'url', 'source', 'manifest', 'hls', 'm3u8', 'file', 'stream', 'video', 'data']) {
if (o[k]) {
if (typeof o[k] === 'string') {
if (o[k].includes('.m3u8')) manifests.hls.add(o[k]);
if (o[k].includes('.mpd')) manifests.dash.add(o[k]);
} else {
deepFind(o[k], depth + 1);
}
}
}
}
deepFind(obj, 0);
}
} catch(e) {}
}
// Post results to content script via postMessage
window.postMessage({
type: '__GOON_SCAN_RESULT__',
hlsManifests: Array.from(manifests.hls),
dashManifests: Array.from(manifests.dash),
videoSources: sources,
scanCount: scanCount,
}, '*');
}
// Run initial scan + periodic rescans
scanVideos();
setInterval(scanVideos, 1500);
})();
`;
// Inject as a script element into the page (runs in MAIN world)
const script = document.createElement('script');
script.textContent = interceptorCode;
script.setAttribute('type', 'text/javascript');
(document.head || document.documentElement).appendChild(script);
script.parentNode.removeChild(script);
}
/**
* Listen for results from the injected interceptor.
*/
window.addEventListener('message', function(event) {
if (event.source !== window) return;
if (!event.data || event.data.type !== '__GOON_SCAN_RESULT__') return;
const data = event.data;
if (data.hlsManifests) {
for (const url of data.hlsManifests) {
collectedData.hlsManifests.add(url);
}
}
if (data.dashManifests) {
for (const url of data.dashManifests) {
collectedData.dashManifests.add(url);
}
}
if (data.videoSources && data.videoSources.length > 0) {
collectedData.videoSources = data.videoSources;
}
});
/**
* Extract poster URL from the page (content script CAN access DOM).
*/
function extractPoster() {
const posterSelectors = [
'meta[property="og:image"]',
'meta[name="twitter:image"]',
'meta[itemprop="image"]',
'img.poster, img.thumbnail, .video-poster img, .thumbnail img, .preview img',
'[data-poster]',
];
for (const selector of posterSelectors) {
const el = document.querySelector(selector);
if (el) {
const url = el.getAttribute('content') || el.getAttribute('src') || el.getAttribute('data-src') || el.getAttribute('data-poster');
if (url && url.startsWith('http')) {
collectedData.posterUrl = url;
return;
}
}
}
}
// Inject the interceptor on load
injectInterceptor();
// Extract poster from the content script's DOM view
extractPoster();
// Re-extract after a delay to catch dynamic content
setTimeout(extractPoster, 3000);
setTimeout(extractPoster, 6000);
/**
* Get collected data as a serializable object.
*/
function getData() {
extractPoster(); // Fresh scan
return {
hlsManifests: Array.from(collectedData.hlsManifests),
dashManifests: Array.from(collectedData.dashManifests),
videoSources: collectedData.videoSources,
posterUrl: collectedData.posterUrl,
timestamp: Date.now(),
};
}
// Listen for messages from the popup
chrome.runtime.onMessage.addListener(function(message, _sender, sendResponse) {
if (message.type === 'GOON_GET_VIDEO_DATA') {
sendResponse(getData());
return false;
}
if (message.type === 'GOON_INJECT') {
injectInterceptor();
sendResponse({ injected: true });
return false;
}
return false;
});
console.log('[goon-plugin] Content script loaded (isolated world). Interceptor injected.');
})();