diff --git a/src/background/background.js b/src/background/background.js index 4789a18..7f374cc 100644 --- a/src/background/background.js +++ b/src/background/background.js @@ -176,6 +176,37 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { return true; } + case 'GOON_PROXY_FETCH': { + // Proxy a fetch request through the service worker (bypasses CORS). + // Returns base64-encoded response data. Used by the viewer when + // extension-origin requests to video CDNs are blocked by CORS. + (async () => { + const url = message.url; + try { + const resp = await fetch(url); + if (!resp.ok) { + sendResponse({ ok: false, status: resp.status }); + return; + } + const buffer = await resp.arrayBuffer(); + const bytes = new Uint8Array(buffer); + // Convert to base64 (btoa can only handle strings up to ~65k chars) + let binary = ''; + const CHUNK_SIZE = 32768; + for (let i = 0; i < bytes.length; i += CHUNK_SIZE) { + const chunk = bytes.subarray(i, i + CHUNK_SIZE); + binary += String.fromCharCode.apply(null, Array.from(chunk)); + } + const base64 = btoa(binary); + sendResponse({ ok: true, data: base64, mimeType: resp.headers.get('content-type') || '' }); + } catch(e) { + console.warn('[goon-bg] proxyFetch failed:', e && e.message ? e.message : e); + sendResponse({ ok: false, error: e && e.message ? e.message : String(e) }); + } + })(); + return true; + } + default: console.warn('[goon-plugin] Unknown message type:', message.type); } diff --git a/src/viewer/viewer.js b/src/viewer/viewer.js index 6d46a00..c4b9be7 100644 --- a/src/viewer/viewer.js +++ b/src/viewer/viewer.js @@ -52,6 +52,134 @@ const gridN = parseInt(params.get('grid'), 10) || 2; const tags = params.get('tags') ? JSON.parse(params.get('tags')) : []; 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) and fetches everything else natively. + * Implements hls.js Loader interface: load(config), abort(). + */ +function HlsLoader(config) { + this._config = config; + this._xhr = null; +} + +HlsLoader.prototype.abort = function () { + if (this._xhr) { this._xhr.abort(); this._xhr = null; } +}; + +HlsLoader.prototype.load = function (config) { + this.abort(); + this._loaderContext = config.type + ': ' + (config.url || '').substring(0, 100); + this._onSuccess = config.onSuccess; + this._onError = config.onError; + this._onTimeout = config.onTimeout; + this._timeout = config.timeout || 10000; + + const url = config.url; + const needProxy = url.includes('phncdn') || url.includes('pornhubcdn'); + + if (needProxy) { + // Use service worker proxy to bypass CORS + console.log('[goon-viewer] Proxying via SW:', url.substring(0, 120)); + chrome.runtime.sendMessage({ type: 'GOON_PROXY_FETCH', url }, (resp) => { + if (chrome.runtime.lastError || !resp || !resp.ok) { + const err = new Error('Proxy fetch failed: ' + (chrome.runtime.lastError?.message || resp?.error || 'unknown')); + console.error('[goon-viewer] Proxy error:', err.message); + this._onError(config, { url, context: this._loaderContext, body: null, stats: { trequest: Date.now(), tfirst: 0, tload: Date.now(), loadingPeriod: 0 }, error: err }); + return; + } + // Decode base64 → ArrayBuffer + let binary = ''; + const data = resp.data || ''; + const CHUNK = 32768; + for (let i = 0; i < data.length; i += CHUNK) { + binary += String.fromCharCode.apply(null, Array.from(data.subarray?.(i, i + CHUNK) || new Uint8Array().subarray(0, Math.min(CHUNK, data.length - i)))); + } + // Simpler approach: use atob on the full string + try { + binary = atob(data); + } catch {} + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); + + this._onSuccess(config, { response: { headers: {}, body: bytes.buffer }, trequest: Date.now(), tfirst: Date.now(), tload: Date.now(), loadingPeriod: Date.now() - config.trequest }); + }); + } else { + // Native XHR for non-proxy URLs + const xhr = new XMLHttpRequest(); + this._xhr = xhr; + xhr.open('GET', url, true); + xhr.withCredentials = false; + xhr.onload = () => { + if (xhr.status >= 200 && xhr.status < 300) { + this._onSuccess(config, { response: { headers: xhr.getAllResponseHeaders(), body: xhr.response }, trequest: config.trequest, tfirst: Date.now(), tload: Date.now(), loadingPeriod: Date.now() - config.trequest }); + } else { + this._onError(config, { url, context: this._loaderContext, body: null, stats: { trequest: config.trequest, tfirst: 0, tload: Date.now(), loadingPeriod: 0 }, error: new Error('HTTP ' + xhr.status) }); + } + }; + xhr.onerror = () => { + this._onError(config, { url, context: this._loaderContext, body: null, stats: { trequest: config.trequest, tfirst: 0, tload: Date.now(), loadingPeriod: 0 }, error: new Error('XHR network error') }); + }; + xhr.ontimeout = () => { + this._onTimeout(config, { url, context: this._loaderContext, stats: { trequest: config.trequest, tfirst: 0, tload: Date.now(), loadingPeriod: 0 } }); + }; + xhr.timeout = this._timeout; + xhr.responseType = needProxy ? 'arraybuffer' : 'arraybuffer'; + xhr.send(); + } +}; + +HlsLoader.prototype.destroy = function () { this.abort(); }; +HlsLoader.canSupportLoader = function () { return true; }; + +// === CORS diagnostics — check if phncdn.com allows extension-origin requests === +(async function checkCors() { + 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 }); + if (resp && resp.data && resp.ok) { + // 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) { + 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'))); +} + +/** 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 { + return false; + } +} + /** Track all Hls instances so we can destroy them on unload. */ const hlsInstances = new Map(); @@ -682,9 +810,23 @@ 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'); + } + destroyHls(video); - const hls = new window.Hls(HLS_CONFIG); + const config = Object.assign({}, HLS_CONFIG); + + if (needProxy) { + // Custom fetch loader that uses the service worker proxy for phncdn URLs. + config.loader = window.Hls.DefaultConfig.loader || HlsLoader; + } + + const hls = new window.Hls(config); hlsInstances.set(video, hls); hls.on(window.Hls.Events.MANIFEST_PARSED, () => { @@ -810,6 +952,36 @@ async function init() { overlay.style.cssText = 'position:absolute;top:0;left:0;right:0;background:rgba(0,0,0,0.85);color:#fff;padding:8px 16px;font-family:sans-serif;font-size:14px;z-index:999;'; document.body.style.position = 'relative'; document.body.insertBefore(overlay, document.body.firstChild); + + // === DIAGNOSTIC: Check storage + CORS === + console.log('[goon-viewer] Viewer ID:', id, 'Grid:', gridN + 'x' + gridN); + console.log('[goon-viewer] Raw stored data:', JSON.stringify(videoUrls)); + + if (videoUrls.length > 0) { + for (const entry of videoUrls) { + const src = (entry.src || '').substring(0, 120); + console.log('[goon-viewer] Entry:', { + src: src, + mediaType: entry.mediaType, + hlsManifest: (entry.hlsManifest || '').substring(0, 120), + poster: (entry.poster || '').substring(0, 80), + }); + } + + // 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)); + } + + // 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); + } else { + console.warn('[goon-viewer] No video entries in storage for ID:', id, '— data may have been cleared or never saved'); + } + overlay.textContent = `Found ${videoUrls.length} video URL(s) in ${gridN}x${gridN} grid`; statusElRef = overlay;