fix: use script.src for content.js interceptor injection to bypass CSP
CI / test (pull_request) Successful in 14s
CI / test (pull_request) Successful in 14s
The inline script.textContent injection in content.js violated the
strict CSP on sites like Pornhub ('script-src' without 'unsafe-inline').
Move interceptor code to a separate interceptors.js file and inject
via script.src = chrome.runtime.getURL('interceptors.js'). Chrome
treats extension-originated <script src> as content scripts — not
as page inline code — so the page's CSP does not apply.
- New src/inject/interceptors.js with all main-world interceptor code
- content.js now creates <script src> instead of <script textContent>
- Vite config updated to build interceptors.js to dist/
This commit is contained in:
1 parent
0d4b62c485
commit
a621db9674
3 files changed
+146
-114
No files matched your search
+9
-114
@@ -24,123 +24,18 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Inject interceptors into the MAIN world at document_start.
|
* Inject interceptors into the MAIN world at document_start.
|
||||||
* Runs BEFORE any page JS, so we catch all HLS activity.
|
*
|
||||||
|
* Uses script.src = chrome.runtime.getURL('interceptors.js') instead of
|
||||||
|
* script.textContent to bypass strict page CSP. Sites like Pornhub enforce
|
||||||
|
* 'script-src' that blocks inline execution (textContent). Chrome treats
|
||||||
|
* extension-originated <script src="..."> as content scripts — not as
|
||||||
|
* page inline code — so the page's CSP does not apply.
|
||||||
|
*
|
||||||
|
* Runs BEFORE any page JS (document_start), so we catch all HLS activity.
|
||||||
*/
|
*/
|
||||||
function injectInterceptors() {
|
function injectInterceptors() {
|
||||||
const interceptCode = `
|
|
||||||
(function() {
|
|
||||||
if (window.__goon_plugin_intercepted) return;
|
|
||||||
window.__goon_plugin_intercepted = true;
|
|
||||||
|
|
||||||
window.__goon_plugin_data = window.__goon_plugin_data || {
|
|
||||||
manifests: [],
|
|
||||||
sources: []
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper: resolve relative URLs
|
|
||||||
function resolveUrl(url) {
|
|
||||||
if (!url) return null;
|
|
||||||
try {
|
|
||||||
if (url.startsWith('//')) return 'https:' + url;
|
|
||||||
if (url.startsWith('http')) return url;
|
|
||||||
return new URL(url, window.location.origin).href;
|
|
||||||
} catch { return null; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: report manifest to content script via postMessage
|
|
||||||
function reportManifest(url, source) {
|
|
||||||
var resolved = resolveUrl(url);
|
|
||||||
if (!resolved) return;
|
|
||||||
if (window.__goon_plugin_data.manifests.includes(resolved)) return;
|
|
||||||
window.__goon_plugin_data.manifests.push(resolved);
|
|
||||||
|
|
||||||
window.postMessage({
|
|
||||||
type: 'GOON_PLUGIN_HLS_INTERCEPT',
|
|
||||||
url: resolved,
|
|
||||||
source: source,
|
|
||||||
timestamp: Date.now()
|
|
||||||
}, '*');
|
|
||||||
}
|
|
||||||
|
|
||||||
// === INTERCEPTORS ===
|
|
||||||
|
|
||||||
// 1. Intercept XMLHttpRequest for manifest-like requests
|
|
||||||
var originalOpen = XMLHttpRequest.prototype.open;
|
|
||||||
var originalSend = XMLHttpRequest.prototype.send;
|
|
||||||
XMLHttpRequest.prototype.open = function(method, url) {
|
|
||||||
this._goon_url = url;
|
|
||||||
return originalOpen.apply(this, arguments);
|
|
||||||
};
|
|
||||||
XMLHttpRequest.prototype.send = function(body) {
|
|
||||||
if (this._goon_url && typeof this._goon_url === 'string') {
|
|
||||||
var resolved = resolveUrl(this._goon_url);
|
|
||||||
if (resolved && (resolved.includes('.m3u8') ||
|
|
||||||
resolved.includes('/playlist') ||
|
|
||||||
resolved.includes('/manifest'))) {
|
|
||||||
reportManifest(resolved, 'xhr');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return originalSend.apply(this, arguments);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 2. Intercept fetch() for manifest-like requests
|
|
||||||
var originalFetch = window.fetch;
|
|
||||||
window.fetch = function(resource, init) {
|
|
||||||
var url = typeof resource === 'string' ? resource : (resource && resource.url);
|
|
||||||
if (url && typeof url === 'string') {
|
|
||||||
var resolved = resolveUrl(url);
|
|
||||||
if (resolved && (resolved.includes('.m3u8') ||
|
|
||||||
resolved.includes('/playlist') ||
|
|
||||||
resolved.includes('/manifest'))) {
|
|
||||||
reportManifest(resolved, 'fetch');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return originalFetch.apply(this, arguments);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 3. Intercept HLS.js loadSource (catches HLS.js instances)
|
|
||||||
function patchHlsPrototype() {
|
|
||||||
if (typeof Hls === 'undefined') return;
|
|
||||||
var originalLoadSource = Hls.prototype.loadSource;
|
|
||||||
if (!originalLoadSource) return;
|
|
||||||
|
|
||||||
Hls.prototype.loadSource = function(url) {
|
|
||||||
reportManifest(url, 'hls_loadSource');
|
|
||||||
return originalLoadSource.apply(this, arguments);
|
|
||||||
};
|
|
||||||
|
|
||||||
var originalAttachMedia = Hls.prototype.attachMedia;
|
|
||||||
if (originalAttachMedia) {
|
|
||||||
Hls.prototype.attachMedia = function(media) {
|
|
||||||
if (media && media.tagName === 'VIDEO') {
|
|
||||||
media._goon_hls_manifest = this.url;
|
|
||||||
reportManifest(this.url, 'hls_attachMedia');
|
|
||||||
}
|
|
||||||
return originalAttachMedia.apply(this, arguments);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Patch immediately if HLS is already defined
|
|
||||||
patchHlsPrototype();
|
|
||||||
|
|
||||||
// Watch for HLS.js to be dynamically defined later
|
|
||||||
var originalDefineProperty = Object.defineProperty;
|
|
||||||
var hlsPatched = false;
|
|
||||||
Object.defineProperty = function(obj, prop, descriptor) {
|
|
||||||
var result = originalDefineProperty.apply(this, arguments);
|
|
||||||
if (obj === window && prop === 'Hls' && !hlsPatched) {
|
|
||||||
hlsPatched = true;
|
|
||||||
patchHlsPrototype();
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
})();
|
|
||||||
`;
|
|
||||||
|
|
||||||
const script = document.createElement('script');
|
const script = document.createElement('script');
|
||||||
script.textContent = interceptCode;
|
script.src = chrome.runtime.getURL('interceptors.js');
|
||||||
script.setAttribute('type', 'text/javascript');
|
script.setAttribute('type', 'text/javascript');
|
||||||
(document.head || document.documentElement).appendChild(script);
|
(document.head || document.documentElement).appendChild(script);
|
||||||
// Remove the script element after execution but keep interceptors active
|
// Remove the script element after execution but keep interceptors active
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
/**
|
||||||
|
* interceptors.js — Main-world interceptors for HLS manifest detection.
|
||||||
|
*
|
||||||
|
* This file is injected into the main world via a <script src="..."> tag
|
||||||
|
* (NOT inline textContent) to bypass strict CSP on sites like Pornhub.
|
||||||
|
*
|
||||||
|
* When loaded as an external script from the extension origin, Chrome treats
|
||||||
|
* it as an extension content script — not as page inline code — so the
|
||||||
|
* page's script-src CSP does not apply.
|
||||||
|
*
|
||||||
|
* Loaded by content.js at document_start before any page JS runs.
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
if (window.__goon_plugin_intercepted) return;
|
||||||
|
window.__goon_plugin_intercepted = true;
|
||||||
|
|
||||||
|
window.__goon_plugin_data = window.__goon_plugin_data || {
|
||||||
|
manifests: [],
|
||||||
|
sources: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper: resolve relative URLs
|
||||||
|
function resolveUrl(url) {
|
||||||
|
if (!url) return null;
|
||||||
|
try {
|
||||||
|
if (url.startsWith('//')) return 'https:' + url;
|
||||||
|
if (url.startsWith('http')) return url;
|
||||||
|
return new URL(url, window.location.origin).href;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: report manifest to content script via postMessage
|
||||||
|
function reportManifest(url, source) {
|
||||||
|
var resolved = resolveUrl(url);
|
||||||
|
if (!resolved) return;
|
||||||
|
if (window.__goon_plugin_data.manifests.includes(resolved)) return;
|
||||||
|
window.__goon_plugin_data.manifests.push(resolved);
|
||||||
|
|
||||||
|
window.postMessage(
|
||||||
|
{
|
||||||
|
type: 'GOON_PLUGIN_HLS_INTERCEPT',
|
||||||
|
url: resolved,
|
||||||
|
source: source,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
},
|
||||||
|
'*',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === INTERCEPTORS ===
|
||||||
|
|
||||||
|
// 1. Intercept XMLHttpRequest for manifest-like requests
|
||||||
|
var originalOpen = XMLHttpRequest.prototype.open;
|
||||||
|
var originalSend = XMLHttpRequest.prototype.send;
|
||||||
|
XMLHttpRequest.prototype.open = function (method, url) {
|
||||||
|
this._goon_url = url;
|
||||||
|
return originalOpen.apply(this, arguments);
|
||||||
|
};
|
||||||
|
XMLHttpRequest.prototype.send = function (body) {
|
||||||
|
if (this._goon_url && typeof this._goon_url === 'string') {
|
||||||
|
var resolved = resolveUrl(this._goon_url);
|
||||||
|
if (
|
||||||
|
resolved &&
|
||||||
|
(resolved.includes('.m3u8') ||
|
||||||
|
resolved.includes('/playlist') ||
|
||||||
|
resolved.includes('/manifest'))
|
||||||
|
) {
|
||||||
|
reportManifest(resolved, 'xhr');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return originalSend.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. Intercept fetch() for manifest-like requests
|
||||||
|
var originalFetch = window.fetch;
|
||||||
|
window.fetch = function (resource, init) {
|
||||||
|
var url = typeof resource === 'string' ? resource : resource && resource.url;
|
||||||
|
if (url && typeof url === 'string') {
|
||||||
|
var resolved = resolveUrl(url);
|
||||||
|
if (
|
||||||
|
resolved &&
|
||||||
|
(resolved.includes('.m3u8') ||
|
||||||
|
resolved.includes('/playlist') ||
|
||||||
|
resolved.includes('/manifest'))
|
||||||
|
) {
|
||||||
|
reportManifest(resolved, 'fetch');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return originalFetch.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. Intercept HLS.js loadSource (catches HLS.js instances)
|
||||||
|
function patchHlsPrototype() {
|
||||||
|
if (typeof Hls === 'undefined') return;
|
||||||
|
var originalLoadSource = Hls.prototype.loadSource;
|
||||||
|
if (!originalLoadSource) return;
|
||||||
|
|
||||||
|
Hls.prototype.loadSource = function (url) {
|
||||||
|
reportManifest(url, 'hls_loadSource');
|
||||||
|
return originalLoadSource.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
var originalAttachMedia = Hls.prototype.attachMedia;
|
||||||
|
if (originalAttachMedia) {
|
||||||
|
Hls.prototype.attachMedia = function (media) {
|
||||||
|
if (media && media.tagName === 'VIDEO') {
|
||||||
|
media._goon_hls_manifest = this.url;
|
||||||
|
reportManifest(this.url, 'hls_attachMedia');
|
||||||
|
}
|
||||||
|
return originalAttachMedia.apply(this, arguments);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch immediately if HLS is already defined
|
||||||
|
patchHlsPrototype();
|
||||||
|
|
||||||
|
// Watch for HLS.js to be dynamically defined later
|
||||||
|
var originalDefineProperty = Object.defineProperty;
|
||||||
|
var hlsPatched = false;
|
||||||
|
Object.defineProperty = function (obj, prop, descriptor) {
|
||||||
|
var result = originalDefineProperty.apply(this, arguments);
|
||||||
|
if (obj === window && prop === 'Hls' && !hlsPatched) {
|
||||||
|
hlsPatched = true;
|
||||||
|
patchHlsPrototype();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -57,6 +57,9 @@ export default defineConfig({
|
|||||||
content: resolve(__dirname, 'src/content/content.js'),
|
content: resolve(__dirname, 'src/content/content.js'),
|
||||||
viewer: resolve(__dirname, 'src/viewer/viewer.html'),
|
viewer: resolve(__dirname, 'src/viewer/viewer.html'),
|
||||||
viewerIndex: resolve(__dirname, 'src/viewer/index.html'),
|
viewerIndex: resolve(__dirname, 'src/viewer/index.html'),
|
||||||
|
probeFn: resolve(__dirname, 'src/viewer/probeFn.js'),
|
||||||
|
scrapeFn: resolve(__dirname, 'src/viewer/scrapeFn.js'),
|
||||||
|
interceptors: resolve(__dirname, 'src/inject/interceptors.js'),
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
entryFileNames: '[name].js',
|
entryFileNames: '[name].js',
|
||||||
|
|||||||
Reference in new issue
Block a user