fune/toolkit/components/extensions/ProxyScriptContext.jsm
Kris Maglione 6476f95b13 Bug 1431533: Part 5a - Auto-rewrite code to use ChromeUtils import methods. r=florian
This was done using the following script:
37e3803c7a/processors/chromeutils-import.jsm

MozReview-Commit-ID: 1Nc3XDu0wGl

--HG--
extra : source : 12fc4dee861c812fd2bd032c63ef17af61800c70
2018-01-29 15:20:18 -08:00

394 lines
13 KiB
JavaScript

/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["ProxyScriptContext"];
/* exported ProxyScriptContext */
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm");
ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
ChromeUtils.defineModuleGetter(this, "ExtensionChild",
"resource://gre/modules/ExtensionChild.jsm");
ChromeUtils.defineModuleGetter(this, "Schemas",
"resource://gre/modules/Schemas.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "ProxyService",
"@mozilla.org/network/protocol-proxy-service;1",
"nsIProtocolProxyService");
const CATEGORY_EXTENSION_SCRIPTS_CONTENT = "webextension-scripts-content";
// DNS is resolved on the SOCKS proxy server.
const {TRANSPARENT_PROXY_RESOLVES_HOST} = Ci.nsIProxyInfo;
// The length of time (seconds) to wait for a proxy to resolve before ignoring it.
const PROXY_TIMEOUT_SEC = 10;
const {
ExtensionError,
defineLazyGetter,
} = ExtensionUtils;
const {
BaseContext,
CanOfAPIs,
LocalAPIImplementation,
SchemaAPIManager,
} = ExtensionCommon;
const PROXY_TYPES = Object.freeze({
DIRECT: "direct",
HTTPS: "https",
PROXY: "http", // Synonym for PROXY_TYPES.HTTP
HTTP: "http",
SOCKS: "socks", // SOCKS5
SOCKS4: "socks4",
});
const ProxyInfoData = {
validate(proxyData) {
if (proxyData.type && proxyData.type.toLowerCase() === "direct") {
return {type: proxyData.type};
}
for (let prop of ["type", "host", "port", "username", "password", "proxyDNS", "failoverTimeout"]) {
this[prop](proxyData);
}
return proxyData;
},
type(proxyData) {
let {type} = proxyData;
if (typeof type !== "string" || !PROXY_TYPES.hasOwnProperty(type.toUpperCase())) {
throw new ExtensionError(`FindProxyForURL: Invalid proxy server type: "${type}"`);
}
proxyData.type = PROXY_TYPES[type.toUpperCase()];
},
host(proxyData) {
let {host} = proxyData;
if (typeof host !== "string" || host.includes(" ")) {
throw new ExtensionError(`FindProxyForURL: Invalid proxy server host: "${host}"`);
}
if (!host.length) {
throw new ExtensionError("FindProxyForURL: Proxy server host cannot be empty");
}
proxyData.host = host;
},
port(proxyData) {
let port = Number.parseInt(proxyData.port, 10);
if (!Number.isInteger(port)) {
throw new ExtensionError(`FindProxyForURL: Invalid proxy server port: "${port}"`);
}
if (port < 1 || port > 0xffff) {
throw new ExtensionError(`FindProxyForURL: Proxy server port ${port} outside range 1 to 65535`);
}
proxyData.port = port;
},
username(proxyData) {
let {username} = proxyData;
if (username !== undefined && typeof username !== "string") {
throw new ExtensionError(`FindProxyForURL: Invalid proxy server username: "${username}"`);
}
},
password(proxyData) {
let {password} = proxyData;
if (password !== undefined && typeof password !== "string") {
throw new ExtensionError(`FindProxyForURL: Invalid proxy server password: "${password}"`);
}
},
proxyDNS(proxyData) {
let {proxyDNS, type} = proxyData;
if (proxyDNS !== undefined) {
if (typeof proxyDNS !== "boolean") {
throw new ExtensionError(`FindProxyForURL: Invalid proxyDNS value: "${proxyDNS}"`);
}
if (proxyDNS && type !== PROXY_TYPES.SOCKS && type !== PROXY_TYPES.SOCKS4) {
throw new ExtensionError(`FindProxyForURL: proxyDNS can only be true for SOCKS proxy servers`);
}
}
},
failoverTimeout(proxyData) {
let {failoverTimeout} = proxyData;
if (failoverTimeout !== undefined && (!Number.isInteger(failoverTimeout) || failoverTimeout < 1)) {
throw new ExtensionError(`FindProxyForURL: Invalid failover timeout: "${failoverTimeout}"`);
}
},
createProxyInfoFromData(proxyDataList, defaultProxyInfo, proxyDataListIndex = 0) {
if (proxyDataListIndex >= proxyDataList.length) {
return defaultProxyInfo;
}
let {type, host, port, username, password, proxyDNS, failoverTimeout} =
ProxyInfoData.validate(proxyDataList[proxyDataListIndex]);
if (type === PROXY_TYPES.DIRECT) {
return defaultProxyInfo;
}
let failoverProxy = this.createProxyInfoFromData(proxyDataList, defaultProxyInfo, proxyDataListIndex + 1);
// TODO When Bug 1360404 is fixed use ProxyService.newProxyInfoWithAuth() for all types.
if (type === PROXY_TYPES.SOCKS || type === PROXY_TYPES.SOCKS4) {
return ProxyService.newProxyInfoWithAuth(
type, host, port, username, password,
proxyDNS ? TRANSPARENT_PROXY_RESOLVES_HOST : 0,
failoverTimeout ? failoverTimeout : PROXY_TIMEOUT_SEC,
failoverProxy);
}
return ProxyService.newProxyInfo(
type, host, port,
proxyDNS ? TRANSPARENT_PROXY_RESOLVES_HOST : 0,
failoverTimeout ? failoverTimeout : PROXY_TIMEOUT_SEC,
failoverProxy);
},
/**
* Creates a new proxy info data object using the return value of FindProxyForURL.
*
* @param {Array<string>} rule A single proxy rule returned by FindProxyForURL.
* (e.g. "PROXY 1.2.3.4:8080", "SOCKS 1.1.1.1:9090" or "DIRECT")
* @returns {nsIProxyInfo} The proxy info to apply for the given URI.
*/
parseProxyInfoDataFromPAC(rule) {
if (!rule) {
throw new ExtensionError("FindProxyForURL: Missing Proxy Rule");
}
let parts = rule.toLowerCase().split(/\s+/);
if (!parts[0] || parts.length > 2) {
throw new ExtensionError(`FindProxyForURL: Invalid arguments passed for proxy rule: "${rule}"`);
}
let type = parts[0];
let [host, port] = parts.length > 1 ? parts[1].split(":") : [];
switch (PROXY_TYPES[type.toUpperCase()]) {
case PROXY_TYPES.HTTP:
case PROXY_TYPES.HTTPS:
case PROXY_TYPES.SOCKS:
case PROXY_TYPES.SOCKS4:
if (!host || !port) {
throw new ExtensionError(`FindProxyForURL: Invalid host or port from proxy rule: "${rule}"`);
}
return {type, host, port};
case PROXY_TYPES.DIRECT:
if (host || port) {
throw new ExtensionError(`FindProxyForURL: Invalid argument for proxy type: "${type}"`);
}
return {type};
default:
throw new ExtensionError(`FindProxyForURL: Unrecognized proxy type: "${type}"`);
}
},
};
class ProxyScriptContext extends BaseContext {
constructor(extension, url, contextInfo = {}) {
super("proxy_script", extension);
this.contextInfo = contextInfo;
this.extension = extension;
this.messageManager = Services.cpmm;
this.sandbox = Cu.Sandbox(this.extension.principal, {
sandboxName: `Extension Proxy Script (${extension.policy.debugName}): ${url}`,
metadata: {addonID: extension.id},
});
this.url = url;
this.FindProxyForURL = null;
}
/**
* Loads and validates a proxy script into the sandbox, and then
* registers a new proxy filter for the context.
*
* @returns {boolean} true if load succeeded; false otherwise.
*/
load() {
Schemas.exportLazyGetter(this.sandbox, "browser", () => this.browserObj);
try {
Services.scriptloader.loadSubScript(this.url, this.sandbox, "UTF-8");
} catch (error) {
this.extension.emit("proxy-error", {
message: this.normalizeError(error).message,
});
return false;
}
this.FindProxyForURL = Cu.unwaiveXrays(this.sandbox.FindProxyForURL);
if (typeof this.FindProxyForURL !== "function") {
this.extension.emit("proxy-error", {
message: "The proxy script must define FindProxyForURL as a function",
});
return false;
}
ProxyService.registerFilter(
this /* nsIProtocolProxyFilter aFilter */,
0 /* unsigned long aPosition */
);
return true;
}
get principal() {
return this.extension.principal;
}
get cloneScope() {
return this.sandbox;
}
proxyInfoFromProxyData(proxyData, defaultProxyInfo) {
switch (typeof proxyData) {
case "string":
let proxyRules = [];
try {
for (let result of proxyData.split(";")) {
proxyRules.push(ProxyInfoData.parseProxyInfoDataFromPAC(result.trim()));
}
} catch (e) {
// If we have valid proxies already, lets use them and just emit
// errors for the failovers.
if (proxyRules.length === 0) {
throw e;
}
let error = this.normalizeError(e);
this.extension.emit("proxy-error", {
message: error.message,
fileName: error.fileName,
lineNumber: error.lineNumber,
stack: error.stack,
});
}
proxyData = proxyRules;
// fall through
case "object":
if (Array.isArray(proxyData) && proxyData.length > 0) {
return ProxyInfoData.createProxyInfoFromData(proxyData, defaultProxyInfo);
}
// Not an array, fall through to error.
default:
throw new ExtensionError("FindProxyForURL: Return type must be a string or array of objects");
}
}
/**
* This method (which is required by the nsIProtocolProxyService interface)
* is called to apply proxy filter rules for the given URI and proxy object
* (or list of proxy objects).
*
* @param {Object} service A reference to the Protocol Proxy Service.
* @param {Object} uri The URI for which these proxy settings apply.
* @param {Object} defaultProxyInfo The proxy (or list of proxies) that
* would be used by default for the given URI. This may be null.
* @returns {Object} The proxy info to apply for the given URI.
*/
applyFilter(service, uri, defaultProxyInfo) {
try {
// TODO Bug 1337001 - provide path and query components to non-https URLs.
let ret = this.FindProxyForURL(uri.prePath, uri.host, this.contextInfo);
return this.proxyInfoFromProxyData(ret, defaultProxyInfo);
} catch (e) {
let error = this.normalizeError(e);
this.extension.emit("proxy-error", {
message: error.message,
fileName: error.fileName,
lineNumber: error.lineNumber,
stack: error.stack,
});
}
return defaultProxyInfo;
}
/**
* Unloads the proxy filter and shuts down the sandbox.
*/
unload() {
super.unload();
ProxyService.unregisterFilter(this);
Cu.nukeSandbox(this.sandbox);
this.sandbox = null;
}
}
class ProxyScriptAPIManager extends SchemaAPIManager {
constructor() {
super("proxy", Schemas);
this.initialized = false;
}
lazyInit() {
if (!this.initialized) {
this.initGlobal();
let entries = XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCRIPTS_CONTENT);
for (let [/* name */, value] of entries) {
this.loadScript(value);
}
this.initialized = true;
}
}
}
class ProxyScriptInjectionContext {
constructor(context, apiCan) {
this.context = context;
this.localAPIs = apiCan.root;
this.apiCan = apiCan;
}
shouldInject(namespace, name, allowedContexts) {
if (this.context.envType !== "proxy_script") {
throw new Error(`Unexpected context type "${this.context.envType}"`);
}
// Do not generate proxy script APIs unless explicitly allowed.
return allowedContexts.includes("proxy");
}
getImplementation(namespace, name) {
this.apiCan.findAPIPath(`${namespace}.${name}`);
let obj = this.apiCan.findAPIPath(namespace);
if (obj && name in obj) {
return new LocalAPIImplementation(obj, name, this.context);
}
}
get cloneScope() {
return this.context.cloneScope;
}
get principal() {
return this.context.principal;
}
}
defineLazyGetter(ProxyScriptContext.prototype, "messenger", function() {
let sender = {id: this.extension.id, frameId: this.frameId, url: this.url};
let filter = {extensionId: this.extension.id, toProxyScript: true};
return new ExtensionChild.Messenger(this, [this.messageManager], sender, filter);
});
let proxyScriptAPIManager = new ProxyScriptAPIManager();
defineLazyGetter(ProxyScriptContext.prototype, "browserObj", function() {
let localAPIs = {};
let can = new CanOfAPIs(this, proxyScriptAPIManager, localAPIs);
proxyScriptAPIManager.lazyInit();
let browserObj = Cu.createObjectIn(this.sandbox);
let injectionContext = new ProxyScriptInjectionContext(this, can);
proxyScriptAPIManager.schema.inject(browserObj, injectionContext);
return browserObj;
});