Add per-item toggles to site whitelist

This commit is contained in:
hensm
2022-08-19 18:43:02 +01:00
parent 41ada34c35
commit cbc039a355
7 changed files with 90 additions and 71 deletions

View File

@@ -393,6 +393,14 @@
"message": "If specified, a custom user agent string to use specifically for sites matching this pattern.", "message": "If specified, a custom user agent string to use specifically for sites matching this pattern.",
"description": "Whitelist item user agent option label." "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": { "optionsMirroringCategoryName": {
"message": "Screen/tab casting", "message": "Screen/tab casting",

View File

@@ -137,7 +137,7 @@ async function onMenuClicked(
const whitelist = await options.get("siteWhitelist"); const whitelist = await options.get("siteWhitelist");
if (!whitelist.find(item => item.pattern === pattern)) { if (!whitelist.find(item => item.pattern === pattern)) {
// Add to whitelist and update options // Add to whitelist and update options
whitelist.push({ pattern }); whitelist.push({ pattern, isEnabled: true });
await options.set("siteWhitelist", whitelist); await options.set("siteWhitelist", whitelist);
} }

View File

@@ -25,11 +25,12 @@ type OnBeforeRequestDetails = Parameters<
export interface WhitelistItemData { export interface WhitelistItemData {
pattern: string; pattern: string;
isEnabled: boolean;
isUserAgentDisabled?: boolean; isUserAgentDisabled?: boolean;
customUserAgent?: string; customUserAgent?: string;
} }
const originUrlCache: string[] = []; let matchPatterns: RemoteMatchPattern[] = [];
let platform: string; let platform: string;
let chromeUserAgent: string | undefined; let chromeUserAgent: string | undefined;
@@ -47,31 +48,25 @@ export async function initWhitelist() {
platform = (await browser.runtime.getPlatformInfo()).os; platform = (await browser.runtime.getPlatformInfo()).os;
chromeUserAgent = getChromeUserAgent(platform); chromeUserAgent = getChromeUserAgent(platform);
chromeUserAgentHybrid = getChromeUserAgent(platform, true); 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) { if (!chromeUserAgent) {
throw logger.error("Failed to get Chrome UA string"); throw logger.error("Failed to get Chrome UA string");
} }
customUserAgent = await options.get("siteWhitelistCustomUserAgent");
} }
// Register on first run // Register on first run
await registerSiteWhitelist(); await registerSiteWhitelist();
// Re-register when options change
options.addEventListener("changed", async ev => { options.addEventListener("changed", async ev => {
const alteredOpts = ev.detail; // Update custom UA on change
if (ev.detail.includes("siteWhitelistCustomUserAgent")) {
if (alteredOpts.includes("siteWhitelistCustomUserAgent")) {
customUserAgent = await options.get("siteWhitelistCustomUserAgent"); customUserAgent = await options.get("siteWhitelistCustomUserAgent");
} }
// Re-register whitelist on change
if ( if (
alteredOpts.includes("siteWhitelist") || ev.detail.includes("siteWhitelist") ||
alteredOpts.includes("siteWhitelistEnabled") ev.detail.includes("siteWhitelistEnabled")
) { ) {
unregisterSiteWhitelist(); unregisterSiteWhitelist();
registerSiteWhitelist(); registerSiteWhitelist();
@@ -93,7 +88,7 @@ function getUserAgent(url: string, host?: string): Optional<string> {
new RemoteMatchPattern(item.pattern).matches(url) new RemoteMatchPattern(item.pattern).matches(url)
); );
if (matchingItem) { if (matchingItem) {
if (matchingItem.isUserAgentDisabled) return; if (!matchingItem.isEnabled || matchingItem.isUserAgentDisabled) return;
return matchingItem.customUserAgent; return matchingItem.customUserAgent;
} }
@@ -104,10 +99,9 @@ function getUserAgent(url: string, host?: string): Optional<string> {
} }
/** /**
* Web apps usually only load the sender library and * Override User-Agent header for requests to whitelisted URLs. Sites
* provide cast functionality if the browser is detected * with Chromecast support will usually only load the Cast SDK if they
* as Chrome, so we should rewrite the User-Agent header * detect a Chrome user agent string.
* to reflect this on whitelisted sites.
*/ */
async function onWhitelistedBeforeSendHeaders( async function onWhitelistedBeforeSendHeaders(
details: OnBeforeSendHeadersDetails 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"); const host = details.requestHeaders.find(header => header.name === "Host");
for (const header of details.requestHeaders) { for (const header of details.requestHeaders) {
@@ -137,10 +127,9 @@ async function onWhitelistedBeforeSendHeaders(
} }
/** /**
* Requests from within child frames should also adopt * Override User-Agent header for requests from child frames of
* the modified User-Agent header to support embedded * whitelisted URLs to support embedded players on other origins (e.g.
* players on other origins (like CDN domains) when the * CDN domains).
* main site is whitelisted.
*/ */
function onWhitelistedChildBeforeSendHeaders( function onWhitelistedChildBeforeSendHeaders(
details: OnBeforeSendHeadersDetails details: OnBeforeSendHeadersDetails
@@ -150,55 +139,54 @@ function onWhitelistedChildBeforeSendHeaders(
} }
for (const ancestor of details.frameAncestors) { for (const ancestor of details.frameAncestors) {
if (originUrlCache.includes(ancestor.url)) { // If no matching patterns
const host = details.requestHeaders.find( if (!matchPatterns.some(pattern => pattern.matches(ancestor.url))) {
header => header.name === "Host" continue;
);
for (const header of details.requestHeaders) {
if (header.name === "User-Agent") {
header.value = getUserAgent(details.url, host?.value);
break;
}
}
return {
requestHeaders: details.requestHeaders
};
} }
// 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 * Handle requests to cast_sender.js SDK loader script and redirect to
* functions as a loader for the internal chrome-extension: * the extension's implementation.
* hosted script.
*
* We can redirect this and inject our own script to setup
* the API.
*/ */
async function onBeforeCastSDKRequest(details: OnBeforeRequestDetails) { async function onBeforeCastSDKRequest(details: OnBeforeRequestDetails) {
if (!details.originUrl || details.tabId === -1) { if (!details.originUrl || details.tabId === -1) {
return {}; return {};
} }
// Check against whitelist if enabled // Test against whitelist if enabled
if (await options.get("siteWhitelistEnabled")) { if (await options.get("siteWhitelistEnabled")) {
if (!details?.frameAncestors?.length) { /**
if (!originUrlCache.includes(details.originUrl)) { * Frame ancestor URLs (if present) or origin URL that the SDK
return {}; * is loaded from.
} */
} else { const urls = [
let hasMatchingAncestor = false; ...(details.frameAncestors?.map(ancestor => ancestor.url) ?? []),
for (const ancestor of details.frameAncestors) { details.originUrl
if (originUrlCache.includes(ancestor.url)) { ];
hasMatchingAncestor = true;
}
}
if (!hasMatchingAncestor) { // Allow request if no whitelist matches
return {}; if (
} !urls.some(url =>
matchPatterns.some(pattern => pattern.matches(url))
)
) {
return {};
} }
} }
@@ -228,12 +216,18 @@ async function registerSiteWhitelist() {
siteWhitelist = opts.siteWhitelist; siteWhitelist = opts.siteWhitelist;
siteWhitelistEnabled = opts.siteWhitelistEnabled; siteWhitelistEnabled = opts.siteWhitelistEnabled;
// Parse match patterns once
matchPatterns = siteWhitelist.map(
item => new RemoteMatchPattern(item.pattern)
);
browser.webRequest.onBeforeRequest.addListener( browser.webRequest.onBeforeRequest.addListener(
onBeforeCastSDKRequest, onBeforeCastSDKRequest,
{ urls: [CAST_LOADER_SCRIPT_URL, CAST_FRAMEWORK_LOADER_SCRIPT_URL] }, { urls: [CAST_LOADER_SCRIPT_URL, CAST_FRAMEWORK_LOADER_SCRIPT_URL] },
["blocking"] ["blocking"]
); );
// Skip whitelist request listeners if disabled or empty
if (!siteWhitelistEnabled || !siteWhitelist.length) { if (!siteWhitelistEnabled || !siteWhitelist.length) {
return; return;
} }
@@ -243,7 +237,9 @@ async function registerSiteWhitelist() {
{ {
// Filter for items with UA enabled // Filter for items with UA enabled
urls: siteWhitelist.flatMap(item => urls: siteWhitelist.flatMap(item =>
!item.isUserAgentDisabled ? [item.pattern] : [] item.isEnabled && !item.isUserAgentDisabled
? [item.pattern]
: []
) )
}, },
["blocking", "requestHeaders"] ["blocking", "requestHeaders"]
@@ -257,8 +253,6 @@ async function registerSiteWhitelist() {
} }
function unregisterSiteWhitelist() { function unregisterSiteWhitelist() {
originUrlCache.length = 0;
browser.webRequest.onBeforeSendHeaders.removeListener( browser.webRequest.onBeforeSendHeaders.removeListener(
onWhitelistedBeforeSendHeaders onWhitelistedBeforeSendHeaders
); );

View File

@@ -41,7 +41,7 @@ export default {
receiverSelectorCloseIfFocusLost: true, receiverSelectorCloseIfFocusLost: true,
receiverSelectorWaitForConnection: true, receiverSelectorWaitForConnection: true,
siteWhitelistEnabled: true, siteWhitelistEnabled: true,
siteWhitelist: [{ pattern: "https://www.netflix.com/*" }], siteWhitelist: [{ pattern: "https://www.netflix.com/*", isEnabled: true }],
siteWhitelistCustomUserAgent: "", siteWhitelistCustomUserAgent: "",
showAdvancedOptions: false showAdvancedOptions: false
} as Options; } as Options;

View File

@@ -98,10 +98,13 @@
if (isEditing) return; if (isEditing) return;
if (knownAppToAdd?.matches) { if (knownAppToAdd?.matches) {
items = [...items, { pattern: knownAppToAdd.matches }]; items = [
...items,
{ pattern: knownAppToAdd.matches, isEnabled: true }
];
knownAppToAdd = null; knownAppToAdd = null;
} else { } else {
items = [...items, { pattern: "" }]; items = [...items, { pattern: "", isEnabled: true }];
beginEditing(items.length - 1); beginEditing(items.length - 1);
} }
} }
@@ -129,7 +132,17 @@
class="whitelist__item" class="whitelist__item"
class:whitelist__item--selected={isEditingItem} class:whitelist__item--selected={isEditingItem}
class:whitelist__item--expanded={isItemExpanded} class:whitelist__item--expanded={isItemExpanded}
class:whitelist__item--disabled={!item.isEnabled}
class:whitelist__item--editing={isEditingItem}
> >
{#if !isEditingItem}
<input
type="checkbox"
title={_("optionsSiteWhitelistItemEnabled")}
bind:checked={item.isEnabled}
/>
{/if}
<div <div
class="whitelist__title" class="whitelist__title"
on:dblclick={() => beginEditing(i)} on:dblclick={() => beginEditing(i)}
@@ -140,6 +153,7 @@
class="whitelist__input-pattern" class="whitelist__input-pattern"
pattern={REMOTE_MATCH_PATTERN_REGEX.source} pattern={REMOTE_MATCH_PATTERN_REGEX.source}
required required
title={_("optionsSiteWhitelistItemPattern")}
bind:this={editingInput} bind:this={editingInput}
bind:value={editingValue} bind:value={editingValue}
on:input={onEditInput} on:input={onEditInput}

View File

@@ -367,6 +367,9 @@ button.ghost:not(:hover) {
.whitelist__item--selected { .whitelist__item--selected {
background-color: var(--blue-50-a30) !important; background-color: var(--blue-50-a30) !important;
} }
.whitelist__item--disabled:not(.whitelist__item--editing) .whitelist__title {
opacity: 0.65;
}
.whitelist__title { .whitelist__title {
display: flex; display: flex;

View File

@@ -228,7 +228,7 @@
const whitelist = await options.get("siteWhitelist"); const whitelist = await options.get("siteWhitelist");
if (!whitelist.find(item => item.pattern === app.matches)) { 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 options.set("siteWhitelist", whitelist);
await browser.tabs.reload(pageInfo.tabId); await browser.tabs.reload(pageInfo.tabId);