Split up extension background script

This commit is contained in:
hensm
2021-04-24 02:54:46 +01:00
parent fcce716d8f
commit 2edbfce2e4
3 changed files with 575 additions and 557 deletions

View File

@@ -7,21 +7,15 @@ import messaging from "../messaging";
import options from "../lib/options";
import bridge, { BridgeInfo } from "../lib/bridge";
import { getChromeUserAgent } from "../lib/userAgents";
import { getMediaTypesForPageUrl, stringify } from "../lib/utils";
import { CAST_FRAMEWORK_LOADER_SCRIPT_URL
, CAST_LOADER_SCRIPT_URL } from "../lib/endpoints";
import { ReceiverSelectionActionType
, ReceiverSelectorMediaType } from "./receiverSelector";
import ReceiverSelectorManager
from "./receiverSelector/ReceiverSelectorManager";
import ShimManager from "./ShimManager";
import StatusManager from "./StatusManager";
import { initMenus } from "./menus";
import { initWhitelist } from "./whitelist";
const _ = browser.i18n.getMessage;
@@ -74,554 +68,6 @@ async function initBrowserAction () {
}
async function initMenus () {
logger.info("init (menus)");
const URL_PATTERN_HTTP = "http://*/*";
const URL_PATTERN_HTTPS = "https://*/*";
const URL_PATTERN_FILE = "file://*/*";
const URL_PATTERNS_REMOTE = [ URL_PATTERN_HTTP, URL_PATTERN_HTTPS ];
const URL_PATTERNS_ALL = [ ...URL_PATTERNS_REMOTE, URL_PATTERN_FILE ];
type MenuId = string | number;
let menuIdCast: MenuId;
let menuIdMediaCast: MenuId;
let menuIdWhitelist: MenuId;
let menuIdWhitelistRecommended: MenuId;
const whitelistChildMenuPatterns = new Map<MenuId, string>();
const opts = await options.getAll();
// Global "Cast..." menu item
menuIdCast = browser.menus.create({
contexts: [ "browser_action", "page", "tools_menu" ]
, title: _("contextCast")
});
// <video>/<audio> "Cast..." context menu item
menuIdMediaCast = browser.menus.create({
contexts: [ "audio", "video" ]
, title: _("contextCast")
, visible: opts.mediaEnabled
, targetUrlPatterns: opts.localMediaEnabled
? URL_PATTERNS_ALL
: URL_PATTERNS_REMOTE
});
menuIdWhitelist = browser.menus.create({
contexts: [ "browser_action" ]
, title: _("contextAddToWhitelist")
, enabled: false
});
menuIdWhitelistRecommended = browser.menus.create({
title: _("contextAddToWhitelistRecommended")
, parentId: menuIdWhitelist
});
browser.menus.create({
type: "separator"
, parentId: menuIdWhitelist
});
browser.menus.onClicked.addListener(async (info, tab) => {
if (info.parentMenuItemId === menuIdWhitelist) {
const pattern = whitelistChildMenuPatterns.get(info.menuItemId);
if (!pattern) {
throw logger.error(`Whitelist pattern not found for menu item ID ${info.menuItemId}.`);
}
const whitelist = await options.get("userAgentWhitelist");
if (!whitelist.includes(pattern)) {
// Add to whitelist and update options
whitelist.push(pattern);
await options.set("userAgentWhitelist", whitelist);
}
return;
}
if (tab?.id === undefined) {
throw logger.error("Menu handler tab ID not found.");
}
if (!info.pageUrl) {
throw logger.error("Menu handler page URL not found.");
}
const availableMediaTypes = getMediaTypesForPageUrl(info.pageUrl);
switch (info.menuItemId) {
case menuIdCast: {
const selection = await ReceiverSelectorManager.getSelection(
tab.id, info.frameId);
// Selection cancelled
if (!selection) {
break;
}
loadSender({
tabId: tab.id
, frameId: info.frameId
, selection
});
break;
}
case menuIdMediaCast: {
const selection = await ReceiverSelectorManager.getSelection(
tab.id, info.frameId, true);
// Selection cancelled
if (!selection) {
break;
}
switch (selection.actionType) {
case ReceiverSelectionActionType.Cast: {
/**
* If the selected media type is App, that refers to the
* media sender in this context, so load media sender.
*/
if (selection.mediaType ===
ReceiverSelectorMediaType.App) {
await browser.tabs.executeScript(tab.id, {
code: stringify`
window.receiver = ${selection.receiver};
window.mediaUrl = ${info.srcUrl};
window.targetElementId = ${
info.targetElementId};
`
, frameId: info.frameId
});
await browser.tabs.executeScript(tab.id, {
file: "senders/media/bundle.js"
, frameId: info.frameId
});
} else {
// Handle other responses
loadSender({
tabId: tab.id
, frameId: info.frameId
, selection
});
}
break;
}
}
break;
}
}
});
// Hide cast item on extension pages
browser.menus.onShown.addListener(info => {
if (info.pageUrl?.startsWith(browser.runtime.getURL(""))) {
browser.menus.update(menuIdCast, {
visible: false
});
browser.menus.refresh();
}
});
browser.menus.onHidden.addListener(() => {
browser.menus.update(menuIdCast, {
visible: true
});
});
browser.menus.onShown.addListener(async info => {
// Only rebuild menus if whitelist menu present
// WebExt typings are broken again here, so ugly casting
const menuIds = info.menuIds as unknown as number[];
if (!menuIds.includes(menuIdWhitelist as number)) {
return;
}
/**
* If page URL doesn't exist, we're not on a page and have
* nothing to whitelist, so disable the menu and return.
*/
if (!info.pageUrl) {
browser.menus.update(menuIdWhitelist, {
enabled: false
});
browser.menus.refresh();
return;
}
const url = new URL(info.pageUrl);
const urlHasOrigin = url.origin !== "null";
/**
* If the page URL doesn't have an origin, we're not on a
* remote page and have nothing to whitelist, so disable the
* menu and return.
*/
if (!urlHasOrigin) {
browser.menus.update(menuIdWhitelist, {
enabled: false
});
browser.menus.refresh();
return;
}
// Enable the whitelist menu
browser.menus.update(menuIdWhitelist, {
enabled: true
});
for (const [ menuId ] of whitelistChildMenuPatterns) {
// Clear all page-specific temporary menus
if (menuId !== menuIdWhitelistRecommended) {
browser.menus.remove(menuId);
}
whitelistChildMenuPatterns.delete(menuId);
}
// If there is more than one subdomain, get the base domain
const baseDomain = (url.hostname.match(/\./g) || []).length > 1
? url.hostname.substring(url.hostname.indexOf(".") + 1)
: url.hostname;
const portlessOrigin = `${url.protocol}//${url.hostname}`;
const patternRecommended = `${portlessOrigin}/*`;
const patternSearch = `${portlessOrigin}${url.pathname}${url.search}`;
const patternWildcardProtocol = `*://${url.hostname}/*`;
const patternWildcardSubdomain = `${url.protocol}//*.${baseDomain}/*`;
const patternWildcardProtocolAndSubdomain = `*://*.${baseDomain}/*`;
// Update recommended menu item
browser.menus.update(menuIdWhitelistRecommended, {
title: _("contextAddToWhitelistRecommended", patternRecommended)
});
whitelistChildMenuPatterns.set(
menuIdWhitelistRecommended, patternRecommended);
if (url.search) {
const whitelistSearchMenuId = browser.menus.create({
title: _("contextAddToWhitelistAdvancedAdd", patternSearch)
, parentId: menuIdWhitelist
});
whitelistChildMenuPatterns.set(
whitelistSearchMenuId, patternSearch);
}
/**
* Split URL path into segments and add menu items for each
* partial path as the segments are removed.
*/
{
const pathTrimmed = url.pathname.endsWith("/")
? url.pathname.substring(0, url.pathname.length - 1)
: url.pathname;
const pathSegments = pathTrimmed.split("/")
.filter(segment => segment)
.reverse();
if (pathSegments.length) {
for (let i = 0; i < pathSegments.length; i++) {
const partialPath = pathSegments
.slice(i)
.reverse()
.join("/");
const pattern = `${portlessOrigin}/${partialPath}/*`;
const partialPathMenuId = browser.menus.create({
title: _("contextAddToWhitelistAdvancedAdd", pattern)
, parentId: menuIdWhitelist
});
whitelistChildMenuPatterns.set(
partialPathMenuId, pattern);
}
}
}
const wildcardProtocolMenuId = browser.menus.create({
title: _("contextAddToWhitelistAdvancedAdd"
, patternWildcardProtocol)
, parentId: menuIdWhitelist
});
whitelistChildMenuPatterns.set(
wildcardProtocolMenuId, patternWildcardProtocol);
const wildcardSubdomainMenuId = browser.menus.create({
title: _("contextAddToWhitelistAdvancedAdd"
, patternWildcardSubdomain)
, parentId: menuIdWhitelist
});
whitelistChildMenuPatterns.set(
wildcardSubdomainMenuId, patternWildcardSubdomain);
const wildcardProtocolAndSubdomainMenuId = browser.menus.create({
title: _("contextAddToWhitelistAdvancedAdd"
, patternWildcardProtocolAndSubdomain)
, parentId: menuIdWhitelist
});
whitelistChildMenuPatterns.set(
wildcardProtocolAndSubdomainMenuId
, patternWildcardProtocolAndSubdomain);
await browser.menus.refresh();
});
options.addEventListener("changed", async ev => {
const alteredOpts = ev.detail;
const newOpts = await options.getAll();
if (alteredOpts.includes("mediaEnabled")) {
browser.menus.update(menuIdMediaCast, {
visible: newOpts.mediaEnabled
});
}
if (alteredOpts.includes("localMediaEnabled")) {
browser.menus.update(menuIdMediaCast, {
targetUrlPatterns: newOpts.localMediaEnabled
? URL_PATTERNS_ALL
: URL_PATTERNS_REMOTE
});
}
});
}
async function initWhitelist () {
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<
typeof browser.webRequest.onBeforeRequest.addListener>[0]>[0] & {
frameAncestors?: Array<{ url: string, frameId: number }>
};
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 (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: [ "<all_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
await registerUserAgentWhitelist();
// Re-register when options change
options.addEventListener("changed", ev => {
const alteredOpts = ev.detail;
if (alteredOpts.includes("userAgentWhitelist")
|| alteredOpts.includes("userAgentWhitelistEnabled")) {
unregisterUserAgentWhitelist();
registerUserAgentWhitelist();
}
});
}
async function initMediaOverlay () {
logger.info("init (media overlay)");

367
ext/src/background/menus.ts Normal file
View File

@@ -0,0 +1,367 @@
"use strict";
import loadSender from "../lib/loadSender";
import logger from "../lib/logger";
import options from "../lib/options";
import { getMediaTypesForPageUrl, stringify } from "../lib/utils";
import { ReceiverSelectionActionType
, ReceiverSelectorMediaType } from "./receiverSelector";
import ReceiverSelectorManager
from "./receiverSelector/ReceiverSelectorManager";
const _ = browser.i18n.getMessage;
export async function initMenus () {
logger.info("init (menus)");
const URL_PATTERN_HTTP = "http://*/*";
const URL_PATTERN_HTTPS = "https://*/*";
const URL_PATTERN_FILE = "file://*/*";
const URL_PATTERNS_REMOTE = [ URL_PATTERN_HTTP, URL_PATTERN_HTTPS ];
const URL_PATTERNS_ALL = [ ...URL_PATTERNS_REMOTE, URL_PATTERN_FILE ];
type MenuId = string | number;
let menuIdCast: MenuId;
let menuIdMediaCast: MenuId;
let menuIdWhitelist: MenuId;
let menuIdWhitelistRecommended: MenuId;
const whitelistChildMenuPatterns = new Map<MenuId, string>();
const opts = await options.getAll();
// Global "Cast..." menu item
menuIdCast = browser.menus.create({
contexts: [ "browser_action", "page", "tools_menu" ]
, title: _("contextCast")
});
// <video>/<audio> "Cast..." context menu item
menuIdMediaCast = browser.menus.create({
contexts: [ "audio", "video" ]
, title: _("contextCast")
, visible: opts.mediaEnabled
, targetUrlPatterns: opts.localMediaEnabled
? URL_PATTERNS_ALL
: URL_PATTERNS_REMOTE
});
menuIdWhitelist = browser.menus.create({
contexts: [ "browser_action" ]
, title: _("contextAddToWhitelist")
, enabled: false
});
menuIdWhitelistRecommended = browser.menus.create({
title: _("contextAddToWhitelistRecommended")
, parentId: menuIdWhitelist
});
browser.menus.create({
type: "separator"
, parentId: menuIdWhitelist
});
browser.menus.onClicked.addListener(async (info, tab) => {
if (info.parentMenuItemId === menuIdWhitelist) {
const pattern = whitelistChildMenuPatterns.get(info.menuItemId);
if (!pattern) {
throw logger.error(`Whitelist pattern not found for menu item ID ${info.menuItemId}.`);
}
const whitelist = await options.get("userAgentWhitelist");
if (!whitelist.includes(pattern)) {
// Add to whitelist and update options
whitelist.push(pattern);
await options.set("userAgentWhitelist", whitelist);
}
return;
}
if (tab?.id === undefined) {
throw logger.error("Menu handler tab ID not found.");
}
if (!info.pageUrl) {
throw logger.error("Menu handler page URL not found.");
}
const availableMediaTypes = getMediaTypesForPageUrl(info.pageUrl);
switch (info.menuItemId) {
case menuIdCast: {
const selection = await ReceiverSelectorManager.getSelection(
tab.id, info.frameId);
// Selection cancelled
if (!selection) {
break;
}
loadSender({
tabId: tab.id
, frameId: info.frameId
, selection
});
break;
}
case menuIdMediaCast: {
const selection = await ReceiverSelectorManager.getSelection(
tab.id, info.frameId, true);
// Selection cancelled
if (!selection) {
break;
}
switch (selection.actionType) {
case ReceiverSelectionActionType.Cast: {
/**
* If the selected media type is App, that refers to the
* media sender in this context, so load media sender.
*/
if (selection.mediaType ===
ReceiverSelectorMediaType.App) {
await browser.tabs.executeScript(tab.id, {
code: stringify`
window.receiver = ${selection.receiver};
window.mediaUrl = ${info.srcUrl};
window.targetElementId = ${
info.targetElementId};
`
, frameId: info.frameId
});
await browser.tabs.executeScript(tab.id, {
file: "senders/media/bundle.js"
, frameId: info.frameId
});
} else {
// Handle other responses
loadSender({
tabId: tab.id
, frameId: info.frameId
, selection
});
}
break;
}
}
break;
}
}
});
// Hide cast item on extension pages
browser.menus.onShown.addListener(info => {
if (info.pageUrl?.startsWith(browser.runtime.getURL(""))) {
browser.menus.update(menuIdCast, {
visible: false
});
browser.menus.refresh();
}
});
browser.menus.onHidden.addListener(() => {
browser.menus.update(menuIdCast, {
visible: true
});
});
browser.menus.onShown.addListener(async info => {
// Only rebuild menus if whitelist menu present
// WebExt typings are broken again here, so ugly casting
const menuIds = info.menuIds as unknown as number[];
if (!menuIds.includes(menuIdWhitelist as number)) {
return;
}
/**
* If page URL doesn't exist, we're not on a page and have
* nothing to whitelist, so disable the menu and return.
*/
if (!info.pageUrl) {
browser.menus.update(menuIdWhitelist, {
enabled: false
});
browser.menus.refresh();
return;
}
const url = new URL(info.pageUrl);
const urlHasOrigin = url.origin !== "null";
/**
* If the page URL doesn't have an origin, we're not on a
* remote page and have nothing to whitelist, so disable the
* menu and return.
*/
if (!urlHasOrigin) {
browser.menus.update(menuIdWhitelist, {
enabled: false
});
browser.menus.refresh();
return;
}
// Enable the whitelist menu
browser.menus.update(menuIdWhitelist, {
enabled: true
});
for (const [ menuId ] of whitelistChildMenuPatterns) {
// Clear all page-specific temporary menus
if (menuId !== menuIdWhitelistRecommended) {
browser.menus.remove(menuId);
}
whitelistChildMenuPatterns.delete(menuId);
}
// If there is more than one subdomain, get the base domain
const baseDomain = (url.hostname.match(/\./g) || []).length > 1
? url.hostname.substring(url.hostname.indexOf(".") + 1)
: url.hostname;
const portlessOrigin = `${url.protocol}//${url.hostname}`;
const patternRecommended = `${portlessOrigin}/*`;
const patternSearch = `${portlessOrigin}${url.pathname}${url.search}`;
const patternWildcardProtocol = `*://${url.hostname}/*`;
const patternWildcardSubdomain = `${url.protocol}//*.${baseDomain}/*`;
const patternWildcardProtocolAndSubdomain = `*://*.${baseDomain}/*`;
// Update recommended menu item
browser.menus.update(menuIdWhitelistRecommended, {
title: _("contextAddToWhitelistRecommended", patternRecommended)
});
whitelistChildMenuPatterns.set(
menuIdWhitelistRecommended, patternRecommended);
if (url.search) {
const whitelistSearchMenuId = browser.menus.create({
title: _("contextAddToWhitelistAdvancedAdd", patternSearch)
, parentId: menuIdWhitelist
});
whitelistChildMenuPatterns.set(
whitelistSearchMenuId, patternSearch);
}
/**
* Split URL path into segments and add menu items for each
* partial path as the segments are removed.
*/
{
const pathTrimmed = url.pathname.endsWith("/")
? url.pathname.substring(0, url.pathname.length - 1)
: url.pathname;
const pathSegments = pathTrimmed.split("/")
.filter(segment => segment)
.reverse();
if (pathSegments.length) {
for (let i = 0; i < pathSegments.length; i++) {
const partialPath = pathSegments
.slice(i)
.reverse()
.join("/");
const pattern = `${portlessOrigin}/${partialPath}/*`;
const partialPathMenuId = browser.menus.create({
title: _("contextAddToWhitelistAdvancedAdd", pattern)
, parentId: menuIdWhitelist
});
whitelistChildMenuPatterns.set(
partialPathMenuId, pattern);
}
}
}
const wildcardProtocolMenuId = browser.menus.create({
title: _("contextAddToWhitelistAdvancedAdd"
, patternWildcardProtocol)
, parentId: menuIdWhitelist
});
whitelistChildMenuPatterns.set(
wildcardProtocolMenuId, patternWildcardProtocol);
const wildcardSubdomainMenuId = browser.menus.create({
title: _("contextAddToWhitelistAdvancedAdd"
, patternWildcardSubdomain)
, parentId: menuIdWhitelist
});
whitelistChildMenuPatterns.set(
wildcardSubdomainMenuId, patternWildcardSubdomain);
const wildcardProtocolAndSubdomainMenuId = browser.menus.create({
title: _("contextAddToWhitelistAdvancedAdd"
, patternWildcardProtocolAndSubdomain)
, parentId: menuIdWhitelist
});
whitelistChildMenuPatterns.set(
wildcardProtocolAndSubdomainMenuId
, patternWildcardProtocolAndSubdomain);
await browser.menus.refresh();
});
options.addEventListener("changed", async ev => {
const alteredOpts = ev.detail;
const newOpts = await options.getAll();
if (alteredOpts.includes("mediaEnabled")) {
browser.menus.update(menuIdMediaCast, {
visible: newOpts.mediaEnabled
});
}
if (alteredOpts.includes("localMediaEnabled")) {
browser.menus.update(menuIdMediaCast, {
targetUrlPatterns: newOpts.localMediaEnabled
? URL_PATTERNS_ALL
: URL_PATTERNS_REMOTE
});
}
});
}

View File

@@ -0,0 +1,205 @@
"use strict";
import logger from "../lib/logger";
import options from "../lib/options";
import { getChromeUserAgent } from "../lib/userAgents";
import { CAST_FRAMEWORK_LOADER_SCRIPT_URL
, CAST_LOADER_SCRIPT_URL } from "../lib/endpoints";
export async function initWhitelist () {
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<
typeof browser.webRequest.onBeforeRequest.addListener>[0]>[0] & {
frameAncestors?: Array<{ url: string, frameId: number }>
};
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 (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: [ "<all_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
await registerUserAgentWhitelist();
// Re-register when options change
options.addEventListener("changed", ev => {
const alteredOpts = ev.detail;
if (alteredOpts.includes("userAgentWhitelist")
|| alteredOpts.includes("userAgentWhitelistEnabled")) {
unregisterUserAgentWhitelist();
registerUserAgentWhitelist();
}
});
}