forked from mirrors/gecko-dev
Initially reported and discussed in Bug 610896.
The simple solution of just flipping the pref `network.proxy.socks_remote_dns`
is risky due to potentially breaking SOCKS4 proxy users. Proxying
DNS on SOCKS4 isn't supported. Therefore we speak the incompatible
SOCKS4a protocol when `socks_remote_dns` is enabled, potentially
breaking users setup.
To keep backwards compatibility on SOCKS4 proxy users, that don't have
SOCKS4a support, the pref `network.proxy.socks_remote_dns` is split into
two prefs:
* `network.proxy.socks_remote_dns`: remote DNS for SOCKS4
* `network.proxy.socks5_remote_dns`: remote DNS for SOCKS5.
This way we proxy DNS by default on SOCKS5 while keeping user settings
on SOCKS4. This is a similar approach to the one described in
[Bug 610896 comment 17].
Proxying DNS in SOCKS4 by default is desireable (See [Bug 610896 comment 11]),
but out of scope for this patch. [Telemetry] on proxy usage by socks
version indicated that changing the default for SOCKS4 is likely break
some users setup and needs to be taken with more care.
The default values of [proxyDNS] now defaults to true for SOCKS5 proxies.
When creating nsIProxyInfo objects of SOCKS4 proxies, the default value
false is kept. Setting proxyDNS affects both SOCKS4 and SOCKS5 proxy by
modifying both `socks_remote_dns` and `socks5_remote_dns`. Therefore no
extension breakage is expected.
The enterprise policy can also modify the new pref
`network.proxy.socks5_remote_dns`.
Follow up bugs filed while implementing:
* Bug 1890542 - Also disable Prefetch non-manual configurations of socks
proxy
* Bug 1890554 - Use `ProxyInfo::TRANSPARENT_PROXY_RESOLVES_HOST` flag in
`nsHttpChannel::GetProxyDNSStrategy`
* Bug 1890549 - nsHttpChannel implementation DNS resolve strategy for
proxies incomplete
* Bug 1893670 - Proxy DNS by default for SOCK4 proxies. Defaulting to
SOCKS4a
[Bug 610896 comment 17]: https://bugzilla.mozilla.org/show_bug.cgi?id=610896#c17
[Bug 610896 comment 11]: https://bugzilla.mozilla.org/show_bug.cgi?id=610896#c11
[Telemetry]: https://bugzilla.mozilla.org/show_bug.cgi?id=1741375#c27
[proxyDNS]: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/proxy/settings#proxydns
Differential Revision: https://phabricator.services.mozilla.com/D207532
310 lines
10 KiB
JavaScript
310 lines
10 KiB
JavaScript
/* - 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/. */
|
|
|
|
/* import-globals-from preferences.js */
|
|
|
|
"use strict";
|
|
|
|
var { XPCOMUtils } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/XPCOMUtils.sys.mjs"
|
|
);
|
|
|
|
// Note: we get loaded in dialogs so we need to define our
|
|
// own getters, separate from preferences.js .
|
|
ChromeUtils.defineESModuleGetters(this, {
|
|
AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
|
|
BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
|
|
DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
|
|
|
|
ExtensionPreferencesManager:
|
|
"resource://gre/modules/ExtensionPreferencesManager.sys.mjs",
|
|
|
|
ExtensionSettingsStore:
|
|
"resource://gre/modules/ExtensionSettingsStore.sys.mjs",
|
|
|
|
Management: "resource://gre/modules/Extension.sys.mjs",
|
|
});
|
|
|
|
const PREF_SETTING_TYPE = "prefs";
|
|
const PROXY_KEY = "proxy.settings";
|
|
const API_PROXY_PREFS = [
|
|
"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.socks5_remote_dns",
|
|
"network.proxy.no_proxies_on",
|
|
"network.proxy.autoconfig_url",
|
|
"signon.autologin.proxy",
|
|
];
|
|
|
|
let extensionControlledContentIds = {
|
|
"privacy.containers": "browserContainersExtensionContent",
|
|
webNotificationsDisabled: "browserNotificationsPermissionExtensionContent",
|
|
"services.passwordSavingEnabled": "passwordManagerExtensionContent",
|
|
"proxy.settings": "proxyExtensionContent",
|
|
get "websites.trackingProtectionMode"() {
|
|
return {
|
|
button: "contentBlockingDisableTrackingProtectionExtension",
|
|
section: "contentBlockingTrackingProtectionExtensionContentLabel",
|
|
};
|
|
},
|
|
};
|
|
|
|
const extensionControlledL10nKeys = {
|
|
webNotificationsDisabled: "web-notifications",
|
|
"services.passwordSavingEnabled": "password-saving",
|
|
"privacy.containers": "privacy-containers",
|
|
"websites.trackingProtectionMode": "websites-content-blocking-all-trackers",
|
|
"proxy.settings": "proxy-config",
|
|
};
|
|
|
|
let extensionControlledIds = {};
|
|
|
|
/**
|
|
* Check if a pref is being managed by an extension.
|
|
*/
|
|
async function getControllingExtensionInfo(type, settingName) {
|
|
await ExtensionSettingsStore.initialize();
|
|
return ExtensionSettingsStore.getSetting(type, settingName);
|
|
}
|
|
|
|
function getControllingExtensionEls(settingName) {
|
|
let idInfo = extensionControlledContentIds[settingName];
|
|
let section = document.getElementById(idInfo.section || idInfo);
|
|
let button = idInfo.button
|
|
? document.getElementById(idInfo.button)
|
|
: section.querySelector("button");
|
|
return {
|
|
section,
|
|
button,
|
|
description: section.querySelector("description"),
|
|
};
|
|
}
|
|
|
|
async function getControllingExtension(type, settingName) {
|
|
let info = await getControllingExtensionInfo(type, settingName);
|
|
let addon = info && info.id && (await AddonManager.getAddonByID(info.id));
|
|
return addon;
|
|
}
|
|
|
|
async function handleControllingExtension(type, settingName) {
|
|
let addon = await getControllingExtension(type, settingName);
|
|
|
|
// Sometimes the ExtensionSettingsStore gets in a bad state where it thinks
|
|
// an extension is controlling a setting but the extension has been uninstalled
|
|
// outside of the regular lifecycle. If the extension isn't currently installed
|
|
// then we should treat the setting as not being controlled.
|
|
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1411046 for an example.
|
|
if (addon) {
|
|
extensionControlledIds[settingName] = addon.id;
|
|
showControllingExtension(settingName, addon);
|
|
} else {
|
|
let elements = getControllingExtensionEls(settingName);
|
|
if (
|
|
extensionControlledIds[settingName] &&
|
|
!document.hidden &&
|
|
elements.button
|
|
) {
|
|
showEnableExtensionMessage(settingName);
|
|
} else {
|
|
hideControllingExtension(settingName);
|
|
}
|
|
delete extensionControlledIds[settingName];
|
|
}
|
|
|
|
return !!addon;
|
|
}
|
|
|
|
function settingNameToL10nID(settingName) {
|
|
if (!extensionControlledL10nKeys.hasOwnProperty(settingName)) {
|
|
throw new Error(
|
|
`Unknown extension controlled setting name: ${settingName}`
|
|
);
|
|
}
|
|
return `extension-controlling-${extensionControlledL10nKeys[settingName]}`;
|
|
}
|
|
|
|
/**
|
|
* Set the localization data for the description of the controlling extension.
|
|
*
|
|
* The function alters the inner DOM structure of the fragment to, depending
|
|
* on the `addon` argument, remove the `<img/>` element or ensure it's
|
|
* set to the correct src.
|
|
* This allows Fluent DOM Overlays to localize the fragment.
|
|
*
|
|
* @param elem {Element}
|
|
* <description> element to be annotated
|
|
* @param addon {Object?}
|
|
* Addon object with meta information about the addon (or null)
|
|
* @param settingName {String}
|
|
* If `addon` is set this handled the name of the setting that will be used
|
|
* to fetch the l10n id for the given message.
|
|
* If `addon` is set to null, this will be the full l10n-id assigned to the
|
|
* element.
|
|
*/
|
|
function setControllingExtensionDescription(elem, addon, settingName) {
|
|
const existingImg = elem.querySelector("img");
|
|
if (addon === null) {
|
|
// If the element has an image child element,
|
|
// remove it.
|
|
if (existingImg) {
|
|
existingImg.remove();
|
|
}
|
|
document.l10n.setAttributes(elem, settingName);
|
|
return;
|
|
}
|
|
|
|
const defaultIcon = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
|
|
const src = addon.iconURL || defaultIcon;
|
|
|
|
if (!existingImg) {
|
|
// If an element doesn't have an image child
|
|
// node, add it.
|
|
let image = document.createElementNS("http://www.w3.org/1999/xhtml", "img");
|
|
image.setAttribute("src", src);
|
|
image.setAttribute("data-l10n-name", "icon");
|
|
image.setAttribute("role", "presentation");
|
|
image.classList.add("extension-controlled-icon");
|
|
elem.appendChild(image);
|
|
} else if (existingImg.getAttribute("src") !== src) {
|
|
existingImg.setAttribute("src", src);
|
|
}
|
|
|
|
const l10nId = settingNameToL10nID(settingName);
|
|
document.l10n.setAttributes(elem, l10nId, {
|
|
name: addon.name,
|
|
});
|
|
}
|
|
|
|
async function showControllingExtension(settingName, addon) {
|
|
// Tell the user what extension is controlling the setting.
|
|
let elements = getControllingExtensionEls(settingName);
|
|
|
|
elements.section.classList.remove("extension-controlled-disabled");
|
|
let description = elements.description;
|
|
|
|
setControllingExtensionDescription(description, addon, settingName);
|
|
|
|
if (elements.button) {
|
|
elements.button.hidden = false;
|
|
}
|
|
|
|
// Show the controlling extension row and hide the old label.
|
|
elements.section.hidden = false;
|
|
}
|
|
|
|
function hideControllingExtension(settingName) {
|
|
let elements = getControllingExtensionEls(settingName);
|
|
elements.section.hidden = true;
|
|
if (elements.button) {
|
|
elements.button.hidden = true;
|
|
}
|
|
}
|
|
|
|
function showEnableExtensionMessage(settingName) {
|
|
let elements = getControllingExtensionEls(settingName);
|
|
|
|
elements.button.hidden = true;
|
|
elements.section.classList.add("extension-controlled-disabled");
|
|
|
|
elements.description.textContent = "";
|
|
|
|
// We replace localization of the <description> with a DOM Fragment containing
|
|
// the enable-extension-enable message. That means a change from:
|
|
//
|
|
// <description data-l10n-id="..."/>
|
|
//
|
|
// to:
|
|
//
|
|
// <description>
|
|
// <img/>
|
|
// <label data-l10n-id="..."/>
|
|
// </description>
|
|
//
|
|
// We need to remove the l10n-id annotation from the <description> to prevent
|
|
// Fluent from overwriting the element in case of any retranslation.
|
|
elements.description.removeAttribute("data-l10n-id");
|
|
|
|
let icon = (url, name) => {
|
|
let img = document.createElementNS("http://www.w3.org/1999/xhtml", "img");
|
|
img.src = url;
|
|
img.setAttribute("data-l10n-name", name);
|
|
img.setAttribute("role", "presentation");
|
|
img.className = "extension-controlled-icon";
|
|
return img;
|
|
};
|
|
let label = document.createXULElement("label");
|
|
let addonIcon = icon(
|
|
"chrome://mozapps/skin/extensions/extensionGeneric.svg",
|
|
"addons-icon"
|
|
);
|
|
let toolbarIcon = icon("chrome://browser/skin/menu.svg", "menu-icon");
|
|
label.appendChild(addonIcon);
|
|
label.appendChild(toolbarIcon);
|
|
document.l10n.setAttributes(label, "extension-controlled-enable");
|
|
elements.description.appendChild(label);
|
|
let dismissButton = document.createXULElement("image");
|
|
dismissButton.setAttribute("class", "extension-controlled-icon close-icon");
|
|
dismissButton.addEventListener("click", function dismissHandler() {
|
|
hideControllingExtension(settingName);
|
|
dismissButton.removeEventListener("click", dismissHandler);
|
|
});
|
|
elements.description.appendChild(dismissButton);
|
|
}
|
|
|
|
function makeDisableControllingExtension(type, settingName) {
|
|
return async function disableExtension() {
|
|
let { id } = await getControllingExtensionInfo(type, settingName);
|
|
let addon = await AddonManager.getAddonByID(id);
|
|
await addon.disable();
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Initialize listeners though the Management API to update the UI
|
|
* when an extension is controlling a pref.
|
|
* @param {string} type
|
|
* @param {string} prefId The unique id of the setting
|
|
* @param {HTMLElement} controlledElement
|
|
*/
|
|
async function initListenersForPrefChange(type, prefId, controlledElement) {
|
|
await Management.asyncLoadSettingsModules();
|
|
|
|
let managementObserver = async () => {
|
|
let managementControlled = await handleControllingExtension(type, prefId);
|
|
// Enterprise policy may have locked the pref, so we need to preserve that
|
|
controlledElement.disabled =
|
|
managementControlled || Services.prefs.prefIsLocked(prefId);
|
|
};
|
|
managementObserver();
|
|
Management.on(`extension-setting-changed:${prefId}`, managementObserver);
|
|
|
|
window.addEventListener("unload", () => {
|
|
Management.off(`extension-setting-changed:${prefId}`, managementObserver);
|
|
});
|
|
}
|
|
|
|
function initializeProxyUI(container) {
|
|
let deferredUpdate = new DeferredTask(() => {
|
|
container.updateProxySettingsUI();
|
|
}, 10);
|
|
let proxyObserver = {
|
|
observe: (subject, topic, data) => {
|
|
if (API_PROXY_PREFS.includes(data)) {
|
|
deferredUpdate.arm();
|
|
}
|
|
},
|
|
};
|
|
Services.prefs.addObserver("", proxyObserver);
|
|
window.addEventListener("unload", () => {
|
|
Services.prefs.removeObserver("", proxyObserver);
|
|
});
|
|
}
|