From 378ed90b2134e5c8249daa48bdf49b27fd0a9eb1 Mon Sep 17 00:00:00 2001 From: hensm Date: Sat, 24 Apr 2021 08:01:29 +0100 Subject: [PATCH] Refactor extension whitelist module --- ext/src/background/whitelist.ts | 362 +++++++++++++++++--------------- 1 file changed, 189 insertions(+), 173 deletions(-) diff --git a/ext/src/background/whitelist.ts b/ext/src/background/whitelist.ts index 31bbcd9..dd21abf 100644 --- a/ext/src/background/whitelist.ts +++ b/ext/src/background/whitelist.ts @@ -9,184 +9,39 @@ import { CAST_FRAMEWORK_LOADER_SCRIPT_URL , CAST_LOADER_SCRIPT_URL } from "../lib/endpoints"; +// Missing on @types/firefox-webext-browser +type OnBeforeSendHeadersDetails = Parameters[0]>[0] & { + frameAncestors?: Array<{ url: string, frameId: number }> +}; +type OnBeforeRequestDetails = Parameters[0]>[0] & { + frameAncestors?: Array<{ url: string, frameId: number }> +}; + + +const originUrlCache: string[] = []; + +let platform: string; +let chromeUserAgent: string | undefined; +let chromeUserAgentHybrid: string | undefined; + export async function initWhitelist () { logger.info("init (whitelist)"); - // Missing on @types/firefox-webext-browser - type OnBeforeSendHeadersDetails = Parameters[0]>[0] & { - frameAncestors?: Array<{ url: string, frameId: number }> - }; - type OnBeforeRequestDetails = Parameters[0]>[0] & { - frameAncestors?: Array<{ url: string, frameId: number }> - }; + if (!platform) { + // TODO: Allow hybrid UA to be configurable + platform = (await browser.runtime.getPlatformInfo()).os; + chromeUserAgent = getChromeUserAgent(platform); + chromeUserAgentHybrid = getChromeUserAgent(platform, true); - - const originUrlCache: string[] = []; - - // TODO: Allow hybrid UA to be configurable - const platform = (await browser.runtime.getPlatformInfo()).os; - const chromeUserAgent = getChromeUserAgent(platform); - - /** - * Web apps usually only load the sender library and - * provide cast functionality if the browser is detected - * as Chrome, so we should rewrite the User-Agent header - * to reflect this on whitelisted sites. - */ - async function onWhitelistedBeforeSendHeaders ( - details: OnBeforeSendHeadersDetails) { - - if (!details.requestHeaders) { - throw logger.error("OnBeforeSendHeaders handler details missing requestHeaders."); + /** + * If a UA string can't be obtained, don't bother continuing + * extension initialization + */ + if (!chromeUserAgent) { + throw logger.error("Failed to get Chrome UA string"); } - - if (details.originUrl && !originUrlCache.includes(details.originUrl)) { - originUrlCache.push(details.originUrl); - } - - const host = details.requestHeaders.find( - header => header.name === "Host"); - - for (const header of details.requestHeaders) { - if (header.name === "User-Agent") { - header.value = host?.value === "www.youtube.com" - ? getChromeUserAgent(platform, true) - : chromeUserAgent; - break; - } - } - - return { - requestHeaders: details.requestHeaders - }; - } - - /** - * Requests from within child frames should also adopt - * the modified User-Agent header to support embedded - * players on other origins (like CDN domains) when the - * main site is whitelisted. - */ - function onWhitelistedChildBeforeSendHeaders ( - details: OnBeforeSendHeadersDetails) { - - if (!details.requestHeaders || !details.frameAncestors) { - return; - } - - for (const ancestor of details.frameAncestors) { - if (originUrlCache.includes(ancestor.url)) { - const host = details.requestHeaders.find( - header => header.name === "Host"); - - for (const header of details.requestHeaders) { - if (header.name === "User-Agent") { - header.value = host?.value === "www.youtube.com" - ? getChromeUserAgent(platform, true) - : chromeUserAgent; - break; - } - } - - return { - requestHeaders: details.requestHeaders - }; - } - } - } - - - /** - * Sender applications load a cast_sender.js script that - * functions as a loader for the internal chrome-extension: - * hosted script. - * - * We can redirect this and inject our own script to setup - * the API shim. - */ - async function onBeforeCastSDKRequest (details: OnBeforeRequestDetails) { - if (!details.originUrl) { - return {}; - } - - // Check against whitelist if restricted mode is enabled - if (await options.get("userAgentWhitelistRestrictedEnabled")) { - if (!details?.frameAncestors?.length) { - if (!originUrlCache.includes(details.originUrl)) { - return {}; - } - } else { - let hasMatchingAncestor = false; - for (const ancestor of details.frameAncestors) { - if (originUrlCache.includes(ancestor.url)) { - hasMatchingAncestor = true; - } - } - - if (!hasMatchingAncestor) { - return {}; - } - } - } - - await browser.tabs.executeScript(details.tabId, { - code: ` - window.isFramework = ${ - details.url === CAST_FRAMEWORK_LOADER_SCRIPT_URL}; - ` - , frameId: details.frameId - , runAt: "document_start" - }); - - await browser.tabs.executeScript(details.tabId, { - file: "shim/contentBridge.js" - , frameId: details.frameId - , runAt: "document_start" - }); - - return { - redirectUrl: browser.runtime.getURL("shim/bundle.js") - }; - } - - - async function registerUserAgentWhitelist () { - const { userAgentWhitelist - , userAgentWhitelistEnabled } = await options.getAll(); - - browser.webRequest.onBeforeRequest.addListener( - onBeforeCastSDKRequest - , { urls: [ - CAST_LOADER_SCRIPT_URL - , CAST_FRAMEWORK_LOADER_SCRIPT_URL ]} - , [ "blocking" ]); - - if (!userAgentWhitelistEnabled || !userAgentWhitelist.length) { - return; - } - - browser.webRequest.onBeforeSendHeaders.addListener( - onWhitelistedBeforeSendHeaders - , { urls: userAgentWhitelist } - , [ "blocking", "requestHeaders" ]); - - browser.webRequest.onBeforeSendHeaders.addListener( - onWhitelistedChildBeforeSendHeaders - , { urls: [ "" ] } - , [ "blocking", "requestHeaders" ]); - } - - function unregisterUserAgentWhitelist () { - originUrlCache.length = 0; - - browser.webRequest.onBeforeSendHeaders - .removeListener(onWhitelistedBeforeSendHeaders); - browser.webRequest.onBeforeSendHeaders - .removeListener(onWhitelistedChildBeforeSendHeaders); - browser.webRequest.onBeforeRequest - .removeListener(onBeforeCastSDKRequest); } // Register on first run @@ -203,3 +58,164 @@ export async function initWhitelist () { } }); } + + +/** + * Web apps usually only load the sender library and + * provide cast functionality if the browser is detected + * as Chrome, so we should rewrite the User-Agent header + * to reflect this on whitelisted sites. + */ + async function onWhitelistedBeforeSendHeaders ( + details: OnBeforeSendHeadersDetails) { + + if (!details.requestHeaders) { + throw logger.error("OnBeforeSendHeaders handler details missing requestHeaders."); + } + + if (details.originUrl && !originUrlCache.includes(details.originUrl)) { + originUrlCache.push(details.originUrl); + } + + const host = details.requestHeaders.find( + header => header.name === "Host"); + + for (const header of details.requestHeaders) { + if (header.name === "User-Agent") { + header.value = host?.value === "www.youtube.com" + ? chromeUserAgentHybrid + : chromeUserAgent; + break; + } + } + + return { + requestHeaders: details.requestHeaders + }; +} + +/** + * Requests from within child frames should also adopt + * the modified User-Agent header to support embedded + * players on other origins (like CDN domains) when the + * main site is whitelisted. + */ +function onWhitelistedChildBeforeSendHeaders ( + details: OnBeforeSendHeadersDetails) { + + if (!details.requestHeaders || !details.frameAncestors) { + return; + } + + for (const ancestor of details.frameAncestors) { + if (originUrlCache.includes(ancestor.url)) { + const host = details.requestHeaders.find( + header => header.name === "Host"); + + for (const header of details.requestHeaders) { + if (header.name === "User-Agent") { + header.value = host?.value === "www.youtube.com" + ? chromeUserAgentHybrid + : chromeUserAgent; + break; + } + } + + return { + requestHeaders: details.requestHeaders + }; + } + } +} + + +/** + * Sender applications load a cast_sender.js script that + * functions as a loader for the internal chrome-extension: + * hosted script. + * + * We can redirect this and inject our own script to setup + * the API shim. + */ +async function onBeforeCastSDKRequest (details: OnBeforeRequestDetails) { + if (!details.originUrl) { + return {}; + } + + // Check against whitelist if restricted mode is enabled + if (await options.get("userAgentWhitelistRestrictedEnabled")) { + if (!details?.frameAncestors?.length) { + if (!originUrlCache.includes(details.originUrl)) { + return {}; + } + } else { + let hasMatchingAncestor = false; + for (const ancestor of details.frameAncestors) { + if (originUrlCache.includes(ancestor.url)) { + hasMatchingAncestor = true; + } + } + + if (!hasMatchingAncestor) { + return {}; + } + } + } + + await browser.tabs.executeScript(details.tabId, { + code: ` + window.isFramework = ${ + details.url === CAST_FRAMEWORK_LOADER_SCRIPT_URL}; + ` + , frameId: details.frameId + , runAt: "document_start" + }); + + await browser.tabs.executeScript(details.tabId, { + file: "shim/contentBridge.js" + , frameId: details.frameId + , runAt: "document_start" + }); + + return { + redirectUrl: browser.runtime.getURL("shim/bundle.js") + }; +} + + +async function registerUserAgentWhitelist () { + const { userAgentWhitelist + , userAgentWhitelistEnabled } = await options.getAll(); + + browser.webRequest.onBeforeRequest.addListener( + onBeforeCastSDKRequest + , { urls: [ + CAST_LOADER_SCRIPT_URL + , CAST_FRAMEWORK_LOADER_SCRIPT_URL ]} + , [ "blocking" ]); + + if (!userAgentWhitelistEnabled || !userAgentWhitelist.length) { + return; + } + + browser.webRequest.onBeforeSendHeaders.addListener( + onWhitelistedBeforeSendHeaders + , { urls: userAgentWhitelist } + , [ "blocking", "requestHeaders" ]); + + browser.webRequest.onBeforeSendHeaders.addListener( + onWhitelistedChildBeforeSendHeaders + , { urls: [ "" ] } + , [ "blocking", "requestHeaders" ]); +} + +function unregisterUserAgentWhitelist () { + originUrlCache.length = 0; + + browser.webRequest.onBeforeSendHeaders + .removeListener(onWhitelistedBeforeSendHeaders); + browser.webRequest.onBeforeSendHeaders + .removeListener(onWhitelistedChildBeforeSendHeaders); + browser.webRequest.onBeforeRequest + .removeListener(onBeforeCastSDKRequest); +}