Pier Angelo Vendrame pushed to branch tor-browser-115.6.0esr-13.5-1 at The Tor Project / Applications / Tor Browser
Commits: da0f3108 by Pier Angelo Vendrame at 2024-01-09T18:38:42+01:00 fixup! Bug 40597: Implement TorSettings module
Bug 42358: Extract the domain fronting request functionality form MoatRPC.
- - - - -
3 changed files:
- + toolkit/modules/DomainFrontedRequests.sys.mjs - toolkit/modules/Moat.sys.mjs - toolkit/modules/moz.build
Changes:
===================================== toolkit/modules/DomainFrontedRequests.sys.mjs ===================================== @@ -0,0 +1,525 @@ +/* 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, { + 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) { + console.log(`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(exitObj => { + 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; + } +} + +/** + * 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(request) {} + + // resolve or reject our Promise + onStopRequest(request, status) { + try { + if (!Components.isSuccessCode(status)) { + const errorMessage = + lazy.TorLauncherUtil.getLocalizedStringForError(status); + this.#reject(new Error(errorMessage)); + } + if (request.responseStatus !== 200) { + this.#reject(new Error(request.responseStatusText)); + } + } 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); + } +} + +// constructs the json objects and sends the request over moat +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, val) => { + if (key !== "Host") { + headers.push(key); + } + }, + }); + headers.forEach(key => ch.setRequestHeader(key, "", false)); + + return ch; + } + + /** + * Make a POST request with a JSON body. + * + * @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); + } +}
===================================== toolkit/modules/Moat.sys.mjs ===================================== @@ -10,10 +10,8 @@ import { const lazy = {};
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", + DomainFrontRequestBuilder: + "resource://gre/modules/DomainFrontedRequests.sys.mjs", });
const TorLauncherPrefs = Object.freeze({ @@ -22,372 +20,9 @@ const TorLauncherPrefs = Object.freeze({ moat_service: "extensions.torlauncher.moat_service", });
-function makeMeekCredentials(proxyType) { - // Construct the per-connection arguments. - let meekClientEscapedArgs = ""; - const meekReflector = Services.prefs.getStringPref( - TorLauncherPrefs.bridgedb_reflector - ); - - // 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 (meekReflector) { - meekClientEscapedArgs += "url="; - meekClientEscapedArgs += escapeArgValue(meekReflector); - } - const meekFront = Services.prefs.getStringPref( - TorLauncherPrefs.bridgedb_front - ); - if (meekFront) { - if (meekClientEscapedArgs.length) { - meekClientEscapedArgs += ";"; - } - meekClientEscapedArgs += "front="; - meekClientEscapedArgs += escapeArgValue(meekFront); - } - - // socks5 - if (proxyType === "socks") { - if (meekClientEscapedArgs.length <= 255) { - return [meekClientEscapedArgs, "\x00"]; - } else { - return [ - meekClientEscapedArgs.substring(0, 255), - meekClientEscapedArgs.substring(255), - ]; - } - // socks4 - } else { - return [meekClientEscapedArgs, undefined]; - } -} - -// -// Launches and controls the PT process lifetime -// -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() { - // 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 (TorSettings.proxy.enabled) { - envAdditions.TOR_PT_PROXY = 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) { - console.log(`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(exitObj => { - this.#meekClientProcess = null; - this.uninit(); - }); - [this.proxyUsername, this.proxyPassword] = makeMeekCredentials( - this.proxyType - ); - 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; - } -} - -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; - - #id = 0; - - async init() { - // 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 - ); - } - - 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; - } -} - -// -// Callback object with a cached promise for the returned Moat data -// -class MoatResponseListener { - #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(request) {} - - // resolve or reject our Promise - onStopRequest(request, status) { - try { - if (!Components.isSuccessCode(status)) { - const errorMessage = - lazy.TorLauncherUtil.getLocalizedStringForError(status); - this.#reject(new Error(errorMessage)); - } - if (request.responseStatus != 200) { - this.#reject(new Error(request.responseStatusText)); - } - } 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); - } -} - +/** + * A special response listener that collects the received headers. + */ class InternetTestResponseListener { #promise; #resolve; @@ -436,129 +71,45 @@ class InternetTestResponseListener { } }
-// constructs the json objects and sends the request over moat +/** + * 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.... + */ export class MoatRPC { - #inited = false; - #meekTransport = null; - - get inited() { - return this.#inited; - } + #requestBuilder = null;
async init() { - if (this.#inited) { - throw new Error("MoatRPC: Already initialized"); + if (this.#requestBuilder !== null) { + return; }
- const meekTransport = - Services.appinfo.OS === "Android" - ? new MeekTransportAndroid() - : new MeekTransport(); - await meekTransport.init(); - this.#meekTransport = meekTransport; - this.#inited = true; + const reflector = Services.prefs.getStringPref( + TorLauncherPrefs.bridgedb_reflector + ); + const front = Services.prefs.getStringPref(TorLauncherPrefs.bridgedb_front); + const builder = new lazy.DomainFrontRequestBuilder(); + await builder.init(reflector, front); + this.#requestBuilder = builder; }
async uninit() { - await this.#meekTransport?.uninit(); - this.#meekTransport = null; - this.#inited = false; - } - - #makeHttpHandler(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, val) => { - if (key !== "Host") { - headers.push(key); - } - }, - }); - headers.forEach(key => ch.setRequestHeader(key, "", false)); - - return ch; + await this.#requestBuilder?.uninit(); + this.#requestBuilder = null; }
async #makeRequest(procedure, args) { const procedureURIString = `${Services.prefs.getStringPref( TorLauncherPrefs.moat_service )}/${procedure}`; - const ch = this.#makeHttpHandler(procedureURIString); - - // Arrange for the POST data to be sent. - 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 MoatResponseListener(); - await ch.asyncOpen(listener, ch); - - // wait for response - const responseJSON = await listener.response(); - - // parse that JSON - return JSON.parse(responseJSON); + return this.#requestBuilder.buildPostRequest(procedureURIString, args); }
async testInternetConnection() { const uri = `${Services.prefs.getStringPref( TorLauncherPrefs.moat_service )}/circumvention/countries`; - const ch = this.#makeHttpHandler(uri); + const ch = this.#requestBuilder.buildHttpHandler(uri); ch.requestMethod = "HEAD";
const listener = new InternetTestResponseListener(); @@ -566,10 +117,6 @@ export class MoatRPC { return listener.status; }
- // - // Moat APIs - // - // Receive a CAPTCHA challenge, takes the following parameters: // - transports: array of transport strings available to us eg: ["obfs4", "meek"] //
===================================== toolkit/modules/moz.build ===================================== @@ -166,6 +166,7 @@ EXTRA_JS_MODULES += [ "DateTimePickerPanel.sys.mjs", "DeferredTask.sys.mjs", "Deprecated.sys.mjs", + "DomainFrontedRequests.sys.mjs", "DragDropFilter.sys.mjs", "E10SUtils.sys.mjs", "EventEmitter.sys.mjs",
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/commit/da0f3108...