summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/parent/ext-proxy.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/extensions/parent/ext-proxy.js')
-rw-r--r--toolkit/components/extensions/parent/ext-proxy.js335
1 files changed, 335 insertions, 0 deletions
diff --git a/toolkit/components/extensions/parent/ext-proxy.js b/toolkit/components/extensions/parent/ext-proxy.js
new file mode 100644
index 0000000000..86505f9423
--- /dev/null
+++ b/toolkit/components/extensions/parent/ext-proxy.js
@@ -0,0 +1,335 @@
+/* -*- 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";
+
+ChromeUtils.defineESModuleGetters(this, {
+ ProxyChannelFilter: "resource://gre/modules/ProxyChannelFilter.sys.mjs",
+});
+
+// Delayed wakeup is tied to ExtensionParent.browserPaintedPromise, which is
+// when the first browser window has been painted. On Android, parts of the
+// browser can trigger requests without browser "window" (geckoview.xhtml).
+// Therefore we allow such proxy events to trigger wakeup.
+// On desktop, we do not wake up early, to minimize the amount of work before
+// a browser window is painted.
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "isEarlyWakeupOnRequestEnabled",
+ "extensions.webextensions.early_background_wakeup_on_request",
+ false
+);
+var { ExtensionPreferencesManager } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionPreferencesManager.sys.mjs"
+);
+
+var { ExtensionError } = ExtensionUtils;
+var { getSettingsAPI } = ExtensionPreferencesManager;
+
+const proxySvc = Ci.nsIProtocolProxyService;
+
+const PROXY_TYPES_MAP = new Map([
+ ["none", proxySvc.PROXYCONFIG_DIRECT],
+ ["autoDetect", proxySvc.PROXYCONFIG_WPAD],
+ ["system", proxySvc.PROXYCONFIG_SYSTEM],
+ ["manual", proxySvc.PROXYCONFIG_MANUAL],
+ ["autoConfig", proxySvc.PROXYCONFIG_PAC],
+]);
+
+const DEFAULT_PORTS = new Map([
+ ["http", 80],
+ ["ssl", 443],
+ ["socks", 1080],
+]);
+
+ExtensionPreferencesManager.addSetting("proxy.settings", {
+ permission: "proxy",
+ prefNames: [
+ "network.proxy.type",
+ "network.proxy.http",
+ "network.proxy.http_port",
+ "network.proxy.share_proxy_settings",
+ "network.proxy.ssl",
+ "network.proxy.ssl_port",
+ "network.proxy.socks",
+ "network.proxy.socks_port",
+ "network.proxy.socks_version",
+ "network.proxy.socks_remote_dns",
+ "network.proxy.no_proxies_on",
+ "network.proxy.autoconfig_url",
+ "signon.autologin.proxy",
+ "network.http.proxy.respect-be-conservative",
+ ],
+
+ setCallback(value) {
+ let prefs = {
+ "network.proxy.type": PROXY_TYPES_MAP.get(value.proxyType),
+ "signon.autologin.proxy": value.autoLogin,
+ "network.proxy.socks_remote_dns": value.proxyDNS,
+ "network.proxy.autoconfig_url": value.autoConfigUrl,
+ "network.proxy.share_proxy_settings": value.httpProxyAll,
+ "network.proxy.socks_version": value.socksVersion,
+ "network.proxy.no_proxies_on": value.passthrough,
+ "network.http.proxy.respect-be-conservative": value.respectBeConservative,
+ };
+
+ for (let prop of ["http", "ssl", "socks"]) {
+ if (value[prop]) {
+ let url = new URL(`http://${value[prop]}`);
+ prefs[`network.proxy.${prop}`] = url.hostname;
+ // Only fall back to defaults if no port provided.
+ let [, rawPort] = value[prop].split(":");
+ let port = parseInt(rawPort, 10) || DEFAULT_PORTS.get(prop);
+ prefs[`network.proxy.${prop}_port`] = port;
+ }
+ }
+
+ return prefs;
+ },
+});
+
+function registerProxyFilterEvent(
+ context,
+ extension,
+ fire,
+ filterProps,
+ extraInfoSpec = []
+) {
+ let listener = data => {
+ if (isEarlyWakeupOnRequestEnabled && fire.wakeup) {
+ // Starts the background script if it has not started, no-op otherwise.
+ extension.emit("start-background-script");
+ }
+ return fire.sync(data);
+ };
+
+ let filter = { ...filterProps };
+ if (filter.urls) {
+ let perms = new MatchPatternSet([
+ ...extension.allowedOrigins.patterns,
+ ...extension.optionalOrigins.patterns,
+ ]);
+ filter.urls = new MatchPatternSet(filter.urls);
+
+ if (!perms.overlapsAll(filter.urls)) {
+ Cu.reportError(
+ "The proxy.onRequest filter doesn't overlap with host permissions."
+ );
+ }
+ }
+
+ let proxyFilter = new ProxyChannelFilter(
+ context,
+ extension,
+ listener,
+ filter,
+ extraInfoSpec
+ );
+ return {
+ unregister: () => {
+ proxyFilter.destroy();
+ },
+ convert(_fire, _context) {
+ fire = _fire;
+ proxyFilter.context = _context;
+ },
+ };
+}
+
+this.proxy = class extends ExtensionAPIPersistent {
+ PERSISTENT_EVENTS = {
+ onRequest({ fire, context }, params) {
+ return registerProxyFilterEvent(context, this.extension, fire, ...params);
+ },
+ };
+
+ getAPI(context) {
+ let { extension } = context;
+ let self = this;
+
+ return {
+ proxy: {
+ onRequest: new EventManager({
+ context,
+ module: "proxy",
+ event: "onRequest",
+ extensionApi: self,
+ }).api(),
+
+ // Leaving as non-persistent. By itself it's not useful since proxy-error
+ // is emitted from the proxy filter.
+ onError: new EventManager({
+ context,
+ name: "proxy.onError",
+ register: fire => {
+ let listener = (name, error) => {
+ fire.async(error);
+ };
+ extension.on("proxy-error", listener);
+ return () => {
+ extension.off("proxy-error", listener);
+ };
+ },
+ }).api(),
+
+ settings: Object.assign(
+ getSettingsAPI({
+ context,
+ name: "proxy.settings",
+ callback() {
+ let prefValue = Services.prefs.getIntPref("network.proxy.type");
+ let proxyConfig = {
+ proxyType: Array.from(PROXY_TYPES_MAP.entries()).find(
+ entry => entry[1] === prefValue
+ )[0],
+ autoConfigUrl: Services.prefs.getCharPref(
+ "network.proxy.autoconfig_url"
+ ),
+ autoLogin: Services.prefs.getBoolPref("signon.autologin.proxy"),
+ proxyDNS: Services.prefs.getBoolPref(
+ "network.proxy.socks_remote_dns"
+ ),
+ httpProxyAll: Services.prefs.getBoolPref(
+ "network.proxy.share_proxy_settings"
+ ),
+ socksVersion: Services.prefs.getIntPref(
+ "network.proxy.socks_version"
+ ),
+ passthrough: Services.prefs.getCharPref(
+ "network.proxy.no_proxies_on"
+ ),
+ };
+
+ if (extension.isPrivileged) {
+ proxyConfig.respectBeConservative = Services.prefs.getBoolPref(
+ "network.http.proxy.respect-be-conservative"
+ );
+ }
+
+ for (let prop of ["http", "ssl", "socks"]) {
+ let host = Services.prefs.getCharPref(`network.proxy.${prop}`);
+ let port = Services.prefs.getIntPref(
+ `network.proxy.${prop}_port`
+ );
+ proxyConfig[prop] = port ? `${host}:${port}` : host;
+ }
+
+ return proxyConfig;
+ },
+ // proxy.settings is unsupported on android.
+ validate() {
+ if (AppConstants.platform == "android") {
+ throw new ExtensionError(
+ `proxy.settings is not supported on android.`
+ );
+ }
+ },
+ }),
+ {
+ set: details => {
+ if (AppConstants.platform === "android") {
+ throw new ExtensionError(
+ "proxy.settings is not supported on android."
+ );
+ }
+
+ if (!extension.privateBrowsingAllowed) {
+ throw new ExtensionError(
+ "proxy.settings requires private browsing permission."
+ );
+ }
+
+ if (!Services.policies.isAllowed("changeProxySettings")) {
+ throw new ExtensionError(
+ "Proxy settings are being managed by the Policies manager."
+ );
+ }
+
+ let value = details.value;
+
+ // proxyType is optional and it should default to "system" when missing.
+ if (value.proxyType == null) {
+ value.proxyType = "system";
+ }
+
+ if (!PROXY_TYPES_MAP.has(value.proxyType)) {
+ throw new ExtensionError(
+ `${value.proxyType} is not a valid value for proxyType.`
+ );
+ }
+
+ if (value.httpProxyAll) {
+ // Match what about:preferences does with proxy settings
+ // since the proxy service does not check the value
+ // of share_proxy_settings.
+ value.ssl = value.http;
+ }
+
+ for (let prop of ["http", "ssl", "socks"]) {
+ let host = value[prop];
+ if (host) {
+ try {
+ // Fixup in case a full url is passed.
+ if (host.includes("://")) {
+ value[prop] = new URL(host).host;
+ } else {
+ // Validate the host value.
+ new URL(`http://${host}`);
+ }
+ } catch (e) {
+ throw new ExtensionError(
+ `${value[prop]} is not a valid value for ${prop}.`
+ );
+ }
+ }
+ }
+
+ if (value.proxyType === "autoConfig" || value.autoConfigUrl) {
+ try {
+ new URL(value.autoConfigUrl);
+ } catch (e) {
+ throw new ExtensionError(
+ `${value.autoConfigUrl} is not a valid value for autoConfigUrl.`
+ );
+ }
+ }
+
+ if (value.socksVersion !== undefined) {
+ if (
+ !Number.isInteger(value.socksVersion) ||
+ value.socksVersion < 4 ||
+ value.socksVersion > 5
+ ) {
+ throw new ExtensionError(
+ `${value.socksVersion} is not a valid value for socksVersion.`
+ );
+ }
+ }
+
+ if (
+ value.respectBeConservative !== undefined &&
+ !extension.isPrivileged &&
+ Services.prefs.getBoolPref(
+ "network.http.proxy.respect-be-conservative"
+ ) != value.respectBeConservative
+ ) {
+ throw new ExtensionError(
+ `respectBeConservative can be set by privileged extensions only.`
+ );
+ }
+
+ return ExtensionPreferencesManager.setSetting(
+ extension.id,
+ "proxy.settings",
+ value
+ );
+ },
+ }
+ ),
+ },
+ };
+ }
+};