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.",
"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",

View File

@@ -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);
}

View File

@@ -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<string> {
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<string> {
}
/**
* 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
);

View File

@@ -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;

View File

@@ -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}
<input
type="checkbox"
title={_("optionsSiteWhitelistItemEnabled")}
bind:checked={item.isEnabled}
/>
{/if}
<div
class="whitelist__title"
on:dblclick={() => 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}

View File

@@ -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;

View File

@@ -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);