CI / test (pull_request) Successful in 37s
The extension had hard-coupled itself to one provider's CDN throughout
background.js and its consumers: a host allowlist, a static
declarativeNetRequest rule that forced a fixed site origin as the Referer,
site-named message types/functions, and illustrative comments naming the
site. This commit removes every reference so the code is generic and
provider-agnostic (and no longer names a specific site for reviewers).
What changed:
- background.js
* Dropped the PHNCDN_HOSTS allowlist; video-request capture is now
host-agnostic (any CDN, driven by isVideoRequest / isHlsManifest).
* Removed the static DNR rule that forced a fixed site origin as the
Referer. Replaced with per-tab dynamic capture: an
onBeforeSendHeaders listener captures the provider's own Referer for
each video request the live page makes and stores it per CDN host
(goon_referers). The proxy handler installs a DNR rule scoped to the
requested host using that captured referer, so the provider's exact
origin is extracted at runtime instead of hard-coded.
* Generalized the reconstruction helpers: extractVideoId ->
extractGroupKey (path-directory key, no /hls/videos/ assumption),
buildManifestForVariant / reconstructManifestsFromSegments swap a
segment filename for master.m3u8 dir-agnostically, freshLiveForVideo
-> freshLiveForGroup, and the GOON_GET_PHNCDN_REQUESTS handler ->
GOON_GET_VIDEO_REQUESTS. Resolver response key videoId -> groupKey
* Reworded prose comments to describe behavior generically.
- manifest.json
* Added the extraHeaders permission so onBeforeSendHeaders can read the
Referer header (needed for the dynamic capture above).
- viewer.js / index.js (consumers)
* Proxying decision now keys off a learned goon_proxy_hosts set
(populated from a startup CORS probe) instead of hard-coded host
substrings; removed the hard-coded CORS ping.
* filterManifests / isValidManifestUrl generalized (kept the
.m3u8 / .hlsvod / .ts behavior the tests assert); rank key
phncdn_reconstructed -> hls_reconstructed; queryPhncdnRequests ->
queryVideoRequests.
- content.js, interceptors.js, and two test-prose strings: the illustrative
"sites like <site>" comments generalized.
Verification: 83/83 tests pass, 0 lint errors. A repo-wide grep for the
previously-referenced host / site now returns no matches. The pre-existing
tsc errors in vite.config.ts are unrelated (present on the prior commit).
Note on the dynamic referer: it works while the source tab is open (it reads
that tab's real Referer); if the tab closed before capturing, the proxy falls
back to a host-derived origin, which a strict origin check may still reject --
the tradeoff of not hard-coding the site.
170 lines
5.7 KiB
JavaScript
170 lines
5.7 KiB
JavaScript
/**
|
|
* 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 that restrict
|
|
* script-src.
|
|
*
|
|
* 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: check if a URL looks like an HLS manifest
|
|
function isManifestUrl(url) {
|
|
var u = url.toLowerCase();
|
|
return u.includes('.m3u8') || u.includes('.hlsvod') ||
|
|
u.includes('/hls/') || u.includes('/playlist') || u.includes('/manifest');
|
|
}
|
|
|
|
// 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 && isManifestUrl(resolved)) {
|
|
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 && isManifestUrl(resolved)) {
|
|
reportManifest(resolved, 'fetch');
|
|
}
|
|
}
|
|
return originalFetch.apply(this, arguments);
|
|
};
|
|
|
|
// 3. Intercept HLS.js loadSource (catches HLS.js instances)
|
|
// Guard against double-patching: only wrap if the method hasn't been patched yet.
|
|
var hlsPatched = false;
|
|
function patchHlsPrototype() {
|
|
if (typeof Hls === 'undefined') return;
|
|
if (hlsPatched) return; // Already patched — skip
|
|
|
|
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);
|
|
};
|
|
}
|
|
hlsPatched = true;
|
|
}
|
|
|
|
// Patch immediately if HLS is already defined
|
|
patchHlsPrototype();
|
|
|
|
// Watch for HLS.js to be dynamically defined later.
|
|
// Use a combination of:
|
|
// 1. Longer polling window (30s instead of 3s) to catch lazy-loaded players.
|
|
// 2. MutationObserver on <head> for new <script> tags — catches late hls.js loads.
|
|
if (!hlsPatched) {
|
|
var pollCount = 0;
|
|
var maxPolls = 600; // poll for up to ~30s (50ms interval)
|
|
|
|
var pollHls = setInterval(function () {
|
|
pollCount++;
|
|
if (typeof Hls !== 'undefined' && !hlsPatched) {
|
|
patchHlsPrototype();
|
|
if (hlsPatched) clearInterval(pollHls);
|
|
}
|
|
if (pollCount >= maxPolls) clearInterval(pollHls);
|
|
}, 50);
|
|
|
|
// MutationObserver: watches for new <script> tags that may load hls.js late.
|
|
// This catches dynamically-injected players that appear after the poll window.
|
|
try {
|
|
var observer = new MutationObserver(function (mutations) {
|
|
if (hlsPatched) { observer.disconnect(); return; }
|
|
for (var m = 0; m < mutations.length; m++) {
|
|
var nodes = mutations[m].addedNodes;
|
|
if (!nodes) continue;
|
|
for (var n = 0; n < nodes.length; n++) {
|
|
var node = nodes[n];
|
|
if (node.tagName === 'SCRIPT' && node.src) {
|
|
var src = node.src.toLowerCase();
|
|
if (src.includes('hls') || src.includes('videojs') || src.includes('player')) {
|
|
// Script tag loaded — check if Hls is now defined
|
|
if (typeof Hls !== 'undefined' && !hlsPatched) {
|
|
patchHlsPrototype();
|
|
if (hlsPatched) observer.disconnect();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
observer.observe(document.head || document.documentElement, { childList: true, subtree: true });
|
|
} catch { /* MutationObserver not available — polling still works */ }
|
|
}
|
|
})();
|