--- a/toolkit/components/extensions/ProxyScriptContext.jsm
+++ b/toolkit/components/extensions/ProxyScriptContext.jsm
@@ -1,32 +1,38 @@
/* -*- 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";
-var EXPORTED_SYMBOLS = ["ProxyScriptContext"];
+var EXPORTED_SYMBOLS = ["ProxyScriptContext", "ProxyChannelFilter"];
-/* exported ProxyScriptContext */
+/* exported ProxyScriptContext, ProxyChannelFilter */
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, "ExtensionParent",
+ "resource://gre/modules/ExtensionParent.jsm");
ChromeUtils.defineModuleGetter(this, "Schemas",
"resource://gre/modules/Schemas.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "ProxyService",
"@mozilla.org/network/protocol-proxy-service;1",
"nsIProtocolProxyService");
+XPCOMUtils.defineLazyGetter(this, "tabTracker", () => {
+ return ExtensionParent.apiManager.global.tabTracker;
+});
+
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;
@@ -60,88 +66,92 @@ const ProxyInfoData = {
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}"`);
+ throw new ExtensionError(`ProxyInfoData: 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}"`);
+ throw new ExtensionError(`ProxyInfoData: Invalid proxy server host: "${host}"`);
}
if (!host.length) {
- throw new ExtensionError("FindProxyForURL: Proxy server host cannot be empty");
+ throw new ExtensionError("ProxyInfoData: 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}"`);
+ throw new ExtensionError(`ProxyInfoData: Invalid proxy server port: "${port}"`);
}
if (port < 1 || port > 0xffff) {
- throw new ExtensionError(`FindProxyForURL: Proxy server port ${port} outside range 1 to 65535`);
+ throw new ExtensionError(`ProxyInfoData: 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}"`);
+ throw new ExtensionError(`ProxyInfoData: 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}"`);
+ throw new ExtensionError(`ProxyInfoData: 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}"`);
+ throw new ExtensionError(`ProxyInfoData: 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`);
+ throw new ExtensionError(`ProxyInfoData: 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}"`);
+ throw new ExtensionError(`ProxyInfoData: Invalid failover timeout: "${failoverTimeout}"`);
}
},
createProxyInfoFromData(proxyDataList, defaultProxyInfo, proxyDataListIndex = 0) {
if (proxyDataListIndex >= proxyDataList.length) {
return defaultProxyInfo;
}
+ let proxyData = proxyDataList[proxyDataListIndex];
+ if (proxyData == null) {
+ return null;
+ }
let {type, host, port, username, password, proxyDNS, failoverTimeout} =
- ProxyInfoData.validate(proxyDataList[proxyDataListIndex]);
+ ProxyInfoData.validate(proxyData);
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(
@@ -155,47 +165,199 @@ const ProxyInfoData = {
* 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");
+ throw new ExtensionError("ProxyInfoData: 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}"`);
+ throw new ExtensionError(`ProxyInfoData: 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}"`);
+ throw new ExtensionError(`ProxyInfoData: 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}"`);
+ throw new ExtensionError(`ProxyInfoData: Invalid argument for proxy type: "${type}"`);
}
return {type};
default:
- throw new ExtensionError(`FindProxyForURL: Unrecognized proxy type: "${type}"`);
+ throw new ExtensionError(`ProxyInfoData: Unrecognized proxy type: "${type}"`);
}
},
+ proxyInfoFromProxyData(context, 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 = context.normalizeError(e);
+ context.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("ProxyInfoData: proxyData must be a string or array of objects");
+ }
+ },
};
+function normalizeFilter(filter) {
+ if (!filter) {
+ filter = {};
+ }
+
+ return {urls: filter.urls || null, types: filter.types || null};
+}
+
+class ProxyChannelFilter {
+ constructor(context, listener, filter, extraInfoSpec) {
+ this.context = context;
+ this.filter = normalizeFilter(filter);
+ this.listener = listener;
+ this.extraInfoSpec = extraInfoSpec || [];
+
+ ProxyService.registerChannelFilter(
+ this /* nsIProtocolProxyChannelFilter aFilter */,
+ 0 /* unsigned long aPosition */
+ );
+ }
+
+ // Copy from WebRequest.jsm with small changes.
+ getRequestData(channel, extraData) {
+ let data = {
+ requestId: String(channel.id),
+ url: channel.finalURL,
+ method: channel.method,
+ type: channel.type,
+ fromCache: !!channel.fromCache,
+
+ originUrl: channel.originURL || undefined,
+ documentUrl: channel.documentURL || undefined,
+
+ frameId: channel.windowId,
+ parentFrameId: channel.parentWindowId,
+
+ frameAncestors: channel.frameAncestors || undefined,
+
+ timeStamp: Date.now(),
+
+ ...extraData,
+ };
+ if (this.extraInfoSpec.includes("requestHeaders")) {
+ data.requestHeaders = channel.getRequestHeaders();
+ }
+ return data;
+ }
+
+ /**
+ * 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 {nsIProtocolProxyService} service A reference to the Protocol Proxy Service.
+ * @param {nsIChannel} channel The channel for which these proxy settings apply.
+ * @param {nsIProxyInfo} defaultProxyInfo The proxy (or list of proxies) that
+ * would be used by default for the given URI. This may be null.
+ * @param {nsIProtocolProxyChannelFilter} proxyFilter
+ */
+ async applyFilter(service, channel, defaultProxyInfo, proxyFilter) {
+ let proxyInfo;
+ try {
+ let wrapper = ChannelWrapper.get(channel);
+
+ let browserData = {tabId: -1, windowId: -1};
+ if (wrapper.browserElement) {
+ browserData = tabTracker.getBrowserData(wrapper.browserElement);
+ }
+ let {filter} = this;
+ if (filter.tabId != null && browserData.tabId !== filter.tabId) {
+ return;
+ }
+ if (filter.windowId != null && browserData.windowId !== filter.windowId) {
+ return;
+ }
+
+ if (wrapper.matches(filter, this.context.extension.policy, {isProxy: true})) {
+ let data = this.getRequestData(wrapper, {tabId: browserData.tabId});
+
+ let ret = await this.listener(data);
+ if (ret == null) {
+ // If ret undefined or null, fall through to the `finally` block to apply the proxy result.
+ proxyInfo = ret;
+ return;
+ }
+ // We only accept proxyInfo objects, not the PAC strings. ProxyInfoData will
+ // accept either, so we want to enforce the limit here.
+ if (typeof ret !== "object") {
+ throw new ExtensionError("ProxyInfoData: proxyData must be an object or array of objects");
+ }
+ // We allow the call to return either a single proxyInfo or an array of proxyInfo.
+ if (!Array.isArray(ret)) {
+ ret = [ret];
+ }
+ proxyInfo = ProxyInfoData.createProxyInfoFromData(ret, defaultProxyInfo);
+ }
+ } catch (e) {
+ let error = this.context.normalizeError(e);
+ this.context.extension.emit("proxy-error", {
+ message: error.message,
+ fileName: error.fileName,
+ lineNumber: error.lineNumber,
+ stack: error.stack,
+ });
+ } finally {
+ // We must call onProxyFilterResult. proxyInfo may be null or nsIProxyInfo.
+ // defaultProxyInfo will be null unless a prior proxy handler has set something.
+ // If proxyInfo is null, that removes any prior proxy config. This allows a
+ // proxy extension to override higher level (e.g. prefs) config under certain
+ // circumstances.
+ proxyFilter.onProxyFilterResult(proxyInfo !== undefined ? proxyInfo : defaultProxyInfo);
+ }
+ }
+
+ destroy() {
+ ProxyService.unregisterFilter(this);
+ }
+}
+
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}`,
@@ -242,67 +404,33 @@ class ProxyScriptContext extends BaseCon
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.
* @param {Object} callback nsIProxyProtocolFilterResult to call onProxyFilterResult
on with the proxy info to apply for the given URI.
*/
applyFilter(service, uri, defaultProxyInfo, callback) {
try {
// TODO Bug 1337001 - provide path and query components to non-https URLs.
let ret = this.FindProxyForURL(uri.prePath, uri.host, this.contextInfo);
- ret = this.proxyInfoFromProxyData(ret, defaultProxyInfo);
+ ret = ProxyInfoData.proxyInfoFromProxyData(this, ret, defaultProxyInfo);
callback.onProxyFilterResult(ret);
} catch (e) {
let error = this.normalizeError(e);
this.extension.emit("proxy-error", {
message: error.message,
fileName: error.fileName,
lineNumber: error.lineNumber,
stack: error.stack,
--- a/toolkit/components/extensions/ext-proxy.js
+++ b/toolkit/components/extensions/ext-proxy.js
@@ -7,33 +7,79 @@
// The ext-* files are imported into the same scopes.
/* import-globals-from ext-toolkit.js */
"use strict";
ChromeUtils.defineModuleGetter(this, "ProxyScriptContext",
"resource://gre/modules/ProxyScriptContext.jsm");
+ChromeUtils.defineModuleGetter(this, "ProxyChannelFilter",
+ "resource://gre/modules/ProxyScriptContext.jsm");
// WeakMap[Extension -> ProxyScriptContext]
-let proxyScriptContextMap = new WeakMap();
+const proxyScriptContextMap = new WeakMap();
+
+// EventManager-like class specifically for Proxy filters. Inherits from
+// EventManager. Takes care of converting |details| parameter
+// when invoking listeners.
+class ProxyFilterEventManager extends EventManager {
+ constructor(context, eventName) {
+ let name = `proxy.${eventName}`;
+ let register = (fire, filterProps, extraInfoSpec = []) => {
+ let listener = (data) => {
+ return fire.sync(data);
+ };
+
+ let filter = {...filterProps};
+ if (filter.urls) {
+ let perms = new MatchPatternSet([...context.extension.whiteListedHosts.patterns,
+ ...context.extension.optionalOrigins.patterns]);
+
+ filter.urls = new MatchPatternSet(filter.urls);
+
+ if (!perms.overlapsAll(filter.urls)) {
+ throw new context.cloneScope.Error("The proxy.addListener filter doesn't overlap with host permissions.");
+ }
+ }
+
+ let proxyFilter = new ProxyChannelFilter(context, listener, filter, extraInfoSpec);
+ return () => {
+ proxyFilter.destroy();
+ };
+ };
+
+ super(context, name, register);
+ }
+}
this.proxy = class extends ExtensionAPI {
onShutdown() {
let {extension} = this;
let proxyScriptContext = proxyScriptContextMap.get(extension);
if (proxyScriptContext) {
proxyScriptContext.unload();
proxyScriptContextMap.delete(extension);
}
}
getAPI(context) {
let {extension} = context;
+
+ let onError = new EventManager(context, "proxy.onError", fire => {
+ let listener = (name, error) => {
+ fire.async(error);
+ };
+ extension.on("proxy-error", listener);
+ return () => {
+ extension.off("proxy-error", listener);
+ };
+ }).api();
+
return {
proxy: {
register(url) {
this.unregister();
let proxyScriptContext = new ProxyScriptContext(extension, url);
if (proxyScriptContext.load()) {
proxyScriptContextMap.set(extension, proxyScriptContext);
@@ -47,21 +93,18 @@ this.proxy = class extends ExtensionAPI
proxyScriptContextMap.delete(extension);
}
},
registerProxyScript(url) {
this.register(url);
},
- onProxyError: new EventManager(context, "proxy.onProxyError", fire => {
- let listener = (name, error) => {
- fire.async(error);
- };
- extension.on("proxy-error", listener);
- return () => {
- extension.off("proxy-error", listener);
- };
- }).api(),
+ onRequest: new ProxyFilterEventManager(context, "onRequest").api(),
+
+ onError,
+
+ // TODO Bug 1388619 deprecate onProxyError.
+ onProxyError: onError,
},
};
}
};
--- a/toolkit/components/extensions/schemas/proxy.json
+++ b/toolkit/components/extensions/schemas/proxy.json
@@ -50,19 +50,73 @@
"type": "string",
"format": "strictRelativeUrl"
}
]
}
],
"events": [
{
+ "name": "onRequest",
+ "type": "function",
+ "description": "Fired when proxy data is needed for a request.",
+ "parameters": [
+ {
+ "type": "object",
+ "name": "details",
+ "properties": {
+ "requestId": {"type": "string", "description": "The ID of the request. Request IDs are unique within a browser session. As a result, they could be used to relate different events of the same request."},
+ "url": {"type": "string"},
+ "method": {"type": "string", "description": "Standard HTTP method."},
+ "frameId": {"type": "integer", "description": "The value 0 indicates that the request happens in the main frame; a positive value indicates the ID of a subframe in which the request happens. If the document of a (sub-)frame is loaded (<code>type</code> is <code>main_frame</code> or <code>sub_frame</code>), <code>frameId</code> indicates the ID of this frame, not the ID of the outer frame. Frame IDs are unique within a tab."},
+ "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."},
+ "originUrl": {"type": "string", "optional": true, "description": "URL of the resource that triggered this request."},
+ "documentUrl": {"type": "string", "optional": true, "description": "URL of the page into which the requested resource will be loaded."},
+ "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."},
+ "type": {"$ref": "webRequest.ResourceType", "description": "How the requested resource will be used."},
+ "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."},
+ "ip": {"type": "string", "optional": true, "description": "The server IP address that the request was actually sent to. Note that it may be a literal IPv6 address."},
+ "fromCache": {"type": "boolean", "description": "Indicates if this response was fetched from disk cache."},
+ "requestHeaders": {"$ref": "webRequest.HttpHeaders", "optional": true, "description": "The HTTP request headers that are going to be sent out with this request."}
+ }
+ }
+ ],
+ "extraParameters": [
+ {
+ "$ref": "webRequest.RequestFilter",
+ "name": "filter",
+ "description": "A set of filters that restricts the events that will be sent to this listener."
+ },
+ {
+ "type": "array",
+ "optional": true,
+ "name": "extraInfoSpec",
+ "description": "Array of extra information that should be passed to the listener function.",
+ "items": {
+ "type": "string",
+ "enum": ["requestHeaders"]
+ }
+ }
+ ]
+ },
+ {
+ "name": "onError",
+ "type": "function",
+ "description": "Notifies about proxy script errors.",
+ "parameters": [
+ {
+ "name": "error",
+ "type": "object"
+ }
+ ]
+ },
+ {
"name": "onProxyError",
"type": "function",
- "description": "Notifies about proxy script errors.",
+ "description": "Please use $(ref:proxy.onError).",
"parameters": [
{
"name": "error",
"type": "object"
}
]
}
]
copy from toolkit/components/extensions/test/xpcshell/test_ext_proxy_auth.js
copy to toolkit/components/extensions/test/xpcshell/test_ext_proxy_onauthrequired.js
--- a/toolkit/components/extensions/test/xpcshell/test_ext_proxy_auth.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_proxy_onauthrequired.js
@@ -26,91 +26,98 @@ function getExtension(background) {
permissions: [
"proxy",
"webRequest",
"webRequestBlocking",
"<all_urls>",
],
},
background: `(${background})(${proxy.identity.primaryPort})`,
- files: {
- "proxy.js": `
- function FindProxyForURL(url, host) {
- return "PROXY localhost:${proxy.identity.primaryPort}; DIRECT";
- }`,
- },
});
}
+
add_task(async function test_webRequest_auth_proxy() {
async function background(port) {
- browser.webRequest.onBeforeRequest.addListener(details => {
- browser.test.log(`details ${JSON.stringify(details)}\n`);
+ browser.webRequest.onBeforeSendHeaders.addListener(details => {
+ browser.test.log(`onBeforeSendHeaders ${JSON.stringify(details)}\n`);
browser.test.assertEq("localhost", details.proxyInfo.host, "proxy host");
browser.test.assertEq(port, details.proxyInfo.port, "proxy port");
browser.test.assertEq("http", details.proxyInfo.type, "proxy type");
browser.test.assertEq("", details.proxyInfo.username, "proxy username not set");
}, {urls: ["<all_urls>"]});
+
browser.webRequest.onAuthRequired.addListener(details => {
+ browser.test.log(`onAuthRequired ${JSON.stringify(details)}\n`);
browser.test.assertTrue(details.isProxy, "proxied request");
browser.test.assertEq("localhost", details.proxyInfo.host, "proxy host");
browser.test.assertEq(port, details.proxyInfo.port, "proxy port");
browser.test.assertEq("http", details.proxyInfo.type, "proxy type");
browser.test.assertEq("localhost", details.challenger.host, "proxy host");
browser.test.assertEq(port, details.challenger.port, "proxy port");
return {authCredentials: {username: "puser", password: "ppass"}};
}, {urls: ["<all_urls>"]}, ["blocking"]);
+
browser.webRequest.onCompleted.addListener(details => {
- browser.test.log(`details ${JSON.stringify(details)}\n`);
+ browser.test.log(`onCompleted ${JSON.stringify(details)}\n`);
browser.test.assertEq("localhost", details.proxyInfo.host, "proxy host");
browser.test.assertEq(port, details.proxyInfo.port, "proxy port");
browser.test.assertEq("http", details.proxyInfo.type, "proxy type");
browser.test.assertEq("", details.proxyInfo.username, "proxy username not set by onAuthRequired");
browser.test.assertEq(undefined, details.proxyInfo.password, "no proxy password");
browser.test.sendMessage("done");
}, {urls: ["<all_urls>"]});
- await browser.proxy.register("proxy.js");
- browser.test.sendMessage("pac-ready");
+ // Handle the proxy request.
+ browser.proxy.onRequest.addListener(details => {
+ browser.test.log(`onRequest ${JSON.stringify(details)}`);
+ return [{host: "localhost", port, type: "http"}];
+ }, {urls: ["<all_urls>"]}, ["requestHeaders"]);
+ browser.test.sendMessage("ready");
}
let handlingExt = getExtension(background);
await handlingExt.startup();
- await handlingExt.awaitMessage("pac-ready");
+ await handlingExt.awaitMessage("ready");
let contentPage = await ExtensionTestUtils.loadContentPage(`http://mozilla.org/`);
await handlingExt.awaitMessage("done");
await contentPage.close();
await handlingExt.unload();
});
add_task(async function test_webRequest_auth_proxy_system() {
async function background(port) {
browser.webRequest.onBeforeRequest.addListener(details => {
browser.test.fail("onBeforeRequest");
}, {urls: ["<all_urls>"]});
+
browser.webRequest.onAuthRequired.addListener(details => {
browser.test.sendMessage("onAuthRequired");
// cancel is silently ignored, if it were not (e.g someone messes up in
// WebRequest.jsm and allows cancel) this test would fail.
return {
cancel: true,
authCredentials: {username: "puser", password: "ppass"},
};
}, {urls: ["<all_urls>"]}, ["blocking"]);
- await browser.proxy.register("proxy.js");
- browser.test.sendMessage("pac-ready");
+ // Handle the proxy request.
+ browser.proxy.onRequest.addListener(details => {
+ browser.test.log(`onRequest ${JSON.stringify(details)}`);
+ return {host: "localhost", port, type: "http"};
+ }, {urls: ["<all_urls>"]});
+ browser.test.sendMessage("ready");
}
let handlingExt = getExtension(background);
await handlingExt.startup();
- await handlingExt.awaitMessage("pac-ready");
+ await handlingExt.awaitMessage("ready");
function fetch(url) {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.mozBackgroundRequest = true;
xhr.open("GET", url);
xhr.onload = () => { resolve(xhr.responseText); };
xhr.onerror = () => { reject(xhr.status); };
copy from toolkit/components/extensions/test/xpcshell/test_proxy_scripts.js
copy to toolkit/components/extensions/test/xpcshell/test_proxy_listener.js
--- a/toolkit/components/extensions/test/xpcshell/test_proxy_scripts.js
+++ b/toolkit/components/extensions/test/xpcshell/test_proxy_listener.js
@@ -1,165 +1,188 @@
"use strict";
-/* eslint no-unused-vars: ["error", {"args": "none", "varsIgnorePattern": "^(FindProxyForURL)$"}] */
-
ChromeUtils.import("resource://gre/modules/Extension.jsm");
ChromeUtils.import("resource://gre/modules/ProxyScriptContext.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "gProxyService",
"@mozilla.org/network/protocol-proxy-service;1",
"nsIProtocolProxyService");
+const TRANSPARENT_PROXY_RESOLVES_HOST = Ci.nsIProxyInfo.TRANSPARENT_PROXY_RESOLVES_HOST;
+
function getProxyInfo() {
return new Promise((resolve, reject) => {
let channel = NetUtil.newChannel({
uri: "http://www.mozilla.org/",
loadUsingSystemPrincipal: true,
});
gProxyService.asyncResolve(channel, 0, {
onProxyAvailable(req, uri, pi, status) {
resolve(pi);
},
});
});
}
-async function testProxyScript(script, expected = {}) {
- let scriptData = String(script).replace(/^.*?\{([^]*)\}$/, "$1");
+
+const testData = [
+ {
+ // An ExtensionError is thrown for this, but we are unable to catch it as we
+ // do with the PAC script api. In this case, we expect null for proxyInfo.
+ proxyInfo: "not_defined",
+ expected: {
+ proxyInfo: null,
+ },
+ },
+ {
+ proxyInfo: 1,
+ expected: {
+ error: {
+ message: "ProxyInfoData: proxyData must be an object or array of objects",
+ },
+ },
+ },
+ {
+ proxyInfo: [{type: "socks", host: "foo.bar", port: 1080, username: "johnsmith", password: "pass123", proxyDNS: true, failoverTimeout: 3},
+ {type: "http", host: "192.168.1.1", port: 3128}, {type: "https", host: "192.168.1.2", port: 1121, failoverTimeout: 1},
+ {type: "socks", host: "192.168.1.3", port: 1999, proxyDNS: true, username: "mungosantamaria", password: "foobar"}],
+ expected: {
+ proxyInfo: {
+ type: "socks",
+ host: "foo.bar",
+ port: 1080,
+ proxyDNS: true,
+ username: "johnsmith",
+ password: "pass123",
+ failoverTimeout: 3,
+ failoverProxy: {
+ host: "192.168.1.1",
+ port: 3128,
+ type: "http",
+ failoverProxy: {
+ host: "192.168.1.2",
+ port: 1121,
+ type: "https",
+ failoverTimeout: 1,
+ failoverProxy: {
+ host: "192.168.1.3",
+ port: 1999,
+ type: "socks",
+ proxyDNS: TRANSPARENT_PROXY_RESOLVES_HOST,
+ username: "mungosantamaria",
+ password: "foobar",
+ failoverProxy: {
+ type: "direct",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+];
+
+add_task(async function test_proxy_listener() {
let extensionData = {
manifest: {
- "permissions": ["proxy"],
+ "permissions": ["proxy", "<all_urls>"],
},
background() {
// Some tests generate multiple errors, we'll just rely on the first.
let seenError = false;
- browser.proxy.onProxyError.addListener(error => {
+ let proxyInfo;
+ browser.proxy.onError.addListener(error => {
if (!seenError) {
browser.test.sendMessage("proxy-error-received", error);
seenError = true;
}
});
- browser.test.onMessage.addListener(msg => {
- if (msg === "unregister-proxy-script") {
- browser.proxy.unregister().then(() => {
- browser.test.notifyPass("proxy");
- });
+ browser.proxy.onRequest.addListener(details => {
+ browser.test.log(`onRequest ${JSON.stringify(details)}`);
+ if (proxyInfo == "not_defined") {
+ return not_defined; // eslint-disable-line no-undef
+ }
+ return proxyInfo;
+ }, {urls: ["<all_urls>"]});
+
+ browser.test.onMessage.addListener((message, data) => {
+ if (message === "set-proxy") {
+ seenError = false;
+ proxyInfo = data.proxyInfo;
}
});
- browser.proxy.register("proxy.js").then(() => {
- browser.test.sendMessage("ready");
- });
- },
- files: {
- "proxy.js": scriptData,
+ browser.test.sendMessage("ready");
},
};
let extension = ExtensionTestUtils.loadExtension(extensionData);
await extension.startup();
await extension.awaitMessage("ready");
- let errorWait = extension.awaitMessage("proxy-error-received");
-
- let proxyInfo = await getProxyInfo();
+ for (let test of testData) {
+ extension.sendMessage("set-proxy", test);
+ let testError = test.expected.error;
+ let errorWait = testError && extension.awaitMessage("proxy-error-received");
- let error = await errorWait;
- equal(error.message, expected.message, "Correct error message received");
- if (!expected.proxyInfo) {
- equal(proxyInfo, null, "no proxyInfo received");
- } else {
- let {host, port, type} = expected.proxyInfo;
- equal(proxyInfo.host, host, `Expected proxy host to be ${host}`);
- equal(proxyInfo.port, port, `Expected proxy port to be ${port}`);
- equal(proxyInfo.type, type, `Expected proxy type to be ${type}`);
- }
- if (expected.errorInfo) {
- ok(error.fileName.includes("proxy.js"), "Error should include file name");
- equal(error.lineNumber, 3, "Error should include line number");
- ok(error.stack.includes("proxy.js:3:7"), "Error should include stack trace");
- }
- extension.sendMessage("unregister-proxy-script");
- await extension.awaitFinish("proxy");
- await extension.unload();
-}
-
-add_task(async function test_invalid_FindProxyForURL_function() {
- await testProxyScript(() => { }, {
- message: "The proxy script must define FindProxyForURL as a function",
- });
+ let proxyInfo = await getProxyInfo();
+ let expectedProxyInfo = test.expected.proxyInfo;
- await testProxyScript(() => {
- var FindProxyForURL = 5; // eslint-disable-line mozilla/var-only-at-top-level
- }, {
- message: "The proxy script must define FindProxyForURL as a function",
- });
-
- await testProxyScript(() => {
- function FindProxyForURL() {
- return not_defined; // eslint-disable-line no-undef
+ if (testError) {
+ info("waiting for error data");
+ let error = await errorWait;
+ equal(error.message, testError.message, "Correct error message received");
+ equal(proxyInfo, null, "no proxyInfo received");
+ } else if (expectedProxyInfo === null) {
+ equal(proxyInfo, null, "no proxyInfo received");
+ } else {
+ for (let proxyUsed = proxyInfo; proxyUsed; proxyUsed = proxyUsed.failoverProxy) {
+ let {type, host, port, username, password, proxyDNS, failoverTimeout} = expectedProxyInfo;
+ equal(proxyUsed.host, host, `Expected proxy host to be ${host}`);
+ equal(proxyUsed.port, port, `Expected proxy port to be ${port}`);
+ equal(proxyUsed.type, type, `Expected proxy type to be ${type}`);
+ // May be null or undefined depending on use of newProxyInfoWithAuth or newProxyInfo
+ equal(proxyUsed.username || "", username || "", `Expected proxy username to be ${username}`);
+ equal(proxyUsed.password || "", password || "", `Expected proxy password to be ${password}`);
+ equal(proxyUsed.flags, proxyDNS == undefined ? 0 : proxyDNS, `Expected proxyDNS to be ${proxyDNS}`);
+ // Default timeout is 10
+ equal(proxyUsed.failoverTimeout, failoverTimeout || 10, `Expected failoverTimeout to be ${failoverTimeout}`);
+ expectedProxyInfo = expectedProxyInfo.failoverProxy;
+ }
}
- }, {
- message: "not_defined is not defined",
- errorInfo: true,
- });
+ }
- // The following tests will produce multiple errors.
- await testProxyScript(() => {
- function FindProxyForURL() {
- return ";;;;;PROXY 1.2.3.4:8080";
- }
- }, {
- message: "FindProxyForURL: Missing Proxy Rule",
- });
-
- // We take any valid proxy up to the error.
- await testProxyScript(() => {
- function FindProxyForURL() {
- return "PROXY 1.2.3.4:8080; UNEXPECTED; SOCKS 1.2.3.4:8080";
- }
- }, {
- message: "FindProxyForURL: Unrecognized proxy type: \"unexpected\"",
- proxyInfo: {
- host: "1.2.3.4",
- port: "8080",
- type: "http",
- failoverProxy: null,
- },
- });
+ await extension.unload();
});
-async function getExtension(proxyResult) {
+async function getExtension(expectedProxyInfo) {
+ function background(proxyInfo) {
+ browser.test.log(`testing proxy.onRequest with proxyInfo = ${JSON.stringify(proxyInfo)}`);
+ browser.proxy.onRequest.addListener(details => {
+ return proxyInfo;
+ }, {urls: ["<all_urls>"]});
+ browser.test.sendMessage("ready");
+ }
let extensionData = {
manifest: {
- "permissions": ["proxy"],
- },
- background() {
- browser.proxy.register("proxy.js").then(() => {
- browser.test.sendMessage("ready");
- });
+ "permissions": ["proxy", "<all_urls>"],
},
- files: {
- "proxy.js": `
- function FindProxyForURL(url, host) {
- return ${proxyResult};
- }`,
- },
+ background: `(${background})(${JSON.stringify(expectedProxyInfo)})`,
};
let extension = ExtensionTestUtils.loadExtension(extensionData);
await extension.startup();
await extension.awaitMessage("ready");
return extension;
}
add_task(async function test_passthrough() {
let ext1 = await getExtension(null);
- let ext2 = await getExtension("\"PROXY 1.2.3.4:8888\"");
+ let ext2 = await getExtension({host: "1.2.3.4", port: 8888, type: "http"});
let proxyInfo = await getProxyInfo();
equal(proxyInfo.host, "1.2.3.4", `second extension won`);
equal(proxyInfo.port, "8888", `second extension won`);
equal(proxyInfo.type, "http", `second extension won`);
await ext2.unload();
--- a/toolkit/components/extensions/test/xpcshell/test_proxy_scripts.js
+++ b/toolkit/components/extensions/test/xpcshell/test_proxy_scripts.js
@@ -105,26 +105,26 @@ add_task(async function test_invalid_Fin
});
// The following tests will produce multiple errors.
await testProxyScript(() => {
function FindProxyForURL() {
return ";;;;;PROXY 1.2.3.4:8080";
}
}, {
- message: "FindProxyForURL: Missing Proxy Rule",
+ message: "ProxyInfoData: Missing Proxy Rule",
});
// We take any valid proxy up to the error.
await testProxyScript(() => {
function FindProxyForURL() {
return "PROXY 1.2.3.4:8080; UNEXPECTED; SOCKS 1.2.3.4:8080";
}
}, {
- message: "FindProxyForURL: Unrecognized proxy type: \"unexpected\"",
+ message: "ProxyInfoData: Unrecognized proxy type: \"unexpected\"",
proxyInfo: {
host: "1.2.3.4",
port: "8080",
type: "http",
failoverProxy: null,
},
});
});
--- a/toolkit/components/extensions/test/xpcshell/test_proxy_scripts_results.js
+++ b/toolkit/components/extensions/test/xpcshell/test_proxy_scripts_results.js
@@ -100,97 +100,97 @@ async function testProxyResolution(test)
}
}
add_task(async function test_pac_results() {
let tests = [
{
proxy: undefined,
expected: {
- error: "FindProxyForURL: Return type must be a string or array of objects",
+ error: "ProxyInfoData: proxyData must be a string or array of objects",
},
},
{
proxy: 5,
expected: {
- error: "FindProxyForURL: Return type must be a string or array of objects",
+ error: "ProxyInfoData: proxyData must be a string or array of objects",
},
},
{
proxy: "INVALID",
expected: {
- error: "FindProxyForURL: Unrecognized proxy type: \"invalid\"",
+ error: "ProxyInfoData: Unrecognized proxy type: \"invalid\"",
},
},
{
proxy: "SOCKS",
expected: {
- error: "FindProxyForURL: Invalid host or port from proxy rule: \"SOCKS\"",
+ error: "ProxyInfoData: Invalid host or port from proxy rule: \"SOCKS\"",
},
},
{
proxy: "PROXY 1.2.3.4:8080 EXTRA",
expected: {
- error: "FindProxyForURL: Invalid arguments passed for proxy rule: \"PROXY 1.2.3.4:8080 EXTRA\"",
+ error: "ProxyInfoData: Invalid arguments passed for proxy rule: \"PROXY 1.2.3.4:8080 EXTRA\"",
},
},
{
proxy: "PROXY :",
expected: {
- error: "FindProxyForURL: Invalid host or port from proxy rule: \"PROXY :\"",
+ error: "ProxyInfoData: Invalid host or port from proxy rule: \"PROXY :\"",
},
},
{
proxy: "PROXY :8080",
expected: {
- error: "FindProxyForURL: Invalid host or port from proxy rule: \"PROXY :8080\"",
+ error: "ProxyInfoData: Invalid host or port from proxy rule: \"PROXY :8080\"",
},
},
{
proxy: "PROXY ::",
expected: {
- error: "FindProxyForURL: Invalid host or port from proxy rule: \"PROXY ::\"",
+ error: "ProxyInfoData: Invalid host or port from proxy rule: \"PROXY ::\"",
},
},
{
proxy: "PROXY 1.2.3.4:",
expected: {
- error: "FindProxyForURL: Invalid host or port from proxy rule: \"PROXY 1.2.3.4:\"",
+ error: "ProxyInfoData: Invalid host or port from proxy rule: \"PROXY 1.2.3.4:\"",
},
},
{
proxy: "DIRECT 1.2.3.4:8080",
expected: {
- error: "FindProxyForURL: Invalid argument for proxy type: \"direct\"",
+ error: "ProxyInfoData: Invalid argument for proxy type: \"direct\"",
},
},
{
proxy: ["SOCKS foo.bar:1080", {type: "http", host: "foo.bar", port: 3128}],
expected: {
- error: "FindProxyForURL: Invalid proxy server type: \"undefined\"",
+ error: "ProxyInfoData: Invalid proxy server type: \"undefined\"",
},
},
{
proxy: {type: "socks", host: "foo.bar", port: 1080, username: "mungosantamaria", password: "pass123"},
expected: {
- error: "FindProxyForURL: Return type must be a string or array of objects",
+ error: "ProxyInfoData: proxyData must be a string or array of objects",
},
},
{
proxy: [{type: "pptp", host: "foo.bar", port: 1080, username: "mungosantamaria", password: "pass123", proxyDNS: true, failoverTimeout: 3},
{type: "http", host: "192.168.1.1", port: 1128, username: "mungosantamaria", password: "word321"}],
expected: {
- error: "FindProxyForURL: Invalid proxy server type: \"pptp\"",
+ error: "ProxyInfoData: Invalid proxy server type: \"pptp\"",
},
},
{
proxy: [{type: "http", host: "foo.bar", port: 65536, username: "mungosantamaria", password: "pass123", proxyDNS: true, failoverTimeout: 3},
{type: "http", host: "192.168.1.1", port: 3128, username: "mungosantamaria", password: "word321"}],
expected: {
- error: "FindProxyForURL: Proxy server port 65536 outside range 1 to 65535",
+ error: "ProxyInfoData: Proxy server port 65536 outside range 1 to 65535",
},
},
{
proxy: [{type: "direct"}],
expected: {
proxyInfo: null,
},
},
--- a/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
@@ -45,16 +45,17 @@ skip-if = os == "android" # checking for
skip-if = (os == "win" && !debug) #Bug 1419183 disable on Windows
[test_ext_management_uninstall_self.js]
[test_ext_onmessage_removelistener.js]
skip-if = true # This test no longer tests what it is meant to test.
[test_ext_privacy.js]
[test_ext_privacy_disable.js]
[test_ext_privacy_update.js]
[test_ext_proxy_auth.js]
+[test_ext_proxy_onauthrequired.js]
[test_ext_proxy_socks.js]
[test_ext_redirects.js]
[test_ext_runtime_connect_no_receiver.js]
[test_ext_runtime_getBrowserInfo.js]
[test_ext_runtime_getPlatformInfo.js]
[test_ext_runtime_onInstalled_and_onStartup.js]
skip-if = true # bug 1315829
[test_ext_runtime_sendMessage.js]
@@ -79,11 +80,12 @@ skip-if = os == "android" # checking for
[test_ext_trustworthy_origin.js]
[test_ext_topSites.js]
skip-if = os == "android"
[test_native_manifests.js]
subprocess = true
skip-if = os == "android"
[test_ext_permissions.js]
skip-if = os == "android" # Bug 1350559
+[test_proxy_listener.js]
[test_proxy_scripts.js]
[test_proxy_scripts_results.js]
[test_ext_brokenlinks.js]