Skip to content
Snippets Groups Projects
Commit 1aaa1516 authored by Richard Pospesel's avatar Richard Pospesel Committed by morgan
Browse files

Bug 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 a17d5071
No related branches found
No related tags found
2 merge requests!1202Bug_43099: 2024 YEC Strings,!1136Bug 43085: Rebased alpha onto 128.2.0esr
......@@ -303,3 +303,4 @@ dom/base/test/jsmodules/import_circular_1.mjs
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
......@@ -1532,3 +1532,4 @@ 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
......@@ -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();
}
}
......@@ -153,3 +153,5 @@ toolkit.jar:
# Third party files
content/global/third_party/d3/d3.js (/third_party/js/d3/d3.js)
content/global/third_party/cfworker/json-schema.js (/third_party/js/cfworker/json-schema.js)
content/global/pt_config.json (pt_config.json)
{
"_comment": "Used for dev build, replaced for release builds in tor-browser-build. This file is copied from tor-browser-build 0554d981:projects/tor-expert-bundle/pt_config.json",
"recommendedDefault" : "obfs4",
"pluggableTransports" : {
"lyrebird" : "ClientTransportPlugin meek_lite,obfs2,obfs3,obfs4,scramblesuit exec ${pt_path}lyrebird${pt_extension}",
"snowflake" : "ClientTransportPlugin snowflake exec ${pt_path}snowflake-client${pt_extension}",
"webtunnel" : "ClientTransportPlugin webtunnel exec ${pt_path}webtunnel-client${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.18:80 BE776A53492E1E044A26F17306E1BC46A55A1625 url=https://meek.azureedge.net/ front=ajax.aspnetcdn.com"
],
"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.l.google.com:19302,stun:stun.antisip.com:3478,stun:stun.bluesip.net:3478,stun:stun.dus.net:3478,stun:stun.epygi.com:3478,stun:stun.sonetel.com:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.voys.nl:3478 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.l.google.com:19302,stun:stun.antisip.com:3478,stun:stun.bluesip.net:3478,stun:stun.dus.net:3478,stun:stun.epygi.com:3478,stun:stun.sonetel.net:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.voys.nl:3478 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,
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 response = await this._moatRPC.check(
"obfs4",
this._challenge,
solution,
false
);
this._bridges = response?.bridges;
return this._bridges;
},
async requestNewCaptchaImage() {
try {
if (!this._moatRPC) {
this._moatRPC = new lazy.MoatRPC();
await this._moatRPC.init();
}
const response = await this._moatRPC.fetch(["obfs4"]);
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() {
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.proxy.uri;
}
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;
}
}
/**
* 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;
});
}
// callers wait on this for final response
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("MoatRPC: 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("MoatRPC: 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 POST request with a JSON body and a JSON response.
*
* @param {string} url The URL to load
* @param {object} args The arguments to send to the procedure. It will be
* serialized to JSON by this function and then set as POST body
* @returns {Promise<object>} A promise with the parsed response
*/
async buildPostRequest(url, args) {
const ch = this.buildHttpHandler(url);
const argsJson = JSON.stringify(args);
const inStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
Ci.nsIStringInputStream
);
inStream.setData(argsJson, argsJson.length);
const upChannel = ch.QueryInterface(Ci.nsIUploadChannel);
const contentType = "application/vnd.api+json";
upChannel.setUploadStream(inStream, contentType, argsJson.length);
ch.requestMethod = "POST";
// Make request
const listener = new ResponseListener();
await ch.asyncOpen(listener, ch);
// wait for response
const responseJSON = await listener.response();
// parse that JSON
return JSON.parse(responseJSON);
}
}
/* 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: "Moat",
});
ChromeUtils.defineESModuleGetters(lazy, {
DomainFrontRequestBuilder:
"resource://gre/modules/DomainFrontedRequests.sys.mjs",
TorBridgeSource: "resource://gre/modules/TorSettings.sys.mjs",
TorSettings: "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",
});
/**
* A special response listener that collects the received headers.
*/
class InternetTestResponseListener {
#promise;
#resolve;
#reject;
constructor() {
this.#promise = new Promise((resolve, reject) => {
this.#resolve = resolve;
this.#reject = reject;
});
}
// callers wait on this for final response
get status() {
return this.#promise;
}
onStartRequest() {}
// resolve or reject our Promise
onStopRequest(request, status) {
try {
const statuses = {
components: status,
successful: Components.isSuccessCode(status),
};
try {
if (statuses.successful) {
statuses.http = request.responseStatus;
statuses.date = request.getResponseHeader("Date");
}
} catch (err) {
console.warn(
"Successful request, but could not get the HTTP status or date",
err
);
}
this.#resolve(statuses);
} catch (err) {
this.#reject(err);
}
}
onDataAvailable() {
// We do not care of the actual data, as long as we have a successful
// connection
}
}
/**
* 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;
}
async #makeRequest(procedure, args) {
const procedureURIString = `${Services.prefs.getStringPref(
TorLauncherPrefs.moat_service
)}/${procedure}`;
return this.#requestBuilder.buildPostRequest(procedureURIString, args);
}
async testInternetConnection() {
const uri = `${Services.prefs.getStringPref(
TorLauncherPrefs.moat_service
)}/circumvention/countries`;
const ch = this.#requestBuilder.buildHttpHandler(uri);
ch.requestMethod = "HEAD";
const listener = new InternetTestResponseListener();
await ch.asyncOpen(listener, ch);
return listener.status;
}
// Receive a CAPTCHA challenge, takes the following parameters:
// - transports: array of transport strings available to us eg: ["obfs4", "meek"]
//
// returns an object with the following fields:
// - transport: a transport string the moat server decides it will send you selected
// from the list of provided transports
// - image: a base64 encoded jpeg with the captcha to complete
// - challenge: a nonce/cookie string associated with this request
async fetch(transports) {
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 = await this.#makeRequest("fetch", args);
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 CAPTCHA challenge and get back bridges, takes the following
// parameters:
// - transport: the transport string associated with a previous fetch request
// - challenge: the nonce string associated with the fetch request
// - solution: solution to the CAPTCHA associated with the fetch request
// - qrcode: true|false whether we want to get back a qrcode containing the bridge strings
//
// returns an object with the following fields:
// - bridges: an array of bridge line strings
// - qrcode: base64 encoded jpeg of bridges if requested, otherwise null
// if the provided solution is incorrect, returns an empty object
async check(transport, challenge, solution, qrcode) {
const args = {
data: [
{
id: "2",
version: "0.1.0",
type: "moat-solution",
transport,
challenge,
solution,
qrcode: qrcode ? "true" : "false",
},
],
};
const response = await this.#makeRequest("check", args);
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 {};
}
throw new Error(`MoatRPC: ${detail} (${code})`);
}
const bridges = response.data[0].bridges;
const qrcodeImg = qrcode ? response.data[0].qrcode : null;
return { bridges, qrcode: qrcodeImg };
}
// Convert received settings object to format used by TorSettings module.
#fixupSettings(settings) {
if (!("bridges" in settings)) {
throw new Error("Expected to find `bridges` in the settings object.");
}
const retval = {
bridges: {
enabled: true,
},
};
switch (settings.bridges.source) {
case "builtin":
retval.bridges.source = lazy.TorBridgeSource.BuiltIn;
retval.bridges.builtin_type = settings.bridges.type;
// TorSettings will ignore strings for built-in bridges, and use the
// ones it already knows, instead. However, when we try these settings
// in the connect assist, we skip TorSettings. Therefore, we set the
// lines also here (the ones we already know, not the ones we receive
// from Moat). This needs TorSettings to be initialized, which by now
// should have already happened (this method is used only by TorConnect,
// that needs TorSettings to be initialized).
// In any case, getBuiltinBridges will throw if the data is not ready,
// yet.
retval.bridges.bridge_strings = lazy.TorSettings.getBuiltinBridges(
settings.bridges.type
);
break;
case "bridgedb":
retval.bridges.source = lazy.TorBridgeSource.BridgeDB;
if (settings.bridges.bridge_strings) {
retval.bridges.bridge_strings = settings.bridges.bridge_strings;
} else {
throw new Error(
"Received no bridge-strings for BridgeDB bridge source"
);
}
break;
default:
throw new Error(
`Unexpected bridge source '${settings.bridges.source}'`
);
}
return retval;
}
// Converts a list of settings objects received from BridgeDB to a list of
// settings objects understood by the TorSettings module.
// In the event of error, returns an empty list.
#fixupSettingsList(settingsList) {
const retval = [];
for (const settings of settingsList) {
try {
retval.push(this.#fixupSettings(settings));
} catch (ex) {
log.error(ex);
}
}
return retval;
}
// Request tor settings for the user optionally based on their location
// (derived from their IP). Takes the following parameters:
// - transports: optional, an array of transports available to the client; if
// empty (or not given) returns settings using all working transports known
// to the server
// - country: optional, an ISO 3166-1 alpha-2 country code to request settings
// for; if not provided the country is determined by the user's IP address
//
// Returns an object with the detected country code and an array of settings
// in a format that can be passed to the TorSettings module. This array might
// be empty if the country has no associated settings.
// If the server cannot determine the user's country (and no country code is
// provided), then null is returned instead of the object.
async circumvention_settings(transports, country) {
const args = {
transports: transports ? transports : [],
country,
};
const response = await this.#makeRequest("circumvention/settings", args);
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.settings = this.#fixupSettingsList(response.settings);
}
if ("country" in response) {
settings.country = response.country;
}
return settings;
}
// Request a list of country codes with available censorship circumvention
// settings.
//
// returns an array of ISO 3166-1 alpha-2 country codes which we can query
// settings for.
async circumvention_countries() {
const args = {};
return this.#makeRequest("circumvention/countries", args);
}
// 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 defaul/fallback bridge settings, takes the following
// parameters:
// - transports: optional, an array of transports available to the client; if
// empty (or not given) returns settings using all working transports known
// to the server
//
// returns an array of settings objects in roughly the same format as the
// _settings object on the TorSettings module
async circumvention_defaults(transports) {
const args = {
transports: transports ? transports : [],
};
const response = await this.#makeRequest("circumvention/defaults", args);
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.#fixupSettingsList(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",
......@@ -188,6 +190,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",
......@@ -209,6 +212,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 register or to comment