From adfbd5b2ef4531ce2e40598178d4f815baf01ef7 Mon Sep 17 00:00:00 2001 From: hensm Date: Sun, 21 Oct 2018 15:32:07 +0100 Subject: [PATCH] Improve user agent switcher whitelist handling --- ext/src/_locales/en/messages.json | 4 +- ext/src/main.js | 95 +++++++++++++++++++++---------- ext/src/options/index.css | 3 +- ext/src/options/index.jsx | 61 +++++++++++++++++--- 4 files changed, 120 insertions(+), 43 deletions(-) diff --git a/ext/src/_locales/en/messages.json b/ext/src/_locales/en/messages.json index b3c196b..c5dadf7 100755 --- a/ext/src/_locales/en/messages.json +++ b/ext/src/_locales/en/messages.json @@ -34,13 +34,13 @@ "message": "User agent whitelist" } , "options_category_uaWhitelist_description": { - "message": "Sites for which to replace the user agent with a Chrome version for compatibility." + "message": "Sites for which to replace the user agent with a Chrome version for compatibility. Must be valid match patterns." } , "options_option_uaWhitelistEnabled": { "message": "Enabled" } , "options_option_uaWhitelist": { - "message": "Site list (newline-separated)" + "message": "Match patterns (newline-separated)" } , "options_submit": { diff --git a/ext/src/main.js b/ext/src/main.js index a9d917a..99d8690 100755 --- a/ext/src/main.js +++ b/ext/src/main.js @@ -10,7 +10,9 @@ browser.runtime.onInstalled.addListener(async details => { option_localMediaEnabled: true , option_localMediaServerPort: 9555 , option_uaWhitelistEnabled: true - , option_uaWhitelist: [ "www.netflix.com" ].join("\n") + , option_uaWhitelist: [ + "https://www.netflix.com/*" + ] }; switch (details.reason) { @@ -118,38 +120,71 @@ let currentUAString; * TODO: Inject script to change navigator.userAgent * property. */ -browser.webRequest.onBeforeSendHeaders.addListener( - async details => { - const { options } = await browser.storage.sync.get("options"); +async function onBeforeSendHeaders (details) { + const { options } = await browser.storage.sync.get("options"); - // Cancel if feature is disabled - if (!options.option_uaWhitelistEnabled) return; + // Create Chrome UA from platform info on first run + if (!currentUAString) { + currentUAString = UA_STRINGS[ + (await browser.runtime.getPlatformInfo()).os] + } - // Cancel if not on whitelist - // TODO: Do this with the built in filter - const { hostname } = new URL(details.url); - if (!options.option_uaWhitelist.split("\n").includes(hostname)) return; - - // Create Chrome UA from platform info on first run - if (!currentUAString) { - currentUAString = UA_STRINGS[ - (await browser.runtime.getPlatformInfo()).os] - } - - // Find and rewrite the User-Agent header - for (const header of details.requestHeaders) { - if (header.name.toLowerCase() === "user-agent") { - header.value = currentUAString; - break; - } - } - - return { - requestHeaders: details.requestHeaders - }; + // Find and rewrite the User-Agent header + for (const header of details.requestHeaders) { + if (header.name.toLowerCase() === "user-agent") { + header.value = currentUAString; + break; } - , { urls: [ "" ]} - , [ "blocking", "requestHeaders" ]); + } + + return { + requestHeaders: details.requestHeaders + }; +} + +async function registerWebRequestListeners (alteredOptions) { + const { options } = await browser.storage.sync.get("options"); + + // If options aren't set yet, return + if (!options) return; + + const registerFunctions = { + onBeforeSendHeaders () { + browser.webRequest.onBeforeSendHeaders.addListener( + onBeforeSendHeaders + , { urls: options.option_uaWhitelistEnabled + ? options.option_uaWhitelist + : [] } + , [ "blocking", "requestHeaders" ]); + } + }; + + + if (!alteredOptions) { + // If no altered properties specified, register all listeners + for (const func of Object.values(registerFunctions)) { + func(); + } + + } else { + if ( alteredOptions.includes("option_uaWhitelist") + || alteredOptions.includes("option_uaWhitelistEnabled")) { + browser.webRequest.onBeforeSendHeaders.removeListener(onBeforeSendHeaders); + registerFunctions.onBeforeSendHeaders(); + } + } +} + +registerWebRequestListeners(); + +browser.runtime.onMessage.addListener(message => { + switch (message.subject) { + case "optionsUpdated": + const { alteredOptions } = message.data; + registerWebRequestListeners(alteredOptions); + break; + } +}); // Defines window.chrome for site compatibility diff --git a/ext/src/options/index.css b/ext/src/options/index.css index 1bd3c32..bb3b744 100644 --- a/ext/src/options/index.css +++ b/ext/src/options/index.css @@ -1,6 +1,6 @@ .category { display: grid; - grid-template-columns: min-content min-content; + grid-template-columns: 150px 1fr; grid-column-gap: 20px; grid-row-gap: 5px; } @@ -16,7 +16,6 @@ .option-label { text-align: right; - white-space: nowrap; display: inline-block; } diff --git a/ext/src/options/index.jsx b/ext/src/options/index.jsx index a1c5f48..4b0eee5 100644 --- a/ext/src/options/index.jsx +++ b/ext/src/options/index.jsx @@ -7,6 +7,8 @@ import ReactDOM from "react-dom"; const _ = browser.i18n.getMessage; +const MATCH_PATTERN_REGEX = /^(?:(\*|https?|file|ftp):\/\/((?:\*\.|[^\/\*])+)(\/.*)|)$/; + class OptionsApp extends Component { constructor (props) { super(props); @@ -15,9 +17,10 @@ class OptionsApp extends Component { isFormValid: true }; - this.handleFormSubmit = this.handleFormSubmit.bind(this); - this.handleFormChange = this.handleFormChange.bind(this); - this.handleInputChange = this.handleInputChange.bind(this); + this.handleFormSubmit = this.handleFormSubmit.bind(this); + this.handleFormChange = this.handleFormChange.bind(this); + this.handleInputChange = this.handleInputChange.bind(this); + this.handleUAWhitelistChange = this.handleUAWhitelistChange.bind(this); } /** @@ -26,10 +29,10 @@ class OptionsApp extends Component { setStorage () { return browser.storage.sync.set({ options: { - option_localMediaEnabled: this.state.option_localMediaEnabled - , option_localMediaServerPort: this.state.option_localMediaServerPort - , option_uaWhitelistEnabled: this.state.option_uaWhitelistEnabled - , option_uaWhitelist: this.state.option_uaWhitelist + option_localMediaEnabled : this.state.option_localMediaEnabled + , option_localMediaServerPort : this.state.option_localMediaServerPort + , option_uaWhitelistEnabled : this.state.option_uaWhitelistEnabled + , option_uaWhitelist : this.state.option_uaWhitelist } }); } @@ -61,7 +64,24 @@ class OptionsApp extends Component { this.form.reportValidity(); try { + const { options: oldOptions } = await browser.storage.sync.get("options"); await this.setStorage(); + const { options } = await browser.storage.sync.get("options"); + + const alteredOptions = []; + + for (const [ key, val ] of Object.entries(options)) { + const oldVal = oldOptions[key]; + if (oldVal !== val) { + alteredOptions.push(key); + } + } + + // Send update message / event + browser.runtime.sendMessage({ + subject: "optionsUpdated" + , data: { alteredOptions } + }); } catch (err) {} } @@ -88,6 +108,27 @@ class OptionsApp extends Component { }); } + handleUAWhitelistChange (ev) { + // Split patterns by newline + const matchPatterns = ev.target.value.split("\n"); + + // Validate each pattern against a regexp + for (const pattern of matchPatterns) { + if (!MATCH_PATTERN_REGEX.test(pattern)) { + // Set as invalid + ev.target.setCustomValidity(`Match pattern invalid: ${pattern}`); + break; + } + + // Set as valid + ev.target.setCustomValidity(""); + } + + this.setState({ + [ ev.target.name ]: matchPatterns + }); + } + render () { return (
{ this.form = form; }} @@ -148,9 +189,11 @@ class OptionsApp extends Component { { _("options_option_uaWhitelist") }