Fixes issue with pornhub. Removes references.
CI / test (pull_request) Failing after 20s

This commit is contained in:
kfj001 committed 2026-08-30 04:48:23 +00:00
1 parent 7b43d260a2
commit 4357ce4094
9 files changed
+337 -100

No files matched your search

+98 -62
View File
@@ -227,6 +227,11 @@ async function injectFreshToken(url) {
// re-query storage for a freshly-rotated token.
function invalidateTokenCache() { tokenCache.clear(); }
// CDN registrable domains whose referer we have already captured this SW
// lifetime, so we do not re-query the tab for every segment of a stream.
// Lost if the SW is killed; persistReferer's storage dedupe covers a fresh SW.
const refererCapturedDomains = new Set();
chrome.webRequest.onBeforeRequest.addListener(
async (details) => {
if (details.tabId < 0) return;
@@ -235,6 +240,28 @@ chrome.webRequest.onBeforeRequest.addListener(
if (isVideoRequest(details.url)) {
console.log('[goon-bg] webRequest: video request in tab', details.tabId, ':', details.url.substring(0, 180));
await persistVideoRequest(details.tabId, details.url);
// Capture the provider referer for this CDN host from the source tab's
// URL: the browser sends the referring page's URL as the Referer for a
// cross-origin video request, and that IS the tab's URL. extraInfoSpec
// (reading the actual request headers) is rejected by some Chromium
// builds and fails the whole service worker, so we derive the referer
// from the tab instead. The `tabs` + host_permissions already allow it.
const host = hostOf(details.url);
const domain = host ? registrableDomain(host) : '';
if (domain && !refererCapturedDomains.has(domain)) {
try {
const tab = await chrome.tabs.get(details.tabId);
const ref = tab && tab.url;
if (ref) {
await persistReferer(host, ref);
refererCapturedDomains.add(domain);
console.log('[goon-bg] captured referer for', domain, '->', ref);
}
} catch {
// Tab may have closed mid-stream; a later request from a live tab
// re-captures. Best-effort.
}
}
}
// Also capture HLS manifest URLs (original logic)
@@ -258,7 +285,7 @@ chrome.webRequest.onBeforeRequest.addListener(
);
// Clean up stale PER-TAB entries when tabs are closed. NOTE: 'goon_referers' is
// a host-keyed GLOBAL store — it must outlive its capture tab so a proxy fetch
// a domain-keyed GLOBAL store — it must outlive its capture tab so a proxy fetch
// for that host still finds its referer after the source tab is closed. It is
// therefore NOT removed here (only the per-tab vr_requests_/wr_manifests_ keys
// are). Stale referers are pruned opportunistically in persistReferer.
@@ -308,87 +335,74 @@ function hostOf(url) {
try { return new URL(url).host.toLowerCase(); } catch { return ''; }
}
// Best-effort origin for a host when no referer was captured for it. Used only
// as a fallback; a captured referer always wins.
function originFromHost(host) {
return host ? 'https://' + host + '/' : '';
// Registrable domain of a CDN host — the label a single provider's referer
// applies to. A referer-protecting CDN serves manifests and segments from many
// edge subdomains that share ONE
// provider referer, so we key the referer store, the DNR rule, and proxy-host
// matching on this domain rather than the exact subhost.
//
// Heuristic: the last two DNS labels. Correct for the overwhelmingly common
// single-label public suffixes (.com, .net, .org, .io, ...). Known caveat:
// multi-label public suffixes such as .co.uk / .com.au collapse to the public
// suffix itself (foo.co.uk -> co.uk); that is an exotic edge case and no worse
// than the exact-host behavior it replaces. A single-level host (no dot, or a
// bare TLD) is returned unchanged.
function registrableDomain(host) {
if (!host) return '';
const parts = host.split('.');
if (parts.length < 2) return host;
return parts.slice(-2).join('.');
}
// Read the `Referer` request header (the browser may title-case it; the
// (onBeforeSendHeaders cannot read request headers in MV3 without a blocking API,
// so we fall back to the page origin for hosts that send no referer).
function readReferer(headers) {
if (!Array.isArray(headers)) return '';
for (const h of headers) {
if (h && h.name && h.name.toLowerCase() === 'referer') return h.value || '';
}
return '';
}
// How long a captured referer is trusted before it is pruned on the next
// persist. Referers are short-lived session tokens; this keeps the per-host
// global store from accumulating stale entries without tying cleanup to tab
// close (a still-open source tab's referer must survive other tabs closing).
const REFERER_TTL_MS = 30 * 60 * 1000;
// persist. Kept long enough for the scan-now / watch-later flow (a user scans a
// page, then opens the viewer much later) without tying cleanup to tab close (a
// still-open source tab's referer must survive other tabs closing).
const REFERER_TTL_MS = 7 * 24 * 60 * 60 * 1000;
// Persist the captured referer per CDN host so the proxy can re-use it even
// after this capture tab closes. Keyed by host so a proxy for that host can
// look its referer up without knowing the source tab. Each entry is stamped
// with a capture time; entries older than the TTL are pruned opportunistically
// here (never on tab close).
// Persist the captured referer per CDN registrable domain so the proxy can
// re-use it even after the capture tab closes. Keyed by the host's registrable
// domain so a single provider referer covers every edge subdomain (a CDN serves
// manifests/segments from many subhosts that share one referer), and so a proxy
// for any sibling subhost can look it up without knowing the source tab. Each
// entry is stamped with a capture time; entries older than the TTL are pruned
// opportunistically here (never on tab close).
async function persistReferer(host, referer) {
if (!host || !referer) return;
const domain = registrableDomain(host);
if (!domain || !referer) return;
try {
const stored = await chrome.storage.local.get('goon_referers');
const map = stored['goon_referers'] || {};
const now = Date.now();
// Opportunistic per-host prune: drop entries older than the TTL.
// Opportunistic per-domain prune: drop entries older than the TTL.
for (const [h, entry] of Object.entries(map)) {
if (h !== host && (!entry || !entry.at || now - entry.at > REFERER_TTL_MS)) {
if (h !== domain && (!entry || !entry.at || now - entry.at > REFERER_TTL_MS)) {
delete map[h];
}
}
const current = map[host];
const current = map[domain];
if (current && current.referer === referer) return;
map[host] = { referer, at: now };
map[domain] = { referer, at: now };
await chrome.storage.local.set({ goon_referers: map });
} catch (e) {
console.warn('[goon-bg] persistReferer failed:', e && e.message ? e.message : e);
}
}
// Look up the referer captured for a host. '' when none has been captured
// (or the stored entry is malformed).
// Look up the referer captured for a host's registrable domain. '' when none
// has been captured (or the stored entry is malformed).
async function getReferer(host) {
if (!host) return '';
try {
const stored = await chrome.storage.local.get('goon_referers');
const entry = (stored['goon_referers'] || {})[host];
const entry = (stored['goon_referers'] || {})[registrableDomain(host)];
return entry && entry.referer ? entry.referer : '';
} catch {
return '';
}
}
// Capture the referer the real page sends for its video requests.
chrome.webRequest.onBeforeSendHeaders.addListener(
async (details) => {
if (details.tabId < 0) return;
if (!isVideoRequest(details.url)) return;
const host = hostOf(details.url);
if (!host) return;
const referer = readReferer(details.requestHeaders);
if (referer) {
// The browser already built a referer from the source page — use it.
await persistReferer(host, referer);
} else {
// No referer was sent; assume the page's origin is the expected referer.
await persistReferer(host, originFromHost(host));
}
},
{ urls: ['<all_urls>'] }
);
// Install (idempotently) a DNR rule that sets the captured referer for a given
// CDN host. The value comes from capture (or a fallback origin), never a
// hardcoded site. Scoped to the requested host so only proxy fetches for that
@@ -404,7 +418,8 @@ const refererRuleInstalled = new Map();
const refererRuleLocks = new Map();
async function ensureRefererRule(host, referer) {
if (!host || !referer) return;
const domain = registrableDomain(host);
if (!domain || !referer) return;
const api = chrome.declarativeNetRequest;
if (!api || typeof api.updateSessionRules !== 'function') {
console.warn('[goon-bg] declarativeNetRequest unavailable; skipping referer rule');
@@ -413,10 +428,10 @@ async function ensureRefererRule(host, referer) {
// Serialize per host so the idempotency check + updateSessionRules is atomic.
const run = (async () => {
if (refererRuleInstalled.has(host) && refererRuleInstalled.get(host) === referer) {
if (refererRuleInstalled.has(domain) && refererRuleInstalled.get(domain) === referer) {
return;
}
const ruleId = refererRuleIdFor(host);
const ruleId = refererRuleIdFor(domain);
try {
// Remove only THIS host's own rule id, then add it back scoped to the host.
await api.updateSessionRules({
@@ -435,17 +450,17 @@ async function ensureRefererRule(host, referer) {
condition: {
// Host + subdomains ONLY, so only proxy fetches for this host get the
// rewritten referer; the source page's own requests keep theirs.
requestDomains: [host],
requestDomains: [domain],
},
}],
});
refererRuleInstalled.set(host, referer);
console.log('[goon-bg] DNR referer rule installed for', host, '(' + ruleId + ')');
refererRuleInstalled.set(domain, referer);
console.log('[goon-bg] DNR referer rule installed for', domain, '(' + ruleId + ')');
} catch (e) {
console.warn('[goon-bg] ensureRefererRule failed:', e && e.message ? e.message : e);
}
})().finally(() => { refererRuleLocks.set(host, null); });
refererRuleLocks.set(host, run);
})().finally(() => { refererRuleLocks.set(domain, null); });
refererRuleLocks.set(domain, run);
return run;
}
@@ -533,10 +548,13 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
const proxyHost = hostOf(message.url);
if (proxyHost) {
// Prefer the provider's real referer captured from the live
// tab; fall back to a viewer-supplied or host-derived origin.
// tab (domain-aware), or the entry's scan-time referer the viewer sent.
// With neither, install no rule: a self-origin referer is
// guaranteed-wrong for hotlink protection, so we never fall
// back to one (it would just 412).
const captured = await getReferer(proxyHost);
const referer = captured || message.referer || originFromHost(proxyHost);
await ensureRefererRule(proxyHost, referer);
const referer = captured || message.referer || '';
if (referer) await ensureRefererRule(proxyHost, referer);
}
// hls.js resolves sub-playslists/segments relative to the master
// and can DROP the token query, handing us a token-less
@@ -571,6 +589,24 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
return true;
}
case 'GOON_GET_REFERERS': {
// Resolve the provider referer captured for each manifest host
// (domain-aware: one provider referer covers every edge subdomain).
// Used at scan time to bind the referer to a stored video entry so it
// survives the watch-later flow.
async function getReferers(message, sendResponse) {
const hosts = Array.isArray(message.hosts) ? message.hosts : [];
const referers = {};
for (const host of hosts) {
const r = await getReferer(host);
if (r) referers[host] = r;
}
sendResponse({ ok: true, referers });
}
getReferers(message, sendResponse);
return true;
}
default:
console.warn('[goon-plugin] Unknown message type:', message.type);
}
+27
View File
@@ -1147,6 +1147,7 @@ async function scanVideos(tabsToScan) {
}
const items = [...seen.values()];
await attachReferers(items);
const skipped = scrapeResults.filter(r => r === null).length;
log('scanVideos complete: scanned=', tabResults.length, 'skipped=', skipped, 'unique items=', items.length);
@@ -1157,6 +1158,31 @@ async function scanVideos(tabsToScan) {
return { items, scanned: tabResults.length, skipped };
}
/**
* Bind the provider referer the SW captured (domain-aware) to each item so it
* survives the watch-later flow and covers the sibling edge subdomains a segment
* may resolve to. Best-effort: playback still falls back to the SW's live store.
*/
async function attachReferers(items) {
const hostOf = (url) => { try { return new URL(url).host.toLowerCase(); } catch { return ''; } };
const hosts = [];
for (const it of items) {
const host = hostOf(it.hlsManifest || it.src || '');
if (host) hosts.push(host);
}
if (hosts.length === 0) return;
let referers = {};
try {
const resp = await chrome.runtime.sendMessage({ type: 'GOON_GET_REFERERS', hosts });
if (resp && resp.referers) referers = resp.referers;
} catch { /* referer is best-effort; the SW still resolves it at play time */ }
if (!Object.keys(referers).length) return;
for (const it of items) {
const host = hostOf(it.hlsManifest || it.src || '');
if (host && referers[host]) it.referer = referers[host];
}
}
/** Store video entries in chrome.storage.local. */
async function storeVideos(items) {
const stored = items.map(v => ({
@@ -1165,6 +1191,7 @@ async function storeVideos(items) {
tab: v.tab,
mediaType: v.mediaType || 'video',
hlsManifest: v.hlsManifest || undefined,
referer: v.referer || undefined,
}));
await chrome.storage.local.set({
[STORAGE_KEY]: stored,
+5
View File
@@ -33,6 +33,11 @@
"type": "string",
"enum": ["direct", "hls"],
"description": "Media delivery format. Defaults to 'direct' when omitted. Set to 'hls' for HTTP Live Streaming."
},
"referer": {
"type": ["string", "null"],
"format": "uri",
"description": "Provider Referer captured at scan time for the manifest's CDN; the proxy sends it so a referer-protecting CDN accepts sibling edge subdomains. Optional."
}
},
"additionalProperties": false,
+33 -9
View File
@@ -178,7 +178,7 @@ const panelsEl = document.getElementById('panels');
// 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, referer: originForUrl(url) }, (resp) => {
chrome.runtime.sendMessage({ type: 'GOON_PROXY_FETCH', url, referer: refererForUrl(url) }, (resp) => {
if (self._aborted) return;
const lastErr = chrome.runtime.lastError;
if (lastErr || !resp || !resp.ok) {
@@ -264,19 +264,34 @@ HlsLoader.canSupportLoader = function () { return true; };
// hardcode a particular CDN.
const PROXY_HOSTS_KEY = 'goon_proxy_hosts';
const proxyHosts = new Set();
// Provider referers bound to a stored entry at scan time, keyed by the entry's
// manifest registrable domain. A single provider referer covers every edge
// subdomain, so a segment on a sibling subhost resolves to the same referer.
const refererByDomain = new Map();
(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());
if (Array.isArray(hosts)) for (const h of hosts) proxyHosts.add(registrableDomainOf(String(h).toLowerCase()));
} catch { /* ignore */ }
})();
// 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 ''; }
// Registrable domain of a CDN host — the label a single provider referer
// applies to (a referer-protecting CDN serves from many edge subdomains that
// share one referer). Last-two-labels heuristic; mirrors the SW's copy.
function registrableDomainOf(host) {
if (!host) return '';
const parts = host.split('.');
if (parts.length < 2) return host;
return parts.slice(-2).join('.');
}
// The provider referer to send for a proxied request: the referer bound to the
// entry at scan time (domain-keyed), or '' when none was captured. No self-origin
// fallback — a self-origin referer is guaranteed-wrong for hotlink protection.
function refererForUrl(url) {
const ref = refererByDomain.get(registrableDomainOf(hostOfUrl(url)));
return ref || '';
}
// Whether a given video URL should be proxied: its host is a known
@@ -284,7 +299,7 @@ HlsLoader.canSupportLoader = function () { return true; };
function needsProxy(url) {
if (!url || typeof url !== 'string') return false;
const host = hostOfUrl(url);
return host ? proxyHosts.has(host) : false;
return host ? proxyHosts.has(registrableDomainOf(host)) : false;
}
// host of a URL (lowercased); '' when malformed.
@@ -1184,11 +1199,20 @@ async function init() {
// 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.
// Bind each entry's scan-time referer to its manifest's registrable
// domain, so the proxy sends it for every edge subdomain the provider
// serves from (segments often resolve to a sibling subhost).
for (const entry of videoUrls) {
const ref = entry && entry.referer;
if (!ref) continue;
const dom = registrableDomainOf(hostOfUrl(entry.src || entry.hlsManifest || ''));
if (dom) refererByDomain.set(dom, ref);
}
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));
if (!corsOk) blockedHosts.add(registrableDomainOf(hostOfUrl(url)));
console.log('[goon-viewer] CORS', corsOk ? 'OK' : 'BLOCKED:', url.substring(0, 120));
}
if (blockedHosts.size > 0) {