Improve user agent switcher whitelist handling

This commit is contained in:
hensm
2018-10-21 15:32:07 +01:00
parent e5686c2017
commit adfbd5b2ef
4 changed files with 120 additions and 43 deletions

View File

@@ -34,13 +34,13 @@
"message": "User agent whitelist" "message": "User agent whitelist"
} }
, "options_category_uaWhitelist_description": { , "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": { , "options_option_uaWhitelistEnabled": {
"message": "Enabled" "message": "Enabled"
} }
, "options_option_uaWhitelist": { , "options_option_uaWhitelist": {
"message": "Site list (newline-separated)" "message": "Match patterns (newline-separated)"
} }
, "options_submit": { , "options_submit": {

View File

@@ -10,7 +10,9 @@ browser.runtime.onInstalled.addListener(async details => {
option_localMediaEnabled: true option_localMediaEnabled: true
, option_localMediaServerPort: 9555 , option_localMediaServerPort: 9555
, option_uaWhitelistEnabled: true , option_uaWhitelistEnabled: true
, option_uaWhitelist: [ "www.netflix.com" ].join("\n") , option_uaWhitelist: [
"https://www.netflix.com/*"
]
}; };
switch (details.reason) { switch (details.reason) {
@@ -118,38 +120,71 @@ let currentUAString;
* TODO: Inject script to change navigator.userAgent * TODO: Inject script to change navigator.userAgent
* property. * property.
*/ */
browser.webRequest.onBeforeSendHeaders.addListener( async function onBeforeSendHeaders (details) {
async details => { const { options } = await browser.storage.sync.get("options");
const { options } = await browser.storage.sync.get("options");
// Cancel if feature is disabled // Create Chrome UA from platform info on first run
if (!options.option_uaWhitelistEnabled) return; if (!currentUAString) {
currentUAString = UA_STRINGS[
(await browser.runtime.getPlatformInfo()).os]
}
// Cancel if not on whitelist // Find and rewrite the User-Agent header
// TODO: Do this with the built in filter for (const header of details.requestHeaders) {
const { hostname } = new URL(details.url); if (header.name.toLowerCase() === "user-agent") {
if (!options.option_uaWhitelist.split("\n").includes(hostname)) return; header.value = currentUAString;
break;
// 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
};
} }
, { urls: [ "<all_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 // Defines window.chrome for site compatibility

View File

@@ -1,6 +1,6 @@
.category { .category {
display: grid; display: grid;
grid-template-columns: min-content min-content; grid-template-columns: 150px 1fr;
grid-column-gap: 20px; grid-column-gap: 20px;
grid-row-gap: 5px; grid-row-gap: 5px;
} }
@@ -16,7 +16,6 @@
.option-label { .option-label {
text-align: right; text-align: right;
white-space: nowrap;
display: inline-block; display: inline-block;
} }

View File

@@ -7,6 +7,8 @@ import ReactDOM from "react-dom";
const _ = browser.i18n.getMessage; const _ = browser.i18n.getMessage;
const MATCH_PATTERN_REGEX = /^(?:(\*|https?|file|ftp):\/\/((?:\*\.|[^\/\*])+)(\/.*)|<all_urls>)$/;
class OptionsApp extends Component { class OptionsApp extends Component {
constructor (props) { constructor (props) {
super(props); super(props);
@@ -15,9 +17,10 @@ class OptionsApp extends Component {
isFormValid: true isFormValid: true
}; };
this.handleFormSubmit = this.handleFormSubmit.bind(this); this.handleFormSubmit = this.handleFormSubmit.bind(this);
this.handleFormChange = this.handleFormChange.bind(this); this.handleFormChange = this.handleFormChange.bind(this);
this.handleInputChange = this.handleInputChange.bind(this); this.handleInputChange = this.handleInputChange.bind(this);
this.handleUAWhitelistChange = this.handleUAWhitelistChange.bind(this);
} }
/** /**
@@ -26,10 +29,10 @@ class OptionsApp extends Component {
setStorage () { setStorage () {
return browser.storage.sync.set({ return browser.storage.sync.set({
options: { options: {
option_localMediaEnabled: this.state.option_localMediaEnabled option_localMediaEnabled : this.state.option_localMediaEnabled
, option_localMediaServerPort: this.state.option_localMediaServerPort , option_localMediaServerPort : this.state.option_localMediaServerPort
, option_uaWhitelistEnabled: this.state.option_uaWhitelistEnabled , option_uaWhitelistEnabled : this.state.option_uaWhitelistEnabled
, option_uaWhitelist: this.state.option_uaWhitelist , option_uaWhitelist : this.state.option_uaWhitelist
} }
}); });
} }
@@ -61,7 +64,24 @@ class OptionsApp extends Component {
this.form.reportValidity(); this.form.reportValidity();
try { try {
const { options: oldOptions } = await browser.storage.sync.get("options");
await this.setStorage(); 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) {} } 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 () { render () {
return ( return (
<form id="form" ref={ form => { this.form = form; }} <form id="form" ref={ form => { this.form = form; }}
@@ -148,9 +189,11 @@ class OptionsApp extends Component {
{ _("options_option_uaWhitelist") } { _("options_option_uaWhitelist") }
</div> </div>
<textarea name="option_uaWhitelist" <textarea name="option_uaWhitelist"
value={this.state.option_uaWhitelist} value={ this.state.option_uaWhitelist
&& this.state.option_uaWhitelist.join("\n") }
required required
onChange={ this.handleInputChange }> rows="8"
onChange={ this.handleUAWhitelistChange }>
</textarea> </textarea>
</label> </label>
</fieldset> </fieldset>