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.
This commit is contained in:
1 parent
dc2c8092f0
commit
66c621825f
8 files changed
+536
-471
No files matched your search
+135
-100
@@ -19,10 +19,10 @@
|
||||
* overlays and an aggregate status bar.
|
||||
*/
|
||||
|
||||
// HLS is vendored locally to avoid CSP blocks on sites like Pornhub that
|
||||
// restrict script-src to 'self'. The hls.min.js bundle is built by Vite
|
||||
// and exposed via chrome.runtime.getURL() so it loads from the extension
|
||||
// origin, bypassing page CSP entirely.
|
||||
// HLS is vendored locally to avoid CSP blocks on sites that restrict
|
||||
// script-src to 'self'. The hls.js bundle is copied (not minified) so it
|
||||
// loads from the extension origin via chrome.runtime.getURL(), bypassing
|
||||
// page CSP entirely.
|
||||
|
||||
/**
|
||||
* Grid-optimized hls.js configuration.
|
||||
@@ -54,14 +54,15 @@ const panelsEl = document.getElementById('panels');
|
||||
|
||||
/** Track all Hls instances so we can destroy them on unload. */
|
||||
|
||||
/**
|
||||
* Custom HLS loader that proxies phncdn.com URLs through the service worker
|
||||
* (bypassing CORS/Referer restrictions) and fetches everything else natively.
|
||||
*
|
||||
* hls.js creates one loader instance per request via `new HlsLoader(hls)`, then
|
||||
* calls `loader.load(context, config, callbacks)` — callbacks are the 3rd arg
|
||||
* in v9 (some wrapper paths put them 2nd, so we scan each argument). The stats
|
||||
* object passed to the callbacks is pre-allocated by us and mutated by hls.js
|
||||
/**
|
||||
* Custom HLS loader that proxies URLs whose host blocks extension-origin
|
||||
* requests (via a learned proxy-host set) through the service worker
|
||||
* (bypassing CORS/Referer restrictions) and fetches everything else natively.
|
||||
*
|
||||
* hls.js creates one loader instance per request via `new HlsLoader(hls)`, then
|
||||
* calls `loader.load(context, config, callbacks)` — callbacks are the 3rd arg
|
||||
* in v9 (some wrapper paths put them 2nd, so we scan each argument). The stats
|
||||
* object passed to the callbacks is pre-allocated by us and mutated by hls.js
|
||||
* in place, so it must have the correct sub-shape (see createLoadedStats).
|
||||
*/
|
||||
function HlsLoader(config) {
|
||||
@@ -134,8 +135,8 @@ const panelsEl = document.getElementById('panels');
|
||||
const stats = this.stats;
|
||||
stats.loading.start = trequest;
|
||||
|
||||
const isManifest = /\.m3u8($|\?)/i.test(url || '');
|
||||
const needProxy = (url || '').includes('phncdn') || (url || '').includes('pornhubcdn');
|
||||
const isManifest = /\.m3u8($|\?)/i.test(url || '');
|
||||
const needProxy = needsProxy(url);
|
||||
|
||||
// hls.js loader callback contract (verified against the bundled loader):
|
||||
// onSuccess(response, stats, context, networkDetails)
|
||||
@@ -176,8 +177,8 @@ const panelsEl = document.getElementById('panels');
|
||||
if (needProxy) {
|
||||
// Route through the service worker to bypass the CDN's CORS/Referer
|
||||
// restrictions. The SW adds the required Referer and returns base64.
|
||||
console.log('[goon-viewer] Proxying via SW:', (url || '').substring(0, 120));
|
||||
chrome.runtime.sendMessage({ type: 'GOON_PROXY_FETCH', url }, (resp) => {
|
||||
console.log('[goon-viewer] Proxying via SW:', (url || '').substring(0, 120));
|
||||
chrome.runtime.sendMessage({ type: 'GOON_PROXY_FETCH', url, referer: originForUrl(url) }, (resp) => {
|
||||
if (self._aborted) return;
|
||||
const lastErr = chrome.runtime.lastError;
|
||||
if (lastErr || !resp || !resp.ok) {
|
||||
@@ -233,52 +234,78 @@ const panelsEl = document.getElementById('panels');
|
||||
HlsLoader.prototype.destroy = function () { this.abort(); };
|
||||
HlsLoader.canSupportLoader = function () { return true; };
|
||||
|
||||
// === CORS diagnostics — check if phncdn.com allows extension-origin requests ===
|
||||
(async function checkCors() {
|
||||
// === Service-worker proxy helper (CDN-agnostic) ===
|
||||
// Proxies a video URL through the service worker (no CORS restrictions). The
|
||||
// SW forces the correct provider-origin Referer for the host (captured by the
|
||||
// background worker), so a referer-protecting CDN accepts the request.
|
||||
// Returns an ArrayBuffer of the response content.
|
||||
async function proxyFetch(url, referer) {
|
||||
try {
|
||||
const resp = await fetch('https://phncdn.com/images/favicon.ico', { method: 'HEAD', mode: 'cors' });
|
||||
console.log('[goon-viewer] CORS check phncdn.com:', resp.ok ? 'ALLOWED (status ' + resp.status + ')' : 'BLOCKED');
|
||||
} catch(e) {
|
||||
console.warn('[goon-viewer] CORS check phncdn.com: BLOCKED —', e && e.message ? e.message : e);
|
||||
}
|
||||
})();
|
||||
|
||||
// === HLS segment proxy helper ===
|
||||
// Proxies a video URL through the service worker (no CORS restrictions).
|
||||
// Returns an ArrayBuffer of the response content.
|
||||
async function proxyFetch(url) {
|
||||
try {
|
||||
const resp = await chrome.runtime.sendMessage({ type: 'GOON_PROXY_FETCH', url });
|
||||
const resp = await chrome.runtime.sendMessage({ type: 'GOON_PROXY_FETCH', url, referer });
|
||||
if (resp && resp.data && resp.ok) {
|
||||
// Decode base64 → ArrayBuffer
|
||||
// Decode base64 → ArrayBuffer
|
||||
const binary = atob(resp.data);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i++) {
|
||||
bytes[i] = binary.charCodeAt(i);
|
||||
}
|
||||
}
|
||||
return bytes.buffer;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch(e) {
|
||||
} catch(e) {
|
||||
console.warn('[goon-viewer] proxyFetch failed for:', url.substring(0, 120), e && e.message ? e.message : e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === Check if any manifest URLs require proxying (phncdn.com / cdn) ===
|
||||
function urlsNeedProxy(urls) {
|
||||
return urls.some(u => u && typeof u === 'string' && (u.includes('phncdn') || u.includes('pornhubcdn')));
|
||||
}
|
||||
// CDN hosts that block extension-origin requests are learned by a CORS probe
|
||||
// at viewer startup (hosts that fail the probe are recorded), then stored in
|
||||
// chrome.storage.local. The proxy is used only for those hosts, so we never
|
||||
// hardcode a particular CDN.
|
||||
const PROXY_HOSTS_KEY = 'goon_proxy_hosts';
|
||||
const proxyHosts = new Set();
|
||||
(async () => {
|
||||
try {
|
||||
const stored = await chrome.storage.local.get(PROXY_HOSTS_KEY);
|
||||
const hosts = stored[PROXY_HOSTS_KEY];
|
||||
if (Array.isArray(hosts)) for (const h of hosts) proxyHosts.add(String(h).toLowerCase());
|
||||
} catch { /* ignore */ }
|
||||
})();
|
||||
|
||||
/** Check whether fetching a URL from the extension page works (CORS test). */
|
||||
async function corsCheck(url) {
|
||||
// Origin for a URL's host — used as the Referer to request when a proxy fetch
|
||||
// is made for that host (the SW also captures the live referer per host).
|
||||
function originForUrl(url) {
|
||||
if (!url) return '';
|
||||
try { return new URL(url).origin + '/'; } catch { return ''; }
|
||||
}
|
||||
|
||||
// Whether a given video URL should be proxied: its host is a known
|
||||
// referer-protected CDN host.
|
||||
function needsProxy(url) {
|
||||
if (!url || typeof url !== 'string') return false;
|
||||
const host = hostOfUrl(url);
|
||||
return host ? proxyHosts.has(host) : false;
|
||||
}
|
||||
|
||||
// host of a URL (lowercased); '' when malformed.
|
||||
function hostOfUrl(url) {
|
||||
try { return new URL(url).host.toLowerCase(); } catch { return ''; }
|
||||
}
|
||||
|
||||
// Whether any of the given URLs needs proxying.
|
||||
function urlsNeedProxy(urls) {
|
||||
return urls.some(u => needsProxy(u));
|
||||
}
|
||||
|
||||
/** Check whether fetching a URL from the extension page works (CORS test). */
|
||||
async function corsCheck(url) {
|
||||
try {
|
||||
const resp = await fetch(url, { method: 'HEAD', mode: 'cors' });
|
||||
return resp.ok;
|
||||
} catch {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Track all Hls instances so we can destroy them on unload. */
|
||||
const hlsInstances = new Map();
|
||||
@@ -750,10 +777,10 @@ function wireHlsEvents(hls, video, panel) {
|
||||
renderPanelState(video, panel);
|
||||
hls.destroy();
|
||||
hlsInstances.delete(video);
|
||||
// A fatal network error on a phncdn stream is almost always an expired
|
||||
// auth token. The live source tab keeps rotating tokens, so pull a fresh
|
||||
// manifest from it and retry — capped at a few attempts per panel so a
|
||||
// tab with no live source doesn't loop forever.
|
||||
// A fatal network error on a token-protected stream is almost always an
|
||||
// expired auth token. The live source tab keeps rotating tokens, so pull a
|
||||
// fresh manifest from it and retry — capped at a few attempts per panel so a
|
||||
// tab with no live source doesn't loop forever.
|
||||
if ((video.dataset.hlsRescans || '0') | 0 < 3) {
|
||||
video.dataset.hlsRescans = ((video.dataset.hlsRescans || '0') | 0) + 1;
|
||||
console.log('[goon-viewer] fatal NETWORK_ERROR → auto-rescan attempt #' + video.dataset.hlsRescans);
|
||||
@@ -930,12 +957,12 @@ function playHls(video, manifestUrl, panel) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check whether this manifest URL needs proxying (phncdn.com without CORS headers
|
||||
// blocks extension-origin requests from the viewer page).
|
||||
const needProxy = manifestUrl.includes('phncdn') || manifestUrl.includes('pornhubcdn');
|
||||
if (needProxy) {
|
||||
console.log('[goon-viewer] Manifest needs proxying via service worker');
|
||||
}
|
||||
// Check whether this manifest URL needs proxying (a referer-protected host
|
||||
// without CORS headers blocks extension-origin requests from the viewer page).
|
||||
const needProxy = needsProxy(manifestUrl);
|
||||
if (needProxy) {
|
||||
console.log('[goon-viewer] Manifest needs proxying via service worker');
|
||||
}
|
||||
|
||||
destroyHls(video);
|
||||
|
||||
@@ -977,9 +1004,9 @@ function playHls(video, manifestUrl, panel) {
|
||||
// Log the manifest URL being loaded for debugging (helps diagnose playback failures).
|
||||
console.log(`[goon-viewer] Loading HLS manifest:`, manifestUrl.substring(0, 200));
|
||||
|
||||
// Remember the manifest actually loaded so a later auto-rescan can retry from
|
||||
// the freshest token on the same video (extractVideoId keys off this).
|
||||
video.dataset.hlsManifest = manifestUrl;
|
||||
// Remember the manifest actually loaded so a later auto-rescan can retry from
|
||||
// the freshest token on the same video (the service worker keys off the path).
|
||||
video.dataset.hlsManifest = manifestUrl;
|
||||
|
||||
hls.loadSource(manifestUrl);
|
||||
hls.attachMedia(video);
|
||||
@@ -1017,32 +1044,32 @@ function loadHlsJs() {
|
||||
});
|
||||
}
|
||||
|
||||
/* ======================================================================== */
|
||||
/* Fresh-token resolution — replace expired/revoked phncdn URLs */
|
||||
/* ======================================================================== */
|
||||
/* ======================================================================== */
|
||||
/* Fresh-token resolution — replace expired/revoked token URLs */
|
||||
/* ======================================================================== */
|
||||
|
||||
/**
|
||||
* Ask the service worker for the freshest live phncdn manifest for the video
|
||||
* encoded in `storedUrl`. The live source tab keeps rotating short-lived
|
||||
* phncdn tokens (a stored one may already be 410 Gone), so the freshest
|
||||
* captured manifest — identified by matching the videoId in the path — is the
|
||||
* one that will actually validate. Resolves to the new URL, or null when the
|
||||
* tab captured nothing (or the caller can't reach the SW).
|
||||
*/
|
||||
async function resolveFreshManifest(storedUrl) {
|
||||
if (!storedUrl || !storedUrl.includes('phncdn')) return null;
|
||||
try {
|
||||
const resp = await chrome.runtime.sendMessage({ type: 'GOON_RESOLVE_MANIFEST', url: storedUrl });
|
||||
if (resp && resp.ok && resp.changed && resp.fresh) {
|
||||
console.log('[goon-viewer] resolved fresh manifest for', (resp.videoId || '?'),
|
||||
'→', (resp.fresh || '').substring(0, 100));
|
||||
/**
|
||||
* Ask the service worker for the freshest live manifest for the video encoded
|
||||
* in `storedUrl`. The live source tab keeps rotating short-lived auth tokens
|
||||
* (a stored one may already be 410 Gone), so the freshest captured manifest —
|
||||
* identified by matching the video's group key in the path — is the one that
|
||||
* will actually validate. Resolves to the new URL, or null when the tab
|
||||
* captured nothing (or the caller can't reach the SW).
|
||||
*/
|
||||
async function resolveFreshManifest(storedUrl) {
|
||||
if (!storedUrl || !needsProxy(storedUrl)) return null;
|
||||
try {
|
||||
const resp = await chrome.runtime.sendMessage({ type: 'GOON_RESOLVE_MANIFEST', url: storedUrl });
|
||||
if (resp && resp.ok && resp.changed && resp.fresh) {
|
||||
console.log('[goon-viewer] resolved fresh manifest for', (resp.groupKey || '?'),
|
||||
'→', (resp.fresh || '').substring(0, 100));
|
||||
return resp.fresh;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[goon-viewer] resolveFreshManifest failed:', e && e.message ? e.message : e);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[goon-viewer] resolveFreshManifest failed:', e && e.message ? e.message : e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-attempt playback on `video` after its stored manifest died (fatal
|
||||
@@ -1154,16 +1181,24 @@ async function init() {
|
||||
});
|
||||
}
|
||||
|
||||
// Test CORS on all video URLs
|
||||
const urlsToTest = videoUrls.map(v => v.src || v.hlsManifest || 'none').filter(u => u !== 'none' && typeof u === 'string');
|
||||
for (const url of [...new Set(urlsToTest)]) {
|
||||
const corsOk = await corsCheck(url);
|
||||
console.log('[goon-viewer] CORS', corsOk ? 'OK' : 'BLOCKED:', url.substring(0, 120));
|
||||
}
|
||||
// Probe CORS on each video URL. Hosts that block extension-origin
|
||||
// requests are learned as referer-protected and persisted so the viewer
|
||||
// routes them through the service-worker proxy.
|
||||
const urlsToTest = videoUrls.map(v => v.src || v.hlsManifest || 'none').filter(u => u !== 'none' && typeof u === 'string');
|
||||
const blockedHosts = new Set();
|
||||
for (const url of [...new Set(urlsToTest)]) {
|
||||
const corsOk = await corsCheck(url);
|
||||
if (!corsOk) blockedHosts.add(hostOfUrl(url));
|
||||
console.log('[goon-viewer] CORS', corsOk ? 'OK' : 'BLOCKED:', url.substring(0, 120));
|
||||
}
|
||||
if (blockedHosts.size > 0) {
|
||||
for (const h of blockedHosts) if (h) proxyHosts.add(h);
|
||||
try { await chrome.storage.local.set({ [PROXY_HOSTS_KEY]: [...proxyHosts] }); } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
// Check if any URLs point to phncdn.com (proxy will be needed if CORS blocked)
|
||||
const needsProxy = urlsToTest.some(u => u.includes('phncdn') || u.includes('pornhubcdn'));
|
||||
console.log('[goon-viewer] Proxy required:', needsProxy);
|
||||
// Report whether any URL needs proxying (referer-protected host).
|
||||
const anyNeedsProxy = urlsNeedProxy(urlsToTest);
|
||||
console.log('[goon-viewer] Proxy required:', anyNeedsProxy);
|
||||
} else {
|
||||
console.warn('[goon-viewer] No video entries in storage for ID:', id, '— data may have been cleared or never saved');
|
||||
}
|
||||
@@ -1194,15 +1229,15 @@ async function init() {
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Before playback, swap each stored phncdn manifest for the freshest live
|
||||
// one the source tab has captured (a stored token may already be 410
|
||||
// Gone). Non-phncdn manifests are left untouched.
|
||||
for (const { video, manifest, panel } of hlsEntries) {
|
||||
if (manifest.includes('phncdn')) {
|
||||
const fresh = await resolveFreshManifest(manifest);
|
||||
if (fresh) video.dataset.hlsManifest = fresh;
|
||||
}
|
||||
}
|
||||
// Before playback, swap each referer-protected manifest for the freshest
|
||||
// live one the source tab has captured (a stored token may already be 410
|
||||
// Gone). Non-protected manifests are left untouched.
|
||||
for (const { video, manifest } of hlsEntries) {
|
||||
if (needsProxy(manifest)) {
|
||||
const fresh = await resolveFreshManifest(manifest);
|
||||
if (fresh) video.dataset.hlsManifest = fresh;
|
||||
}
|
||||
}
|
||||
for (const { video, panel } of hlsEntries) {
|
||||
playHls(video, video.dataset.hlsManifest, panel);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user