Add restricted whitelist mode

This commit is contained in:
hensm
2020-06-28 00:14:36 +01:00
parent 5cf92df3ba
commit 18204f225f
6 changed files with 174 additions and 95 deletions

View File

@@ -20,3 +20,21 @@ Compatibility reports are always helpful. Use the "Compatibility Report" issue t
Either fork and edit the messages files manually or to easily add/edit localizations, use the web-ext-translator tool: Either fork and edit the messages files manually or to easily add/edit localizations, use the web-ext-translator tool:
https://lusito.github.io/web-ext-translator/?gh=https://github.com/hensm/fx_cast/ https://lusito.github.io/web-ext-translator/?gh=https://github.com/hensm/fx_cast/
Missing/outdated strings:
* `de`
* `optionsBridgeBackupEnabled`
* `optionsUserAgentWhitelistRestrictedEnabled`
* `optionsUserAgentWhitelistRestrictedEnabledDescription`
* `es`
* `optionsBridgeBackupEnabled`
* `optionsUserAgentWhitelistRestrictedEnabled`
* `optionsUserAgentWhitelistRestrictedEnabledDescription`
* `nl`
* `optionsBridgeBackupEnabled`
* `optionsUserAgentWhitelistRestrictedEnabled`
* `optionsUserAgentWhitelistRestrictedEnabledDescription`

View File

@@ -314,6 +314,14 @@
"message": "Enable site whitelist" "message": "Enable site whitelist"
, "description": "Whitelist enabled checkbox label." , "description": "Whitelist enabled checkbox label."
} }
, "optionsUserAgentWhitelistRestrictedEnabled": {
"message": "Enable restricted mode"
, "description": "Whitelist restricted mode enabled checkbox label."
}
, "optionsUserAgentWhitelistRestrictedEnabledDescription": {
"message": "Also apply whitelist restrictions to sites attempting to load cast functionality regardless of the current user agent."
, "description": "Whitelist restricted mode enabled description."
}
, "optionsUserAgentWhitelistContent": { , "optionsUserAgentWhitelistContent": {
"message": "Match patterns (newline-separated):" "message": "Match patterns (newline-separated):"
, "description": "Match patterns editor widget label." , "description": "Match patterns editor widget label."

View File

@@ -423,13 +423,119 @@ async function initMenus () {
} }
async function initRequestListener () { async function initWhitelist () {
logger.info("init (request listener)"); logger.info("init (whitelist)");
// Missing on @types/firefox-webext-browser
type OnBeforeSendHeadersDetails = Parameters<Parameters<
typeof browser.webRequest.onBeforeSendHeaders.addListener>[0]>[0] & {
frameAncestors?: Array<{ url: string, frameId: number }>
};
type OnBeforeRequestDetails = Parameters<Parameters< type OnBeforeRequestDetails = Parameters<Parameters<
typeof browser.webRequest.onBeforeRequest.addListener>[0]>[0]; typeof browser.webRequest.onBeforeRequest.addListener>[0]>[0] & {
frameAncestors?: Array<{ url: string, frameId: number }>
};
const originUrlCache: string[] = [];
const chromeUserAgent = getChromeUserAgent(
(await browser.runtime.getPlatformInfo()).os, true);
/**
* 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 = 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)) {
for (const header of details.requestHeaders) {
if (header.name === "User-Agent") {
header.value = 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;
}
}
}
async function onBeforeRequest (details: OnBeforeRequestDetails) {
await browser.tabs.executeScript(details.tabId, { await browser.tabs.executeScript(details.tabId, {
code: ` code: `
window.isFramework = ${ window.isFramework = ${
@@ -450,114 +556,42 @@ async function initRequestListener () {
}; };
} }
/**
* 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.
*/
browser.webRequest.onBeforeRequest.addListener(
onBeforeRequest
, { urls: [
CAST_LOADER_SCRIPT_URL
, CAST_FRAMEWORK_LOADER_SCRIPT_URL ]}
, [ "blocking" ]);
}
async function initWhitelist () {
logger.info("init (whitelist)");
type OnBeforeSendHeadersDetails = Parameters<Parameters<
typeof browser.webRequest.onBeforeSendHeaders.addListener>[0]>[0] & {
// Missing on @types/firefox-webext-browser
frameAncestors?: Array<{ url: string, frameId: number }>
};
const originUrlCache: string[] = [];
const chromeUserAgent = getChromeUserAgent(
(await browser.runtime.getPlatformInfo()).os, true);
/**
* 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 onBeforeSendHeaders (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) {
const { os } = await browser.runtime.getPlatformInfo();
if (header.name === "User-Agent") {
header.value = chromeUserAgent;
break;
}
}
return {
requestHeaders: details.requestHeaders
};
}
function handleResourceRequests (details: OnBeforeSendHeadersDetails) {
if (!details.requestHeaders || !details.frameAncestors) {
return;
}
for (const ancestor of details.frameAncestors) {
if (originUrlCache.includes(ancestor.url)) {
for (const header of details.requestHeaders) {
if (header.name === "User-Agent") {
header.value = chromeUserAgent;
}
}
return {
requestHeaders: details.requestHeaders
};
}
}
}
async function registerUserAgentWhitelist () { async function registerUserAgentWhitelist () {
const { userAgentWhitelist const { userAgentWhitelist
, userAgentWhitelistEnabled } = await options.getAll(); , userAgentWhitelistEnabled } = await options.getAll();
if (!userAgentWhitelistEnabled) { browser.webRequest.onBeforeRequest.addListener(
onBeforeCastSDKRequest
, { urls: [
CAST_LOADER_SCRIPT_URL
, CAST_FRAMEWORK_LOADER_SCRIPT_URL ]}
, [ "blocking" ]);
if (!userAgentWhitelistEnabled || !userAgentWhitelist.length) {
return; return;
} }
browser.webRequest.onBeforeSendHeaders.addListener( browser.webRequest.onBeforeSendHeaders.addListener(
handleResourceRequests onWhitelistedBeforeSendHeaders
, { urls: [ "<all_urls>" ] }
, [ "blocking", "requestHeaders" ]);
browser.webRequest.onBeforeSendHeaders.addListener(
onBeforeSendHeaders
, { urls: userAgentWhitelist } , { urls: userAgentWhitelist }
, [ "blocking", "requestHeaders" ]); , [ "blocking", "requestHeaders" ]);
browser.webRequest.onBeforeSendHeaders.addListener(
onWhitelistedChildBeforeSendHeaders
, { urls: [ "<all_urls>" ] }
, [ "blocking", "requestHeaders" ]);
} }
function unregisterUserAgentWhitelist () { function unregisterUserAgentWhitelist () {
originUrlCache.length = 0; originUrlCache.length = 0;
browser.webRequest.onBeforeSendHeaders browser.webRequest.onBeforeSendHeaders
.removeListener(handleResourceRequests); .removeListener(onWhitelistedBeforeSendHeaders);
browser.webRequest.onBeforeSendHeaders browser.webRequest.onBeforeSendHeaders
.removeListener(onBeforeSendHeaders); .removeListener(onWhitelistedChildBeforeSendHeaders);
browser.webRequest.onBeforeRequest
.removeListener(onBeforeCastSDKRequest);
} }
// Register on first run // Register on first run
@@ -674,7 +708,6 @@ async function init () {
await ShimManager.init(); await ShimManager.init();
await initMenus(); await initMenus();
await initRequestListener();
await initWhitelist(); await initWhitelist();
await initMediaOverlay(); await initMediaOverlay();
await initBrowserAction(); await initBrowserAction();

View File

@@ -21,6 +21,7 @@ export default {
, receiverSelectorCloseIfFocusLost: true , receiverSelectorCloseIfFocusLost: true
, receiverSelectorWaitForConnection: true , receiverSelectorWaitForConnection: true
, userAgentWhitelistEnabled: true , userAgentWhitelistEnabled: true
, userAgentWhitelistRestrictedEnabled: true
, userAgentWhitelist: [ , userAgentWhitelist: [
"https://www.netflix.com/*" "https://www.netflix.com/*"
] ]

View File

@@ -30,6 +30,7 @@ export interface Options {
receiverSelectorCloseIfFocusLost: boolean; receiverSelectorCloseIfFocusLost: boolean;
receiverSelectorWaitForConnection: boolean; receiverSelectorWaitForConnection: boolean;
userAgentWhitelistEnabled: boolean; userAgentWhitelistEnabled: boolean;
userAgentWhitelistRestrictedEnabled: boolean;
userAgentWhitelist: string[]; userAgentWhitelist: string[];
[key: string]: Options[keyof Options]; [key: string]: Options[keyof Options];

View File

@@ -386,6 +386,24 @@ class OptionsApp extends Component<{}, OptionsAppState> {
</div> </div>
</label> </label>
<label className="option option--inline">
<div className="option__control">
<input name="userAgentWhitelistRestrictedEnabled"
type="checkbox"
checked={ this.state.options?.userAgentWhitelistRestrictedEnabled }
onChange={ this.handleInputChange } />
</div>
<div className="option__label">
{ _("optionsUserAgentWhitelistRestrictedEnabled") }
<span className="option__recommended">
{ _("optionsOptionRecommended") }
</span>
</div>
<div className="option__description">
{ _("optionsUserAgentWhitelistRestrictedEnabledDescription") }
</div>
</label>
<div className="option"> <div className="option">
<div className="option__label"> <div className="option__label">
{ _("optionsUserAgentWhitelistContent") } { _("optionsUserAgentWhitelistContent") }