Skip to content
Snippets Groups Projects
Commit d7fb863b authored by Richard Pospesel's avatar Richard Pospesel Committed by Beatriz Rizental
Browse files

TB 40597: Implement TorSettings module

- migrated in-page settings read/write implementation from about:preferences#tor
  to the TorSettings module
- TorSettings initially loads settings from the tor daemon, and saves them to
  firefox prefs
- TorSettings notifies observers when a setting has changed; currently only
  QuickStart notification is implemented for parity with previous preference
  notify logic in about:torconnect and about:preferences#tor
- about:preferences#tor, and about:torconnect now read and write settings
  thorugh the TorSettings module
- all tor settings live in the torbrowser.settings.* preference branch
- removed unused pref modify permission for about:torconnect content page from
  AsyncPrefs.jsm

Bug 40645: Migrate Moat APIs to Moat.jsm module
parent 1e600a2c
Branches
Tags
1 merge request!1507Rebase Tor Browser onto 136.0a1
......@@ -310,4 +310,6 @@ module.exports = [
"browser/app/profile/001-base-profile.js",
"browser/app/profile/000-tor-browser.js",
"mobile/android/app/000-tor-browser-android.js",
"toolkit/content/pt_config.json",
"toolkit/content/moat_contries_dev_build.json",
];
......@@ -1523,3 +1523,5 @@ xpcom/idl-parser/xpidl/fixtures/xpctest.d.json
browser/app/profile/001-base-profile.js
browser/app/profile/000-tor-browser.js
mobile/android/app/000-tor-browser-android.js
toolkit/content/pt_config.json
toolkit/content/moat_countries_dev_build.json
......@@ -131,3 +131,6 @@ pref("extensions.torlauncher.moat_service", "https://bridges.torproject.org/moat
pref("browser.tor_provider.log_level", "Warn");
pref("browser.tor_provider.cp_log_level", "Warn");
pref("lox.log_level", "Warn");
pref("torbrowser.bootstrap.log_level", "Info");
pref("browser.torsettings.log_level", "Warn");
pref("browser.torMoat.loglevel", "Warn");
......@@ -3,8 +3,10 @@ const lazy = {};
// We will use the modules only when the profile is loaded, so prefer lazy
// loading
ChromeUtils.defineESModuleGetters(lazy, {
TorConnect: "resource://gre/modules/TorConnect.sys.mjs",
TorLauncherUtil: "resource://gre/modules/TorLauncherUtil.sys.mjs",
TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
TorSettings: "resource://gre/modules/TorSettings.sys.mjs",
});
/* Browser observer topis */
......@@ -33,11 +35,15 @@ export class TorStartupService {
#init() {
Services.obs.addObserver(this, BrowserTopics.QuitApplicationGranted);
lazy.TorSettings.init();
// Theoretically, build() is expected to await the initialization of the
// provider, and anything needing the Tor Provider should be able to just
// await on TorProviderBuilder.build().
lazy.TorProviderBuilder.init();
lazy.TorConnect.init();
gInited = true;
}
......@@ -46,5 +52,6 @@ export class TorStartupService {
lazy.TorProviderBuilder.uninit();
lazy.TorLauncherUtil.cleanupTempDirectories();
lazy.TorSettings.uninit();
}
}
......@@ -177,3 +177,10 @@ toolkit.jar:
content/global/vendor/react-transition-group.js (vendor/react/react-transition-group.js)
content/global/vendor/redux.js (vendor/react/redux.js)
content/global/vendor/react-redux.js (vendor/react/react-redux.js)
# The pt_config.json content should be replaced in the omni.ja in
# tor-browser-build. See tor-browser#42343.
content/global/pt_config.json (pt_config.json)
# The moat_countries.json content should be replaced in the omni.ja in
# tor-browser-build. See tor-browser#43463.
content/global/moat_countries.json (moat_countries_dev_build.json)
[
{
"_comment1": "Used for dev build, replaced for release builds in tor-browser-build.",
"_comment2": "List is taken from tpo/anti-censorship/rdsys-admin 810fb24b:conf/circumvention.json and filtered with `jq -c keys`."
},
"by","cn","eg","hk","ir","mm","ru","tm"
]
{
"_comment": "Used for dev build, replaced for release builds in tor-browser-build. This file is copied from tor-browser-build efdc0f34475ac8e4df241d3c8ff7253e8470019f:projects/tor-expert-bundle/pt_config.json",
"recommendedDefault" : "obfs4",
"pluggableTransports" : {
"lyrebird" : "ClientTransportPlugin meek_lite,obfs2,obfs3,obfs4,scramblesuit,snowflake,webtunnel exec ${pt_path}lyrebird${pt_extension}",
"conjure" : "ClientTransportPlugin conjure exec ${pt_path}conjure-client${pt_extension} -registerURL https://registration.refraction.network/api"
},
"bridges" : {
"meek-azure" : [
"meek_lite 192.0.2.20:80 url=https://1314488750.rsc.cdn77.org front=www.phpmyadmin.net utls=HelloRandomizedALPN"
],
"obfs4" : [
"obfs4 192.95.36.142:443 CDF2E852BF539B82BD10E27E9115A31734E378C2 cert=qUVQ0srL1JI/vO6V6m/24anYXiJD3QP2HgzUKQtQ7GRqqUvs7P+tG43RtAqdhLOALP7DJQ iat-mode=1",
"obfs4 37.218.245.14:38224 D9A82D2F9C2F65A18407B1D2B764F130847F8B5D cert=bjRaMrr1BRiAW8IE9U5z27fQaYgOhX1UCmOpg2pFpoMvo6ZgQMzLsaTzzQNTlm7hNcb+Sg iat-mode=0",
"obfs4 85.31.186.98:443 011F2599C0E9B27EE74B353155E244813763C3E5 cert=ayq0XzCwhpdysn5o0EyDUbmSOx3X/oTEbzDMvczHOdBJKlvIdHHLJGkZARtT4dcBFArPPg iat-mode=0",
"obfs4 85.31.186.26:443 91A6354697E6B02A386312F68D82CF86824D3606 cert=PBwr+S8JTVZo6MPdHnkTwXJPILWADLqfMGoVvhZClMq/Urndyd42BwX9YFJHZnBB3H0XCw iat-mode=0",
"obfs4 193.11.166.194:27015 2D82C2E354D531A68469ADF7F878FA6060C6BACA cert=4TLQPJrTSaDffMK7Nbao6LC7G9OW/NHkUwIdjLSS3KYf0Nv4/nQiiI8dY2TcsQx01NniOg iat-mode=0",
"obfs4 193.11.166.194:27020 86AC7B8D430DAC4117E9F42C9EAED18133863AAF cert=0LDeJH4JzMDtkJJrFphJCiPqKx7loozKN7VNfuukMGfHO0Z8OGdzHVkhVAOfo1mUdv9cMg iat-mode=0",
"obfs4 193.11.166.194:27025 1AE2C08904527FEA90C4C4F8C1083EA59FBC6FAF cert=ItvYZzW5tn6v3G4UnQa6Qz04Npro6e81AP70YujmK/KXwDFPTs3aHXcHp4n8Vt6w/bv8cA iat-mode=0",
"obfs4 209.148.46.65:443 74FAD13168806246602538555B5521A0383A1875 cert=ssH+9rP8dG2NLDN2XuFw63hIO/9MNNinLmxQDpVa+7kTOa9/m+tGWT1SmSYpQ9uTBGa6Hw iat-mode=0",
"obfs4 146.57.248.225:22 10A6CD36A537FCE513A322361547444B393989F0 cert=K1gDtDAIcUfeLqbstggjIw2rtgIKqdIhUlHp82XRqNSq/mtAjp1BIC9vHKJ2FAEpGssTPw iat-mode=0",
"obfs4 45.145.95.6:27015 C5B7CD6946FF10C5B3E89691A7D3F2C122D2117C cert=TD7PbUO0/0k6xYHMPW3vJxICfkMZNdkRrb63Zhl5j9dW3iRGiCx0A7mPhe5T2EDzQ35+Zw iat-mode=0",
"obfs4 51.222.13.177:80 5EDAC3B810E12B01F6FD8050D2FD3E277B289A08 cert=2uplIpLQ0q9+0qMFrK5pkaYRDOe460LL9WHBvatgkuRr/SL31wBOEupaMMJ6koRE6Ld0ew iat-mode=0"
],
"snowflake" : [
"snowflake 192.0.2.3:80 2B280B23E1107BB62ABFC40DDCC8824814F80A72 fingerprint=2B280B23E1107BB62ABFC40DDCC8824814F80A72 url=https://1098762253.rsc.cdn77.org/ fronts=www.cdn77.com,www.phpmyadmin.net ice=stun:stun.antisip.com:3478,stun:stun.epygi.com:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.mixvoip.com:3478,stun:stun.nextcloud.com:3478,stun:stun.bethesda.net:3478,stun:stun.nextcloud.com:443 utls-imitate=hellorandomizedalpn",
"snowflake 192.0.2.4:80 8838024498816A039FCBBAB14E6F40A0843051FA fingerprint=8838024498816A039FCBBAB14E6F40A0843051FA url=https://1098762253.rsc.cdn77.org/ fronts=www.cdn77.com,www.phpmyadmin.net ice=stun:stun.antisip.com:3478,stun:stun.epygi.com:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.mixvoip.com:3478,stun:stun.nextcloud.com:3478,stun:stun.bethesda.net:3478,stun:stun.nextcloud.com:443 utls-imitate=hellorandomizedalpn"
]
}
}
/* 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/. */
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
MoatRPC: "resource://gre/modules/Moat.sys.mjs",
});
export var BridgeDB = {
_moatRPC: null,
_challenge: null,
_image: null,
_bridges: null,
/**
* A collection of controllers to abort any ongoing Moat requests if the
* dialog is closed.
*
* NOTE: We do not expect this set to ever contain more than one instance.
* However the public API has no assurances to prevent multiple calls.
*
* @type {Set<AbortController>}
*/
_moatAbortControllers: new Set(),
get currentCaptchaImage() {
return this._image;
},
get currentBridges() {
return this._bridges;
},
async submitCaptchaGuess(solution) {
if (!this._moatRPC) {
this._moatRPC = new lazy.MoatRPC();
await this._moatRPC.init();
}
const abortController = new AbortController();
this._moatAbortControllers.add(abortController);
try {
this._bridges = await this._moatRPC.check(
"obfs4",
this._challenge,
solution,
abortController.signal
);
} finally {
this._moatAbortControllers.delete(abortController);
}
return this._bridges;
},
async requestNewCaptchaImage() {
try {
if (!this._moatRPC) {
this._moatRPC = new lazy.MoatRPC();
await this._moatRPC.init();
}
const abortController = new AbortController();
this._moatAbortControllers.add(abortController);
let response;
try {
response = await this._moatRPC.fetch(["obfs4"], abortController.signal);
} finally {
this._moatAbortControllers.delete(abortController);
}
if (response) {
// Not cancelled.
this._challenge = response.challenge;
this._image =
"data:image/jpeg;base64," + encodeURIComponent(response.image);
}
} catch (err) {
console.error("Could not request a captcha image", err);
}
return this._image;
},
close() {
// Abort any ongoing requests.
for (const controller of this._moatAbortControllers) {
controller.abort();
}
this._moatAbortControllers.clear();
this._moatRPC?.uninit();
this._moatRPC = null;
this._challenge = null;
this._image = null;
this._bridges = null;
},
};
/* 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/. */
const lazy = {};
const log = console.createInstance({
maxLogLevel: "Warn",
prefix: "DomainFrontendRequests",
});
ChromeUtils.defineESModuleGetters(lazy, {
EventDispatcher: "resource://gre/modules/Messaging.sys.mjs",
Subprocess: "resource://gre/modules/Subprocess.sys.mjs",
TorLauncherUtil: "resource://gre/modules/TorLauncherUtil.sys.mjs",
TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
TorSettings: "resource://gre/modules/TorSettings.sys.mjs",
});
/**
* The meek pluggable transport takes the reflector URL and front domain as
* proxy credentials, which can be prepared with this function.
*
* @param {string} proxyType The proxy type (socks for socks5 or socks4)
* @param {string} reflector The URL of the service hosted by the CDN
* @param {string} front The domain to use as a front
* @returns {string[]} An array containing [username, password]
*/
function makeMeekCredentials(proxyType, reflector, front) {
// Construct the per-connection arguments.
let meekClientEscapedArgs = "";
// Escape aValue per section 3.5 of the PT specification:
// First the "<Key>=<Value>" formatted arguments MUST be escaped,
// such that all backslash, equal sign, and semicolon characters
// are escaped with a backslash.
const escapeArgValue = aValue =>
aValue
? aValue
.replaceAll("\\", "\\\\")
.replaceAll("=", "\\=")
.replaceAll(";", "\\;")
: "";
if (reflector) {
meekClientEscapedArgs += "url=";
meekClientEscapedArgs += escapeArgValue(reflector);
}
if (front) {
if (meekClientEscapedArgs.length) {
meekClientEscapedArgs += ";";
}
meekClientEscapedArgs += "front=";
meekClientEscapedArgs += escapeArgValue(front);
}
// socks5
if (proxyType === "socks") {
if (meekClientEscapedArgs.length <= 255) {
return [meekClientEscapedArgs, "\x00"];
}
return [
meekClientEscapedArgs.substring(0, 255),
meekClientEscapedArgs.substring(255),
];
} else if (proxyType === "socks4") {
return [meekClientEscapedArgs, undefined];
}
throw new Error(`Unsupported proxy type ${proxyType}.`);
}
/**
* Subprocess-based implementation to launch and control a PT process.
*/
class MeekTransport {
// These members are used by consumers to setup the proxy to do requests over
// meek. They are passed to newProxyInfoWithAuth.
proxyType = null;
proxyAddress = null;
proxyPort = 0;
proxyUsername = null;
proxyPassword = null;
#inited = false;
#meekClientProcess = null;
// launches the meekprocess
async init(reflector, front) {
// ensure we haven't already init'd
if (this.#inited) {
throw new Error("MeekTransport: Already initialized");
}
try {
// figure out which pluggable transport to use
const supportedTransports = ["meek", "meek_lite"];
const provider = await lazy.TorProviderBuilder.build();
const proxy = (await provider.getPluggableTransports()).find(
pt =>
pt.type === "exec" &&
supportedTransports.some(t => pt.transports.includes(t))
);
if (!proxy) {
throw new Error("No supported transport found.");
}
const meekTransport = proxy.transports.find(t =>
supportedTransports.includes(t)
);
// Convert meek client path to absolute path if necessary
const meekWorkDir = lazy.TorLauncherUtil.getTorFile(
"pt-startup-dir",
false
);
if (lazy.TorLauncherUtil.isPathRelative(proxy.pathToBinary)) {
const meekPath = meekWorkDir.clone();
meekPath.appendRelativePath(proxy.pathToBinary);
proxy.pathToBinary = meekPath.path;
}
// Setup env and start meek process
const ptStateDir = lazy.TorLauncherUtil.getTorFile("tordatadir", false);
ptStateDir.append("pt_state"); // Match what tor uses.
const envAdditions = {
TOR_PT_MANAGED_TRANSPORT_VER: "1",
TOR_PT_STATE_LOCATION: ptStateDir.path,
TOR_PT_EXIT_ON_STDIN_CLOSE: "1",
TOR_PT_CLIENT_TRANSPORTS: meekTransport,
};
if (lazy.TorSettings.proxy.enabled) {
envAdditions.TOR_PT_PROXY = lazy.TorSettings.proxyUri;
}
const opts = {
command: proxy.pathToBinary,
arguments: proxy.options.split(/s+/),
workdir: meekWorkDir.path,
environmentAppend: true,
environment: envAdditions,
stderr: "pipe",
};
// Launch meek client
this.#meekClientProcess = await lazy.Subprocess.call(opts);
// Callback chain for reading stderr
const stderrLogger = async () => {
while (this.#meekClientProcess) {
const errString = await this.#meekClientProcess.stderr.readString();
if (errString) {
log.error(`MeekTransport: stderr => ${errString}`);
}
}
};
stderrLogger();
// Read pt's stdout until terminal (CMETHODS DONE) is reached
// returns array of lines for parsing
const getInitLines = async (stdout = "") => {
stdout += await this.#meekClientProcess.stdout.readString();
// look for the final message
const CMETHODS_DONE = "CMETHODS DONE";
let endIndex = stdout.lastIndexOf(CMETHODS_DONE);
if (endIndex !== -1) {
endIndex += CMETHODS_DONE.length;
return stdout.substring(0, endIndex).split("\n");
}
return getInitLines(stdout);
};
// read our lines from pt's stdout
const meekInitLines = await getInitLines();
// tokenize our pt lines
const meekInitTokens = meekInitLines.map(line => {
const tokens = line.split(" ");
return {
keyword: tokens[0],
args: tokens.slice(1),
};
});
// parse our pt tokens
for (const { keyword, args } of meekInitTokens) {
const argsJoined = args.join(" ");
let keywordError = false;
switch (keyword) {
case "VERSION": {
if (args.length !== 1 || args[0] !== "1") {
keywordError = true;
}
break;
}
case "PROXY": {
if (args.length !== 1 || args[0] !== "DONE") {
keywordError = true;
}
break;
}
case "CMETHOD": {
if (args.length !== 3) {
keywordError = true;
break;
}
const transport = args[0];
const proxyType = args[1];
const addrPortString = args[2];
const addrPort = addrPortString.split(":");
if (transport !== meekTransport) {
throw new Error(
`MeekTransport: Expected ${meekTransport} but found ${transport}`
);
}
if (!["socks4", "socks4a", "socks5"].includes(proxyType)) {
throw new Error(
`MeekTransport: Invalid proxy type => ${proxyType}`
);
}
if (addrPort.length !== 2) {
throw new Error(
`MeekTransport: Invalid proxy address => ${addrPortString}`
);
}
const addr = addrPort[0];
const port = parseInt(addrPort[1]);
if (port < 1 || port > 65535) {
throw new Error(`MeekTransport: Invalid proxy port => ${port}`);
}
// convert proxy type to strings used by protocol-proxy-servce
this.proxyType = proxyType === "socks5" ? "socks" : "socks4";
this.proxyAddress = addr;
this.proxyPort = port;
break;
}
// terminal
case "CMETHODS": {
if (args.length !== 1 || args[0] !== "DONE") {
keywordError = true;
}
break;
}
// errors (all fall through):
case "VERSION-ERROR":
case "ENV-ERROR":
case "PROXY-ERROR":
case "CMETHOD-ERROR":
throw new Error(`MeekTransport: ${keyword} => '${argsJoined}'`);
}
if (keywordError) {
throw new Error(
`MeekTransport: Invalid ${keyword} keyword args => '${argsJoined}'`
);
}
}
// register callback to cleanup on process exit
this.#meekClientProcess.wait().then(() => {
this.#meekClientProcess = null;
this.uninit();
});
[this.proxyUsername, this.proxyPassword] = makeMeekCredentials(
this.proxyType,
reflector,
front
);
this.#inited = true;
} catch (ex) {
if (this.#meekClientProcess) {
this.#meekClientProcess.kill();
this.#meekClientProcess = null;
}
throw ex;
}
}
async uninit() {
this.#inited = false;
await this.#meekClientProcess?.kill();
this.#meekClientProcess = null;
this.proxyType = null;
this.proxyAddress = null;
this.proxyPort = 0;
this.proxyUsername = null;
this.proxyPassword = null;
}
}
/**
* Android implementation of the Meek process.
*
* GeckoView does not provide the subprocess module, so we have to use the
* EventDispatcher, and have a Java handler start and stop the proxy process.
*/
class MeekTransportAndroid {
// These members are used by consumers to setup the proxy to do requests over
// meek. They are passed to newProxyInfoWithAuth.
proxyType = null;
proxyAddress = null;
proxyPort = 0;
proxyUsername = null;
proxyPassword = null;
/**
* An id for process this instance is linked to.
*
* Since we do not restrict the transport to be a singleton, we need a handle to
* identify the process we want to stop when the transport owner is done.
* We use a counter incremented on the Java side for now.
*
* This number must be a positive integer (i.e., 0 is an invalid handler).
*
* @type {number}
*/
#id = 0;
async init(reflector, front) {
// ensure we haven't already init'd
if (this.#id) {
throw new Error("MeekTransport: Already initialized");
}
const details = await lazy.EventDispatcher.instance.sendRequestForResult({
type: "GeckoView:Tor:StartMeek",
});
this.#id = details.id;
this.proxyType = "socks";
this.proxyAddress = details.address;
this.proxyPort = details.port;
[this.proxyUsername, this.proxyPassword] = makeMeekCredentials(
this.proxyType,
reflector,
front
);
}
async uninit() {
lazy.EventDispatcher.instance.sendRequest({
type: "GeckoView:Tor:StopMeek",
id: this.#id,
});
this.#id = 0;
this.proxyType = null;
this.proxyAddress = null;
this.proxyPort = 0;
this.proxyUsername = null;
this.proxyPassword = null;
}
}
/**
* Corresponds to a Network error with the request.
*/
export class DomainFrontRequestNetworkError extends Error {
constructor(request, statusCode) {
super(`Error fetching ${request.name}: ${statusCode}`);
this.name = "DomainFrontRequestNetworkError";
this.statusCode = statusCode;
}
}
/**
* Corresponds to a non-ok response from the server.
*/
export class DomainFrontRequestResponseError extends Error {
constructor(request) {
super(
`Error response from ${request.name} server: ${request.responseStatus}`
);
this.name = "DomainFrontRequestResponseError";
this.status = request.responseStatus;
this.statusText = request.responseStatusText;
}
}
/**
* Thrown when the caller cancels the request.
*/
export class DomainFrontRequestCancelledError extends Error {
constructor(url) {
super(`Cancelled request to ${url}`);
}
}
/**
* Callback object to promisify the XPCOM request.
*/
class ResponseListener {
#response = "";
#responsePromise;
#resolve;
#reject;
constructor() {
this.#response = "";
// we need this promise here because await nsIHttpChannel::asyncOpen does
// not return only once the request is complete, it seems to return
// after it begins, so we have to get the result from this listener object.
// This promise is only resolved once onStopRequest is called
this.#responsePromise = new Promise((resolve, reject) => {
this.#resolve = resolve;
this.#reject = reject;
});
}
/**
* A promise that resolves to the response body from the request.
*
* @type {Promise<string>}
*/
get response() {
return this.#responsePromise;
}
// noop
onStartRequest() {}
// resolve or reject our Promise
onStopRequest(request, status) {
try {
if (!Components.isSuccessCode(status)) {
// Assume this is a network error.
this.#reject(new DomainFrontRequestNetworkError(request, status));
}
if (request.responseStatus !== 200) {
this.#reject(new DomainFrontRequestResponseError(request));
}
} catch (err) {
this.#reject(err);
}
this.#resolve(this.#response);
}
// read response data
onDataAvailable(request, stream, offset, length) {
const scriptableStream = Cc[
"@mozilla.org/scriptableinputstream;1"
].createInstance(Ci.nsIScriptableInputStream);
scriptableStream.init(stream);
this.#response += scriptableStream.read(length);
}
}
/**
* Factory to create HTTP(S) requests over a domain fronted transport.
*/
export class DomainFrontRequestBuilder {
#inited = false;
#meekTransport = null;
get inited() {
return this.#inited;
}
async init(reflector, front) {
if (this.#inited) {
throw new Error("DomainFrontRequestBuilder: Already initialized");
}
const meekTransport =
Services.appinfo.OS === "Android"
? new MeekTransportAndroid()
: new MeekTransport();
await meekTransport.init(reflector, front);
this.#meekTransport = meekTransport;
this.#inited = true;
}
async uninit() {
await this.#meekTransport?.uninit();
this.#meekTransport = null;
this.#inited = false;
}
buildHttpHandler(uriString) {
if (!this.#inited) {
throw new Error("DomainFrontRequestBuilder: Not initialized");
}
const { proxyType, proxyAddress, proxyPort, proxyUsername, proxyPassword } =
this.#meekTransport;
const proxyPS = Cc[
"@mozilla.org/network/protocol-proxy-service;1"
].getService(Ci.nsIProtocolProxyService);
const flags = Ci.nsIProxyInfo.TRANSPARENT_PROXY_RESOLVES_HOST;
const noTimeout = 0xffffffff; // UINT32_MAX
const proxyInfo = proxyPS.newProxyInfoWithAuth(
proxyType,
proxyAddress,
proxyPort,
proxyUsername,
proxyPassword,
undefined,
undefined,
flags,
noTimeout,
undefined
);
const uri = Services.io.newURI(uriString);
// There does not seem to be a way to directly create an nsILoadInfo from
// JavaScript, so we create a throw away non-proxied channel to get one.
const secFlags = Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL;
const loadInfo = Services.io.newChannelFromURI(
uri,
undefined,
Services.scriptSecurityManager.getSystemPrincipal(),
undefined,
secFlags,
Ci.nsIContentPolicy.TYPE_OTHER
).loadInfo;
const httpHandler = Services.io
.getProtocolHandler("http")
.QueryInterface(Ci.nsIHttpProtocolHandler);
const ch = httpHandler
.newProxiedChannel(uri, proxyInfo, 0, undefined, loadInfo)
.QueryInterface(Ci.nsIHttpChannel);
// remove all headers except for 'Host"
const headers = [];
ch.visitRequestHeaders({
visitHeader: key => {
if (key !== "Host") {
headers.push(key);
}
},
});
headers.forEach(key => ch.setRequestHeader(key, "", false));
return ch;
}
/**
* Make a request.
*
* @param {string} url The URL to request.
* @param {object} args The arguments to send to the procedure.
* @param {string} args.method The request method.
* @param {string} args.body The request body.
* @param {string} args.contentType The "Content-Type" header to set.
* @param {AbortSignal} [signal] args.signal An optional means of cancelling
* the request early. Will throw DomainFrontRequestCancelledError if
* aborted.
* @returns {Promise<string>} A promise that resolves to the response body.
*/
buildRequest(url, args) {
// Pre-fetch the argument values from `args` so the caller cannot change the
// parameters mid-call.
const { body, method, contentType, signal } = args;
let cancel = null;
const promise = new Promise((resolve, reject) => {
if (signal?.aborted) {
// Unexpected, cancel immediately.
reject(new DomainFrontRequestCancelledError(url));
return;
}
let ch = null;
if (signal) {
cancel = () => {
// Reject prior to calling cancel, since we want to ignore any error
// responses from ResponseListener.
// NOTE: In principle we could let ResponseListener throw this error
// when it receives NS_ERROR_ABORT, but that would rely on mozilla
// never calling this error either.
reject(new DomainFrontRequestCancelledError(url));
ch?.cancel(Cr.NS_ERROR_ABORT);
};
signal.addEventListener("abort", cancel);
}
ch = this.buildHttpHandler(url);
const inStream = Cc[
"@mozilla.org/io/string-input-stream;1"
].createInstance(Ci.nsIStringInputStream);
inStream.setData(body, body.length);
const upChannel = ch.QueryInterface(Ci.nsIUploadChannel);
upChannel.setUploadStream(inStream, contentType, body.length);
ch.requestMethod = method;
// Make request
const listener = new ResponseListener();
ch.asyncOpen(listener);
listener.response.then(
body => {
resolve(body);
},
error => {
reject(error);
}
);
});
// Clean up. Do not return this `Promise.finally` since the caller should
// not depend on it.
// We pre-catch and suppress all errors for this `.finally` to stop the
// errors from being duplicated in the console log.
promise
.catch(() => {})
.finally(() => {
// Remove the callback for the AbortSignal so that it doesn't hold onto
// our channel reference if the caller continues to hold a reference to
// AbortSignal.
if (cancel) {
signal.removeEventListener("abort", cancel);
}
});
return promise;
}
}
/* 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/. */
const lazy = {};
const log = console.createInstance({
prefix: "Moat",
maxLogLevelPref: "browser.torMoat.loglevel",
});
ChromeUtils.defineESModuleGetters(lazy, {
DomainFrontRequestBuilder:
"resource://gre/modules/DomainFrontedRequests.sys.mjs",
DomainFrontRequestCancelledError:
"resource://gre/modules/DomainFrontedRequests.sys.mjs",
TorBridgeSource: "resource://gre/modules/TorSettings.sys.mjs",
});
const TorLauncherPrefs = Object.freeze({
bridgedb_front: "extensions.torlauncher.bridgedb_front",
bridgedb_reflector: "extensions.torlauncher.bridgedb_reflector",
moat_service: "extensions.torlauncher.moat_service",
});
/**
* @typedef {Object} MoatBridges
*
* Bridge settings that can be passed to TorSettings.bridges.
*
* @property {number} source - The `TorBridgeSource` type.
* @property {string} [builtin_type] - The built-in bridge type.
* @property {string[]} [bridge_strings] - The bridge lines.
*/
/**
* @typedef {Object} MoatSettings
*
* The settings returned by Moat.
*
* @property {MoatBridges[]} bridgesList - The list of bridges found.
* @property {string} [country] - The detected country (region).
*/
/**
* @typedef {Object} CaptchaChallenge
*
* The details for a captcha challenge.
*
* @property {string} transport - The transport type selected by the Moat
* server.
* @property {string} image - A base64 encoded jpeg with the captcha to
* complete.
* @property {string} challenge - A nonce/cookie string associated with this
* request.
*/
/**
* Constructs JSON objects and sends requests over Moat.
* The documentation about the JSON schemas to use are available at
* https://gitlab.torproject.org/tpo/anti-censorship/rdsys/-/blob/main/doc/moat.md.
*/
export class MoatRPC {
#requestBuilder = null;
async init() {
if (this.#requestBuilder !== null) {
return;
}
const reflector = Services.prefs.getStringPref(
TorLauncherPrefs.bridgedb_reflector
);
const front = Services.prefs.getStringPref(TorLauncherPrefs.bridgedb_front);
this.#requestBuilder = new lazy.DomainFrontRequestBuilder();
try {
await this.#requestBuilder.init(reflector, front);
} catch (e) {
this.#requestBuilder = null;
throw e;
}
}
async uninit() {
await this.#requestBuilder?.uninit();
this.#requestBuilder = null;
}
/**
* @typedef {Object} MoatResult
*
* The result of a Moat request.
*
* @property {any} response - The parsed JSON response from the Moat server,
* or `undefined` if the request was cancelled.
* @property {boolean} cancelled - Whether the request was cancelled.
*/
/**
* Make a request to Moat.
*
* @param {string} procedure - The name of the procedure.
* @param {object} args - The arguments to pass in as a JSON string.
* @param {AbortSignal} [abortSignal] - An optional signal to be able to abort
* the request early.
* @returns {MoatResult} - The result of the request.
*/
async #makeRequest(procedure, args, abortSignal) {
const procedureURIString = `${Services.prefs.getStringPref(
TorLauncherPrefs.moat_service
)}/${procedure}`;
log.info(`Making request to ${procedureURIString}:`, args);
let response = undefined;
let cancelled = false;
try {
response = JSON.parse(
await this.#requestBuilder.buildRequest(procedureURIString, {
method: "POST",
contentType: "application/vnd.api+json",
body: JSON.stringify(args),
signal: abortSignal,
})
);
log.info(`Response to ${procedureURIString}:`, response);
} catch (e) {
if (abortSignal && e instanceof lazy.DomainFrontRequestCancelledError) {
log.info(`Request to ${procedureURIString} cancelled`);
cancelled = true;
} else {
throw e;
}
}
return { response, cancelled };
}
/**
* Request a CAPTCHA challenge.
*
* @param {string[]} transports - List of transport strings available to us
* eg: ["obfs4", "meek"].
* @param {AbortSignal} abortSignal - A signal to abort the request early.
* @returns {?CaptchaChallenge} - The captcha challenge, or `null` if the
* request was aborted by the caller.
*/
async fetch(transports, abortSignal) {
if (
// ensure this is an array
Array.isArray(transports) &&
// ensure array has values
!!transports.length &&
// ensure each value in the array is a string
transports.reduce((acc, cur) => acc && typeof cur === "string", true)
) {
const args = {
data: [
{
version: "0.1.0",
type: "client-transports",
supported: transports,
},
],
};
const { response, cancelled } = await this.#makeRequest(
"fetch",
args,
abortSignal
);
if (cancelled) {
return null;
}
if ("errors" in response) {
const code = response.errors[0].code;
const detail = response.errors[0].detail;
throw new Error(`MoatRPC: ${detail} (${code})`);
}
const transport = response.data[0].transport;
const image = response.data[0].image;
const challenge = response.data[0].challenge;
return { transport, image, challenge };
}
throw new Error("MoatRPC: fetch() expects a non-empty array of strings");
}
/**
* Submit an answer for a previous CAPTCHA fetch to get bridges.
*
* @param {string} transport - The transport associated with the fetch.
* @param {string} challenge - The nonce string associated with the fetch.
* @param {string} solution - The solution to the CAPTCHA.
* @param {AbortSignal} abortSignal - A signal to abort the request early.
* @returns {?string[]} - The bridge lines for a correct solution, or `null`
* if the solution was incorrect or the request was aborted by the caller.
*/
async check(transport, challenge, solution, abortSignal) {
const args = {
data: [
{
id: "2",
version: "0.1.0",
type: "moat-solution",
transport,
challenge,
solution,
qrcode: "false",
},
],
};
const { response, cancelled } = await this.#makeRequest(
"check",
args,
abortSignal
);
if (cancelled) {
return null;
}
if ("errors" in response) {
const code = response.errors[0].code;
const detail = response.errors[0].detail;
if (code == 419 && detail === "The CAPTCHA solution was incorrect.") {
return null;
}
throw new Error(`MoatRPC: ${detail} (${code})`);
}
return response.data[0].bridges;
}
/**
* Extract bridges from the received Moat settings object.
*
* @param {Object} settings - The received settings.
* @return {MoatBridge} The extracted bridges.
*/
#extractBridges(settings) {
if (!("bridges" in settings)) {
throw new Error("Expected to find `bridges` in the settings object.");
}
const bridges = {};
switch (settings.bridges.source) {
case "builtin":
bridges.source = lazy.TorBridgeSource.BuiltIn;
bridges.builtin_type = String(settings.bridges.type);
// Ignore the bridge_strings argument since we will use our built-in
// bridge strings instead.
break;
case "bridgedb":
bridges.source = lazy.TorBridgeSource.BridgeDB;
if (settings.bridges.bridge_strings?.length) {
bridges.bridge_strings = Array.from(
settings.bridges.bridge_strings,
item => String(item)
);
} else {
throw new Error(
"Received no bridge-strings for BridgeDB bridge source"
);
}
break;
default:
throw new Error(
`Unexpected bridge source '${settings.bridges.source}'`
);
}
return bridges;
}
/**
* Extract a list of bridges from the received Moat settings object.
*
* @param {Object} settings - The received settings.
* @return {MoatBridge[]} The list of extracted bridges.
*/
#extractBridgesList(settingsList) {
const bridgesList = [];
for (const settings of settingsList) {
try {
bridgesList.push(this.#extractBridges(settings));
} catch (ex) {
log.error(ex);
}
}
return bridgesList;
}
/**
* Request tor settings for the user optionally based on their location
* (derived from their IP). Takes the following parameters:
*
* @param {string[]} transports - A list of transports we support.
* @param {?string} country - The region to request bridges for, as an
* ISO 3166-1 alpha-2 region code, or `null` to have the server
* automatically determine the region.
* @param {AbortSignal} abortSignal - A signal to abort the request early.
* @returns {?MoatSettings} - The returned settings from the server, or `null`
* if the region could not be determined by the server or the caller
* cancelled the request.
*/
async circumvention_settings(transports, country, abortSignal) {
const args = {
transports: transports ? transports : [],
country,
};
const { response, cancelled } = await this.#makeRequest(
"circumvention/settings",
args,
abortSignal
);
if (cancelled) {
return null;
}
let settings = {};
if ("errors" in response) {
const code = response.errors[0].code;
const detail = response.errors[0].detail;
if (code == 406) {
log.error(
"MoatRPC::circumvention_settings(): Cannot automatically determine user's country-code"
);
// cannot determine user's country
return null;
}
throw new Error(`MoatRPC: ${detail} (${code})`);
} else if ("settings" in response) {
settings.bridgesList = this.#extractBridgesList(response.settings);
}
if ("country" in response) {
settings.country = response.country;
}
return settings;
}
// Request a copy of the builtin bridges, takes the following parameters:
// - transports: optional, an array of transports we would like the latest
// bridge strings for; if empty (or not given) returns all of them
//
// returns a map whose keys are pluggable transport types and whose values are
// arrays of bridge strings for that type
async circumvention_builtin(transports) {
const args = {
transports: transports ? transports : [],
};
const { response } = await this.#makeRequest("circumvention/builtin", args);
if ("errors" in response) {
const code = response.errors[0].code;
const detail = response.errors[0].detail;
throw new Error(`MoatRPC: ${detail} (${code})`);
}
let map = new Map();
for (const [transport, bridge_strings] of Object.entries(response)) {
map.set(transport, bridge_strings);
}
return map;
}
/**
* Request a copy of the default/fallback bridge settings.
*
* @param {string[]} transports - A list of transports we support.
* @param {AbortSignal} abortSignal - A signal to abort the request early.
* @returns {?MoatBridges[]} - The list of bridges found, or `null` if the
* caller cancelled the request.
*/
async circumvention_defaults(transports, abortSignal) {
const args = {
transports: transports ? transports : [],
};
const { response, cancelled } = await this.#makeRequest(
"circumvention/defaults",
args,
abortSignal
);
if (cancelled) {
return null;
}
if ("errors" in response) {
const code = response.errors[0].code;
const detail = response.errors[0].detail;
throw new Error(`MoatRPC: ${detail} (${code})`);
} else if ("settings" in response) {
return this.#extractBridgesList(response.settings);
}
return [];
}
}
This diff is collapsed.
This diff is collapsed.
......@@ -155,6 +155,7 @@ EXTRA_JS_MODULES += [
"AsyncPrefs.sys.mjs",
"Bech32Decode.sys.mjs",
"BinarySearch.sys.mjs",
"BridgeDB.sys.mjs",
"BrowserTelemetryUtils.sys.mjs",
"BrowserUtils.sys.mjs",
"CanonicalJSON.sys.mjs",
......@@ -165,6 +166,7 @@ EXTRA_JS_MODULES += [
"CreditCard.sys.mjs",
"DateTimePickerPanel.sys.mjs",
"DeferredTask.sys.mjs",
"DomainFrontedRequests.sys.mjs",
"E10SUtils.sys.mjs",
"EventEmitter.sys.mjs",
"FileUtils.sys.mjs",
......@@ -189,6 +191,7 @@ EXTRA_JS_MODULES += [
"LayoutUtils.sys.mjs",
"Log.sys.mjs",
"LogManager.sys.mjs",
"Moat.sys.mjs",
"NewTabUtils.sys.mjs",
"NLP.sys.mjs",
"ObjectUtils.sys.mjs",
......@@ -210,6 +213,8 @@ EXTRA_JS_MODULES += [
"Sqlite.sys.mjs",
"SubDialog.sys.mjs",
"Timer.sys.mjs",
"TorConnect.sys.mjs",
"TorSettings.sys.mjs",
"TorStrings.sys.mjs",
"Troubleshoot.sys.mjs",
"UpdateUtils.sys.mjs",
......
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment