From cbc039a355846255829e034600bf14ecac94e137 Mon Sep 17 00:00:00 2001 From: hensm Date: Fri, 19 Aug 2022 18:43:02 +0100 Subject: [PATCH] Add per-item toggles to site whitelist --- ext/src/_locales/en/messages.json | 8 ++ ext/src/background/menus.ts | 2 +- ext/src/background/whitelist.ts | 126 +++++++++++++--------------- ext/src/defaultOptions.ts | 2 +- ext/src/ui/options/Whitelist.svelte | 18 +++- ext/src/ui/options/styles/index.css | 3 + ext/src/ui/popup/Popup.svelte | 2 +- 7 files changed, 90 insertions(+), 71 deletions(-) diff --git a/ext/src/_locales/en/messages.json b/ext/src/_locales/en/messages.json index 3f73d6b..2942118 100755 --- a/ext/src/_locales/en/messages.json +++ b/ext/src/_locales/en/messages.json @@ -393,6 +393,14 @@ "message": "If specified, a custom user agent string to use specifically for sites matching this pattern.", "description": "Whitelist item user agent option label." }, + "optionsSiteWhitelistItemEnabled": { + "message": "Enable whitelist item", + "description": "Whitelist item enabled checkbox title." + }, + "optionsSiteWhitelistItemPattern": { + "message": "Match pattern", + "description": "Whitelist item pattern edit input title." + }, "optionsMirroringCategoryName": { "message": "Screen/tab casting", diff --git a/ext/src/background/menus.ts b/ext/src/background/menus.ts index b7a289f..844cdf6 100644 --- a/ext/src/background/menus.ts +++ b/ext/src/background/menus.ts @@ -137,7 +137,7 @@ async function onMenuClicked( const whitelist = await options.get("siteWhitelist"); if (!whitelist.find(item => item.pattern === pattern)) { // Add to whitelist and update options - whitelist.push({ pattern }); + whitelist.push({ pattern, isEnabled: true }); await options.set("siteWhitelist", whitelist); } diff --git a/ext/src/background/whitelist.ts b/ext/src/background/whitelist.ts index 18cd009..12ec9e4 100644 --- a/ext/src/background/whitelist.ts +++ b/ext/src/background/whitelist.ts @@ -25,11 +25,12 @@ type OnBeforeRequestDetails = Parameters< export interface WhitelistItemData { pattern: string; + isEnabled: boolean; isUserAgentDisabled?: boolean; customUserAgent?: string; } -const originUrlCache: string[] = []; +let matchPatterns: RemoteMatchPattern[] = []; let platform: string; let chromeUserAgent: string | undefined; @@ -47,31 +48,25 @@ export async function initWhitelist() { platform = (await browser.runtime.getPlatformInfo()).os; chromeUserAgent = getChromeUserAgent(platform); chromeUserAgentHybrid = getChromeUserAgent(platform, true); - - customUserAgent = await options.get("siteWhitelistCustomUserAgent"); - - /** - * 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"); } + + customUserAgent = await options.get("siteWhitelistCustomUserAgent"); } // Register on first run await registerSiteWhitelist(); - // Re-register when options change options.addEventListener("changed", async ev => { - const alteredOpts = ev.detail; - - if (alteredOpts.includes("siteWhitelistCustomUserAgent")) { + // Update custom UA on change + if (ev.detail.includes("siteWhitelistCustomUserAgent")) { customUserAgent = await options.get("siteWhitelistCustomUserAgent"); } + // Re-register whitelist on change if ( - alteredOpts.includes("siteWhitelist") || - alteredOpts.includes("siteWhitelistEnabled") + ev.detail.includes("siteWhitelist") || + ev.detail.includes("siteWhitelistEnabled") ) { unregisterSiteWhitelist(); registerSiteWhitelist(); @@ -93,7 +88,7 @@ function getUserAgent(url: string, host?: string): Optional { new RemoteMatchPattern(item.pattern).matches(url) ); if (matchingItem) { - if (matchingItem.isUserAgentDisabled) return; + if (!matchingItem.isEnabled || matchingItem.isUserAgentDisabled) return; return matchingItem.customUserAgent; } @@ -104,10 +99,9 @@ function getUserAgent(url: string, host?: string): Optional { } /** - * 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. + * Override User-Agent header for requests to whitelisted URLs. Sites + * with Chromecast support will usually only load the Cast SDK if they + * detect a Chrome user agent string. */ async function onWhitelistedBeforeSendHeaders( details: OnBeforeSendHeadersDetails @@ -118,10 +112,6 @@ async function onWhitelistedBeforeSendHeaders( ); } - 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) { @@ -137,10 +127,9 @@ async function onWhitelistedBeforeSendHeaders( } /** - * 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. + * Override User-Agent header for requests from child frames of + * whitelisted URLs to support embedded players on other origins (e.g. + * CDN domains). */ function onWhitelistedChildBeforeSendHeaders( details: OnBeforeSendHeadersDetails @@ -150,55 +139,54 @@ function onWhitelistedChildBeforeSendHeaders( } 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 = getUserAgent(details.url, host?.value); - break; - } - } - - return { - requestHeaders: details.requestHeaders - }; + // If no matching patterns + if (!matchPatterns.some(pattern => pattern.matches(ancestor.url))) { + continue; } + + // Override User-Agent header + const requestHeaders = details.requestHeaders; + for (const header of requestHeaders) { + if (header.name === "User-Agent") { + const host = requestHeaders.find( + header => header.name === "Host" + ); + header.value = getUserAgent(details.url, host?.value); + break; + } + } + + return { 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. + * Handle requests to cast_sender.js SDK loader script and redirect to + * the extension's implementation. */ async function onBeforeCastSDKRequest(details: OnBeforeRequestDetails) { if (!details.originUrl || details.tabId === -1) { return {}; } - // Check against whitelist if enabled + // Test against whitelist if enabled if (await options.get("siteWhitelistEnabled")) { - 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; - } - } + /** + * Frame ancestor URLs (if present) or origin URL that the SDK + * is loaded from. + */ + const urls = [ + ...(details.frameAncestors?.map(ancestor => ancestor.url) ?? []), + details.originUrl + ]; - if (!hasMatchingAncestor) { - return {}; - } + // Allow request if no whitelist matches + if ( + !urls.some(url => + matchPatterns.some(pattern => pattern.matches(url)) + ) + ) { + return {}; } } @@ -228,12 +216,18 @@ async function registerSiteWhitelist() { siteWhitelist = opts.siteWhitelist; siteWhitelistEnabled = opts.siteWhitelistEnabled; + // Parse match patterns once + matchPatterns = siteWhitelist.map( + item => new RemoteMatchPattern(item.pattern) + ); + browser.webRequest.onBeforeRequest.addListener( onBeforeCastSDKRequest, { urls: [CAST_LOADER_SCRIPT_URL, CAST_FRAMEWORK_LOADER_SCRIPT_URL] }, ["blocking"] ); + // Skip whitelist request listeners if disabled or empty if (!siteWhitelistEnabled || !siteWhitelist.length) { return; } @@ -243,7 +237,9 @@ async function registerSiteWhitelist() { { // Filter for items with UA enabled urls: siteWhitelist.flatMap(item => - !item.isUserAgentDisabled ? [item.pattern] : [] + item.isEnabled && !item.isUserAgentDisabled + ? [item.pattern] + : [] ) }, ["blocking", "requestHeaders"] @@ -257,8 +253,6 @@ async function registerSiteWhitelist() { } function unregisterSiteWhitelist() { - originUrlCache.length = 0; - browser.webRequest.onBeforeSendHeaders.removeListener( onWhitelistedBeforeSendHeaders ); diff --git a/ext/src/defaultOptions.ts b/ext/src/defaultOptions.ts index dcba7d0..483bd1a 100644 --- a/ext/src/defaultOptions.ts +++ b/ext/src/defaultOptions.ts @@ -41,7 +41,7 @@ export default { receiverSelectorCloseIfFocusLost: true, receiverSelectorWaitForConnection: true, siteWhitelistEnabled: true, - siteWhitelist: [{ pattern: "https://www.netflix.com/*" }], + siteWhitelist: [{ pattern: "https://www.netflix.com/*", isEnabled: true }], siteWhitelistCustomUserAgent: "", showAdvancedOptions: false } as Options; diff --git a/ext/src/ui/options/Whitelist.svelte b/ext/src/ui/options/Whitelist.svelte index ba8e7cb..9a0e762 100644 --- a/ext/src/ui/options/Whitelist.svelte +++ b/ext/src/ui/options/Whitelist.svelte @@ -98,10 +98,13 @@ if (isEditing) return; if (knownAppToAdd?.matches) { - items = [...items, { pattern: knownAppToAdd.matches }]; + items = [ + ...items, + { pattern: knownAppToAdd.matches, isEnabled: true } + ]; knownAppToAdd = null; } else { - items = [...items, { pattern: "" }]; + items = [...items, { pattern: "", isEnabled: true }]; beginEditing(items.length - 1); } } @@ -129,7 +132,17 @@ class="whitelist__item" class:whitelist__item--selected={isEditingItem} class:whitelist__item--expanded={isItemExpanded} + class:whitelist__item--disabled={!item.isEnabled} + class:whitelist__item--editing={isEditingItem} > + {#if !isEditingItem} + + {/if} +
beginEditing(i)} @@ -140,6 +153,7 @@ class="whitelist__input-pattern" pattern={REMOTE_MATCH_PATTERN_REGEX.source} required + title={_("optionsSiteWhitelistItemPattern")} bind:this={editingInput} bind:value={editingValue} on:input={onEditInput} diff --git a/ext/src/ui/options/styles/index.css b/ext/src/ui/options/styles/index.css index d531c63..6ef7c9f 100644 --- a/ext/src/ui/options/styles/index.css +++ b/ext/src/ui/options/styles/index.css @@ -367,6 +367,9 @@ button.ghost:not(:hover) { .whitelist__item--selected { background-color: var(--blue-50-a30) !important; } +.whitelist__item--disabled:not(.whitelist__item--editing) .whitelist__title { + opacity: 0.65; +} .whitelist__title { display: flex; diff --git a/ext/src/ui/popup/Popup.svelte b/ext/src/ui/popup/Popup.svelte index ae700da..ddfb12b 100644 --- a/ext/src/ui/popup/Popup.svelte +++ b/ext/src/ui/popup/Popup.svelte @@ -228,7 +228,7 @@ const whitelist = await options.get("siteWhitelist"); if (!whitelist.find(item => item.pattern === app.matches)) { - whitelist.push({ pattern: app.matches }); + whitelist.push({ pattern: app.matches, isEnabled: true }); await options.set("siteWhitelist", whitelist); await browser.tabs.reload(pageInfo.tabId);