Files
goon_plugin/src/inject/interceptors.js
T
kfj001 66c621825f
CI / test (pull_request) Successful in 37s
refactor: decouple from site-specific CDN references
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.
2026-08-25 02:26:16 -07:00

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 */ }
}
})();