Pier Angelo Vendrame pushed to branch tor-browser-115.5.0esr-13.5-1 at The Tor Project / Applications / Tor Browser
Commits: 5ad20208 by Pier Angelo Vendrame at 2023-12-06T18:31:01+01:00 fixup! Bug 25741: TBA: Disable GeckoNetworkManager
Fixed a trailing whitespace
- - - - - 8c95910c by Pier Angelo Vendrame at 2023-12-06T18:31:02+01:00 fixup! Bug 40597: Implement TorSettings module
Implement an Android-specific transport, that relies on the Java side to start the domain fronting proxy.
- - - - - fb609914 by Pier Angelo Vendrame at 2023-12-07T19:35:29+01:00 Bug 42247: Android helpers for the TorProvider
GeckoView is missing some API we use on desktop for the integration with the tor daemon, such as subprocess. Therefore, we need to implement them in Java and plumb the data back and forth between JS and Java.
- - - - - 1b7630ec by Pier Angelo Vendrame at 2023-12-07T19:35:34+01:00 fixup! Bug 40933: Add tor-launcher functionality
Use a custom TorProcessAndroid in the TorProvider on Android.
- - - - - d0ae1f7e by Pier Angelo Vendrame at 2023-12-07T19:35:35+01:00 fixup! Bug 42247: Android helpers for the TorProvider
Test to plumb down the circuit display data
- - - - - 51d44491 by Pier Angelo Vendrame at 2023-12-07T19:35:35+01:00 fixup! Bug 42247: Android helpers for the TorProvider
Switch from a gate based on the update channel to a gate based on a pref. Also, draft the JS part for TorSettings events.
- - - - - be3afbb8 by Pier Angelo Vendrame at 2023-12-07T19:35:55+01:00 fixup! Bug 42247: Android helpers for the TorProvider
Keep only a TorProcess in memory at a time. When we forget a process from JS, we also delete the Java instance. When we start a new process, we delete the old one if any (but emit a warning in logs, in case).
- - - - - 766abfe6 by Pier Angelo Vendrame at 2023-12-07T19:35:58+01:00 fixup! Bug 40933: Add tor-launcher functionality
Make TorProcessAndroid.forget signal to Java the forget request.
- - - - -
13 changed files:
- mobile/android/components/geckoview/GeckoViewStartup.jsm - mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoRuntime.java - mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoRuntimeSettings.java - mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java - + mobile/android/geckoview/src/main/java/org/mozilla/geckoview/TorIntegrationAndroid.java - mobile/android/modules/geckoview/GeckoViewContent.sys.mjs - + toolkit/components/tor-launcher/TorProcessAndroid.sys.mjs - toolkit/components/tor-launcher/TorProvider.sys.mjs - toolkit/components/tor-launcher/moz.build - toolkit/modules/Moat.sys.mjs - + toolkit/modules/TorAndroidIntegration.sys.mjs - toolkit/modules/TorConnect.sys.mjs - toolkit/modules/moz.build
Changes:
===================================== mobile/android/components/geckoview/GeckoViewStartup.jsm ===================================== @@ -5,6 +5,10 @@
var EXPORTED_SYMBOLS = ["GeckoViewStartup"];
+const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + const { GeckoViewUtils } = ChromeUtils.importESModule( "resource://gre/modules/GeckoViewUtils.sys.mjs" ); @@ -17,6 +21,7 @@ ChromeUtils.defineESModuleGetters(lazy, { PdfJs: "resource://pdf.js/PdfJs.sys.mjs", Preferences: "resource://gre/modules/Preferences.sys.mjs", RFPHelper: "resource://gre/modules/RFPHelper.sys.mjs", + TorAndroidIntegration: "resource://gre/modules/TorAndroidIntegration.sys.mjs", TorDomainIsolator: "resource://gre/modules/TorDomainIsolator.sys.mjs", });
@@ -259,6 +264,7 @@ class GeckoViewStartup { "GeckoView:SetLocale", ]);
+ lazy.TorAndroidIntegration.init(); lazy.TorDomainIsolator.init();
Services.obs.addObserver(this, "browser-idle-startup-tasks-finished");
===================================== mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoRuntime.java ===================================== @@ -167,7 +167,7 @@ public final class GeckoRuntime implements Parcelable { if (!BuildConfig.TOR_BROWSER) { GeckoNetworkManager.getInstance().start(GeckoAppShell.getApplicationContext()); } else { - Log.d(LOGTAG, "Tor Browser: skip GeckoNetworkManager startup"); + Log.d(LOGTAG, "Tor Browser: skip GeckoNetworkManager startup"); }
// Set settings that may have changed between last app opening @@ -230,6 +230,8 @@ public final class GeckoRuntime implements Parcelable { private final ProfilerController mProfilerController; private final GeckoScreenChangeListener mScreenChangeListener;
+ private TorIntegrationAndroid mTorIntegration; + private GeckoRuntime() { mWebExtensionController = new WebExtensionController(this); mContentBlockingController = new ContentBlockingController(); @@ -484,6 +486,8 @@ public final class GeckoRuntime implements Parcelable { mScreenChangeListener.enable(); }
+ mTorIntegration = new TorIntegrationAndroid(context); + mProfilerController.addMarker( "GeckoView Initialization START", mProfilerController.getProfilerTime()); return true; @@ -600,6 +604,10 @@ public final class GeckoRuntime implements Parcelable { mScreenChangeListener.disable(); }
+ if (mTorIntegration != null) { + mTorIntegration.shutdown(); + } + GeckoThread.forceQuit(); }
===================================== mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoRuntimeSettings.java ===================================== @@ -487,6 +487,11 @@ public final class GeckoRuntimeSettings extends RuntimeSettings { getSettings().mPrioritizeOnions.set(flag); return this; } + + public @NonNull Builder useNewBootstrap(final boolean flag) { + getSettings().mUseNewBootstrap.set(flag); + return this; + } }
private GeckoRuntime mRuntime; @@ -540,6 +545,8 @@ public final class GeckoRuntimeSettings extends RuntimeSettings { new Pref<>("browser.security_level.security_slider", 4); /* package */ final Pref<Boolean> mPrioritizeOnions = new Pref<>("privacy.prioritizeonions.enabled", false); + /* package */ final Pref<Boolean> mUseNewBootstrap = + new Pref<>("browser.tor_android.use_new_bootstrap", false);
/* package */ int mPreferredColorScheme = COLOR_SCHEME_SYSTEM;
@@ -1352,6 +1359,15 @@ public final class GeckoRuntimeSettings extends RuntimeSettings { return this; }
+ public boolean getUseNewBootstrap() { + return mUseNewBootstrap.get(); + } + + public @NonNull GeckoRuntimeSettings setUseNewBootstrap(final boolean flag) { + mUseNewBootstrap.commit(flag); + return this; + } + @Override // Parcelable public void writeToParcel(final Parcel out, final int flags) { super.writeToParcel(out, flags);
===================================== mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java ===================================== @@ -2493,6 +2493,16 @@ public class GeckoSession { return mEventDispatcher.queryBoolean("GeckoView:IsPdfJs"); }
+ /** + * Try to get last circuit used in this session, if possible. + * + * @return The circuit information as a {@link GeckoResult} object. + */ + @AnyThread + public @NonNull GeckoResult<GeckoBundle> getTorCircuit() { + return mEventDispatcher.queryBundle("GeckoView:GetTorCircuit"); + } + /** * Set this GeckoSession as active or inactive, which represents if the session is currently * visible or not. Setting a GeckoSession to inactive will significantly reduce its memory
===================================== mobile/android/geckoview/src/main/java/org/mozilla/geckoview/TorIntegrationAndroid.java ===================================== @@ -0,0 +1,435 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * vim: ts=4 sw=4 expandtab: + * 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/. */ + +package org.mozilla.geckoview; + +import android.content.Context; +import android.util.Log; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import org.mozilla.gecko.EventDispatcher; +import org.mozilla.gecko.GeckoAppShell; +import org.mozilla.gecko.util.BundleEventListener; +import org.mozilla.gecko.util.EventCallback; +import org.mozilla.gecko.util.GeckoBundle; + +/* package */ class TorIntegrationAndroid implements BundleEventListener { + private static final String TAG = "TorIntegrationAndroid"; + + private static final String TOR_EVENT_START = "GeckoView:Tor:StartTor"; + private static final String TOR_EVENT_STOP = "GeckoView:Tor:StopTor"; + private static final String MEEK_EVENT_START = "GeckoView:Tor:StartMeek"; + private static final String MEEK_EVENT_STOP = "GeckoView:Tor:StopMeek"; + + private static final String CONTROL_PORT_FILE = "/control-ipc"; + private static final String SOCKS_FILE = "/socks-ipc"; + private static final String COOKIE_AUTH_FILE = "/auth-file"; + + private final String mLibraryDir; + private final Path mCacheDir; + private final String mIpcDirectory; + private final String mDataDir; + + private TorProcess mTorProcess = null; + /** + * The first time we run a Tor process in this session, we copy some configuration files to be + * sure we always have the latest version, but if we re-launch a tor process we do not need to + * copy them again. + */ + private boolean mCopiedConfigFiles = false; + /** + * Allow multiple proxies to be started, even though it might not actually happen. + * The key should be positive (also 0 is not allowed). + */ + private final HashMap<Integer, MeekTransport> mMeeks = new HashMap<>(); + private int mMeekCounter; + + public TorIntegrationAndroid(Context context) { + mLibraryDir = context.getApplicationInfo().nativeLibraryDir; + mCacheDir = context.getCacheDir().toPath(); + mIpcDirectory = mCacheDir + "/tor-private"; + mDataDir = context.getDataDir().getAbsolutePath() + "/tor"; + registerListener(); + } + + public synchronized void shutdown() { + // FIXME: It seems this never gets called + if (mTorProcess != null) { + mTorProcess.shutdown(); + mTorProcess = null; + } + } + + private void registerListener() { + EventDispatcher.getInstance() + .registerUiThreadListener( + this, + TOR_EVENT_START, + MEEK_EVENT_START, + MEEK_EVENT_STOP); + } + + @Override // BundleEventListener + public synchronized void handleMessage( + final String event, final GeckoBundle message, final EventCallback callback) { + if (TOR_EVENT_START.equals(event)) { + startDaemon(message, callback); + } else if (TOR_EVENT_STOP.equals(event)) { + stopDaemon(message, callback); + } else if (MEEK_EVENT_START.equals(event)) { + startMeek(message, callback); + } else if (MEEK_EVENT_STOP.equals(event)) { + stopMeek(message, callback); + } + } + + private synchronized void startDaemon(final GeckoBundle message, final EventCallback callback) { + // Let JS generate this to possibly reduce the chance of race conditions. + String handle = message.getString("handle", ""); + if (handle.isEmpty()) { + Log.e(TAG, "Requested to start a tor process without a handle."); + callback.sendError("Expected a handle for the new process."); + return; + } + Log.d(TAG, "Starting the a tor process with handle " + handle); + + TorProcess previousProcess = mTorProcess; + if (previousProcess != null) { + Log.w(TAG, "We still have a running process: " + previousProcess.getHandle()); + } + mTorProcess = new TorProcess(handle); + + GeckoBundle bundle = new GeckoBundle(3); + bundle.putString("controlPortPath", mIpcDirectory + CONTROL_PORT_FILE); + bundle.putString("socksPath", mIpcDirectory + SOCKS_FILE); + bundle.putString("cookieFilePath", mIpcDirectory + COOKIE_AUTH_FILE); + callback.sendSuccess(bundle); + } + + private synchronized void stopDaemon(final GeckoBundle message, final EventCallback callback) { + if (mTorProcess == null) { + if (callback != null) { + callback.sendSuccess(null); + } + return; + } + String handle = message.getString("handle", ""); + if (!mTorProcess.getHandle().equals(handle)) { + GeckoBundle bundle = new GeckoBundle(1); + bundle.putString("error", "The requested process has not been found. It might have already been stopped."); + callback.sendError(bundle); + return; + } + mTorProcess.shutdown(); + mTorProcess = null; + callback.sendSuccess(null); + } + + class TorProcess extends Thread { + private static final String TOR_EVENT_STARTED = "GeckoView:Tor:TorStarted"; + private static final String TOR_EVENT_START_FAILED = "GeckoView:Tor:TorStartFailed"; + private static final String TOR_EVENT_EXITED = "GeckoView:Tor:TorExited"; + private final String mHandle; + private Process mProcess = null; + + TorProcess(String handle) { + mHandle = handle; + setName("tor-process-" + handle); + start(); + } + + @Override + public void run() { + cleanIpcDirectory(); + + final String ipcDir = TorIntegrationAndroid.this.mIpcDirectory; + final ArrayList<String> args = new ArrayList<>(); + args.add(mLibraryDir + "/libTor.so"); + args.add("DisableNetwork"); + args.add("1"); + args.add("+__ControlPort"); + args.add("unix:" + ipcDir + CONTROL_PORT_FILE); + args.add("+__SocksPort"); + args.add("unix:" + ipcDir + SOCKS_FILE + " IPv6Traffic PreferIPv6 KeepAliveIsolateSOCKSAuth"); + args.add("CookieAuthentication"); + args.add("1"); + args.add("CookieAuthFile"); + args.add(ipcDir + COOKIE_AUTH_FILE); + args.add("DataDirectory"); + args.add(mDataDir); + boolean copied = true; + try { + copyAndUseConfigFile("--defaults-torrc", "torrc-defaults", args); + } catch (IOException e) { + Log.w(TAG, "torrc-default cannot be created, pluggable transports will not be available", e); + copied = false; + } + try { + copyAndUseConfigFile("GeoIPFile", "geoip", args); + copyAndUseConfigFile("GeoIPv6File", "geoip6", args); + } catch (IOException e) { + Log.w(TAG, "GeoIP files cannot be created, this feature will not be available.", e); + copied = false; + } + mCopiedConfigFiles = copied; + + Log.d(TAG, "Starting tor with the follwing args: " + args.toString()); + final ProcessBuilder builder = new ProcessBuilder(args); + builder.directory(new File(mLibraryDir)); + try { + mProcess = builder.start(); + } catch (IOException e) { + Log.e(TAG, "Cannot start tor " + mHandle, e); + final GeckoBundle data = new GeckoBundle(2); + data.putString("handle", mHandle); + data.putString("error", e.getMessage()); + EventDispatcher.getInstance().dispatch(TOR_EVENT_START_FAILED, data); + return; + } + Log.i(TAG, "Tor process " + mHandle + " started."); + { + final GeckoBundle data = new GeckoBundle(1); + data.putString("handle", mHandle); + EventDispatcher.getInstance().dispatch(TOR_EVENT_STARTED, data); + } + try { + BufferedReader reader = new BufferedReader(new InputStreamReader(mProcess.getInputStream())); + String line; + while ((line = reader.readLine()) != null) { + Log.i(TAG, "[tor-" + mHandle + "] " + line); + } + } catch (IOException e) { + Log.e(TAG, "Failed to read stdout of the tor process " + mHandle, e); + } + Log.d(TAG, "Exiting the stdout loop for process " + mHandle); + final GeckoBundle data = new GeckoBundle(2); + data.putString("handle", mHandle); + try { + data.putInt("status", mProcess.waitFor()); + } catch (InterruptedException e) { + Log.e(TAG, "Failed to wait for the tor process " + mHandle, e); + data.putInt("status", 0xdeadbeef); + } + // FIXME: We usually don't reach this when the application is killed! + // So, we don't do our cleanup. + Log.i(TAG, "Tor process " + mHandle + " has exited."); + EventDispatcher.getInstance().dispatch(TOR_EVENT_EXITED, data); + } + + private void cleanIpcDirectory() { + File directory = new File(TorIntegrationAndroid.this.mIpcDirectory); + if (!Files.isDirectory(directory.toPath())) { + if (!directory.mkdirs()) { + Log.e(TAG, "Failed to create the IPC directory."); + return; + } + try { + Set<PosixFilePermission> chmod = PosixFilePermissions.fromString("rwx------"); + Files.setPosixFilePermissions(directory.toPath(), chmod); + } catch (IOException e) { + Log.e(TAG, "Could not set the permissions to the IPC directory.", e); + } + return; + } + // We assume we do not have child directories, only files + File[] maybeFiles = directory.listFiles(); + if (maybeFiles != null) { + for (File file : maybeFiles) { + if (!file.delete()) { + Log.d(TAG, "Could not delete " + file); + } + } + } + } + + private void copyAndUseConfigFile(String option, String name, ArrayList<String> args) throws IOException { + final Path path = Paths.get(mCacheDir.toFile().getAbsolutePath(), name); + if (!mCopiedConfigFiles || !path.toFile().exists()) { + final Context context = GeckoAppShell.getApplicationContext(); + final InputStream in = context.getAssets().open("common/" + name); + Files.copy(in, path, StandardCopyOption.REPLACE_EXISTING); + in.close(); + } + args.add(option); + args.add(path.toString()); + } + + public void shutdown() { + if (mProcess != null && mProcess.isAlive()) { + mProcess.destroy(); + } + if (isAlive()) { + try { + join(); + } catch (InterruptedException e) { + Log.e(TAG, "Cannot join the thread for tor process " + mHandle + ", possibly already terminated", e); + } + } + } + + public String getHandle() { + return mHandle; + } + } + + private synchronized void startMeek(final GeckoBundle message, final EventCallback callback) { + if (callback == null) { + Log.e(TAG, "Tried to start Meek without a callback."); + return; + } + mMeekCounter++; + mMeeks.put(new Integer(mMeekCounter), new MeekTransport(callback, mMeekCounter)); + } + + private synchronized void stopMeek(final GeckoBundle message, final EventCallback callback) { + final Integer key = message.getInteger("id"); + final MeekTransport meek = mMeeks.remove(key); + if (meek != null) { + meek.shutdown(); + } + if (callback != null) { + callback.sendSuccess(null); + } + } + + private class MeekTransport extends Thread { + private static final String TRANSPORT = "meek_lite"; + private Process mProcess; + private final EventCallback mCallback; + private final int mId; + + MeekTransport(final EventCallback callback, int id) { + setName("meek-" + id); + final ProcessBuilder builder = new ProcessBuilder(mLibraryDir + "/libObfs4proxy.so"); + { + final Map<String, String> env = builder.environment(); + env.put("TOR_PT_MANAGED_TRANSPORT_VER", "1"); + env.put("TOR_PT_STATE_LOCATION", mDataDir + "/pt_state"); + env.put("TOR_PT_EXIT_ON_STDIN_CLOSE", "1"); + env.put("TOR_PT_CLIENT_TRANSPORTS", TRANSPORT); + } + mCallback = callback; + mId = id; + try { + // We expect this process to be short-lived, therefore we do not bother with + // implementing this as a service. + mProcess = builder.start(); + } catch (IOException e) { + Log.e(TAG, "Cannot start the PT", e); + callback.sendError(e.getMessage()); + return; + } + start(); + } + + /** + * Parse the standard output of the pluggable transport to find the hostname and port it is + * listening on. + * <p> + * See also the specs for the IPC protocol at https://spec.torproject.org/pt-spec/ipc.html. + */ + @Override + public void run() { + final String PROTOCOL_VERSION = "1"; + String hostname = ""; + boolean valid = false; + int port = 0; + String error = "Did not see a CMETHOD"; + try { + InputStreamReader isr = new InputStreamReader(mProcess.getInputStream()); + BufferedReader reader = new BufferedReader(isr); + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + Log.d(TAG, "Meek line: " + line); + // Split produces always at least one item + String[] tokens = line.split(" "); + if ("VERSION".equals(tokens[0]) && (tokens.length != 2 || !PROTOCOL_VERSION.equals(tokens[1]))) { + error = "Bad version: " + line; + break; + } + if ("CMETHOD".equals(tokens[0])) { + if (tokens.length != 4) { + error = "Bad number of tokens in CMETHOD: " + line; + break; + } + if (!tokens[1].equals(TRANSPORT)) { + error = "Unexpected transport: " + tokens[1]; + break; + } + if (!"socks5".equals(tokens[2])) { + error = "Unexpected proxy type: " + tokens[2]; + break; + } + String[] addr = tokens[3].split(":"); + if (addr.length != 2) { + error = "Invalid address"; + break; + } + hostname = addr[0]; + try { + port = Integer.parseInt(addr[1]); + } catch (NumberFormatException e) { + error = "Invalid port: " + e.getMessage(); + break; + } + if (port < 1 || port > 65535) { + error = "Invalid port: out of bounds"; + break; + } + valid = true; + break; + } + if (tokens[0].endsWith("-ERROR")) { + error = "Seen an error: " + line; + break; + } + } + } catch (Exception e) { + error = e.getMessage(); + } + if (valid) { + Log.d(TAG, "Setup a meek transport " + mId + ": " + hostname + ":" + port); + final GeckoBundle bundle = new GeckoBundle(3); + bundle.putInt("id", mId); + bundle.putString("address", hostname); + bundle.putInt("port", port); + mCallback.sendSuccess(bundle); + } else { + Log.e(TAG, "Failed to get a usable config from the PT: " + error); + mCallback.sendError(error); + } + } + + void shutdown() { + if (mProcess != null) { + mProcess.destroy(); + mProcess = null; + } + try { + join(); + } catch (InterruptedException e) { + Log.e(TAG, "Could not join the meek thread", e); + } + } + } +}
===================================== mobile/android/modules/geckoview/GeckoViewContent.sys.mjs ===================================== @@ -4,6 +4,12 @@
import { GeckoViewModule } from "resource://gre/modules/GeckoViewModule.sys.mjs";
+const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + TorDomainIsolator: "resource://gre/modules/TorDomainIsolator.sys.mjs", +}); + export class GeckoViewContent extends GeckoViewModule { onInit() { this.registerListener([ @@ -22,6 +28,7 @@ export class GeckoViewContent extends GeckoViewModule { "GeckoView:UpdateInitData", "GeckoView:ZoomToInput", "GeckoView:IsPdfJs", + "GeckoView:GetTorCircuit", ]); }
@@ -190,6 +197,21 @@ export class GeckoViewContent extends GeckoViewModule { case "GeckoView:HasCookieBannerRuleForBrowsingContextTree": this._hasCookieBannerRuleForBrowsingContextTree(aCallback); break; + case "GeckoView:GetTorCircuit": + if (this.browser && aCallback) { + const domain = lazy.TorDomainIsolator.getDomainForBrowser( + this.browser + ); + const nodes = lazy.TorDomainIsolator.getCircuit( + this.browser, + domain, + this.browser.contentPrincipal.originAttributes.userContextId + ); + aCallback?.onSuccess({ domain, nodes }); + } else { + aCallback?.onSuccess(null); + } + break; } }
===================================== toolkit/components/tor-launcher/TorProcessAndroid.sys.mjs ===================================== @@ -0,0 +1,122 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { ConsoleAPI } from "resource://gre/modules/Console.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + EventDispatcher: "resource://gre/modules/Messaging.sys.mjs", +}); + +const logger = new ConsoleAPI({ + maxLogLevel: "info", + prefix: "TorProcessAndroid", +}); + +const TorOutgoingEvents = Object.freeze({ + start: "GeckoView:Tor:StartTor", + stop: "GeckoView:Tor:StopTor", +}); + +// The events we will listen to +const TorIncomingEvents = Object.freeze({ + started: "GeckoView:Tor:TorStarted", + startFailed: "GeckoView:Tor:TorStartFailed", + exited: "GeckoView:Tor:TorExited", +}); + +export class TorProcessAndroid { + /** + * The handle the Java counterpart uses to refer to the process we started. + * We use it to filter the exit events and make sure they refer to the daemon + * we are interested in. + */ + #processHandle = null; + /** + * The promise resolver we call when the Java counterpart sends the event that + * tor has started. + */ + #startResolve = null; + /** + * The promise resolver we call when the Java counterpart sends the event that + * it failed to start tor. + */ + #startReject = null; + + onExit = () => {}; + + get isRunning() { + return !!this.#processHandle; + } + + async start() { + // Generate the handle on the JS side so that it's ready in case it takes + // less to start the process than to propagate the success. + this.#processHandle = crypto.randomUUID(); + logger.info(`Starting new process with handle ${this.#processHandle}`); + // Let's declare it immediately, so that the Java side can do its stuff in + // an async manner and we avoid possible race conditions (at most we await + // an already resolved/rejected promise. + const startEventPromise = new Promise((resolve, reject) => { + this.#startResolve = resolve; + this.#startReject = reject; + }); + lazy.EventDispatcher.instance.registerListener( + this, + Object.values(TorIncomingEvents) + ); + let config; + try { + config = await lazy.EventDispatcher.instance.sendRequestForResult({ + type: TorOutgoingEvents.start, + handle: this.#processHandle, + }); + logger.debug("Sent the start event."); + } catch (e) { + this.forget(); + throw e; + } + await startEventPromise; + return config; + } + + forget() { + // Processes usually exit when we close the control port connection to them. + logger.trace(`Forgetting process ${this.#processHandle}`); + lazy.EventDispatcher.instance.sendRequestForResult({ + type: TorOutgoingEvents.stop, + handle: this.#processHandle, + }); + logger.debug("Sent the start event."); + this.#processHandle = null; + lazy.EventDispatcher.instance.unregisterListener( + this, + Object.values(TorIncomingEvents) + ); + } + + onEvent(event, data, callback) { + if (data?.handle !== this.#processHandle) { + logger.debug(`Ignoring event ${event} with another handle`, data); + return; + } + logger.info(`Received an event ${event}`, data); + switch (event) { + case TorIncomingEvents.started: + this.#startResolve(); + break; + case TorIncomingEvents.startFailed: + this.#startReject(new Error(data.error)); + break; + case TorIncomingEvents.exited: + this.forget(); + if (this.#startReject !== null) { + this.#startReject(); + } + this.onExit(data.status); + break; + } + } +}
===================================== toolkit/components/tor-launcher/TorProvider.sys.mjs ===================================== @@ -14,6 +14,7 @@ ChromeUtils.defineESModuleGetters(lazy, { FileUtils: "resource://gre/modules/FileUtils.sys.mjs", TorController: "resource://gre/modules/TorControlPort.sys.mjs", TorProcess: "resource://gre/modules/TorProcess.sys.mjs", + TorProcessAndroid: "resource://gre/modules/TorProcessAndroid.sys.mjs", });
const logger = new ConsoleAPI({ @@ -182,8 +183,12 @@ export class TorProvider { logger.debug("Initializing the Tor provider.");
// These settings might be customized in the following steps. - this.#socksSettings = TorLauncherUtil.getPreferredSocksConfiguration(); - logger.debug("Requested SOCKS configuration", this.#socksSettings); + if (TorLauncherUtil.isAndroid) { + this.#socksSettings = { transproxy: false }; + } else { + this.#socksSettings = TorLauncherUtil.getPreferredSocksConfiguration(); + logger.debug("Requested SOCKS configuration", this.#socksSettings); + }
try { await this.#setControlPortConfiguration(); @@ -490,10 +495,14 @@ export class TorProvider { return; }
- this.#torProcess = new lazy.TorProcess( - this.#controlPortSettings, - this.#socksSettings - ); + if (TorLauncherUtil.isAndroid) { + this.#torProcess = new lazy.TorProcessAndroid(); + } else { + this.#torProcess = new lazy.TorProcess( + this.#controlPortSettings, + this.#socksSettings + ); + } // Use a closure instead of bind because we reassign #cancelConnection. // Also, we now assign an exit handler that cancels the first connection, // so that a sudden exit before the first connection is completed might @@ -507,7 +516,17 @@ export class TorProvider { };
logger.debug("Trying to start the tor process."); - await this.#torProcess.start(); + const res = await this.#torProcess.start(); + if (TorLauncherUtil.isAndroid) { + this.#controlPortSettings = { + ipcFile: new lazy.FileUtils.File(res.controlPortPath), + cookieFilePath: res.cookieFilePath, + }; + this.#socksSettings = { + transproxy: false, + ipcFile: new lazy.FileUtils.File(res.socksPath), + }; + } logger.info("Started a tor process"); }
@@ -521,6 +540,11 @@ export class TorProvider { logger.debug("Reading the control port configuration"); const settings = {};
+ if (TorLauncherUtil.isAndroid) { + // We will populate the settings after having started the daemon. + return; + } + const isWindows = Services.appinfo.OS === "WINNT"; // Determine how Tor Launcher will connect to the Tor control port. // Environment variables get top priority followed by preferences.
===================================== toolkit/components/tor-launcher/moz.build ===================================== @@ -5,6 +5,7 @@ EXTRA_JS_MODULES += [ "TorLauncherUtil.sys.mjs", "TorParsers.sys.mjs", "TorProcess.sys.mjs", + "TorProcessAndroid.sys.mjs", "TorProvider.sys.mjs", "TorProviderBuilder.sys.mjs", "TorStartupService.sys.mjs",
===================================== toolkit/modules/Moat.sys.mjs ===================================== @@ -10,6 +10,7 @@ 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", @@ -290,6 +291,48 @@ class MeekTransport { } }
+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 // @@ -407,7 +450,10 @@ export class MoatRPC { throw new Error("MoatRPC: Already initialized"); }
- const meekTransport = new MeekTransport(); + const meekTransport = + Services.appinfo.OS === "Android" + ? new MeekTransportAndroid() + : new MeekTransport(); await meekTransport.init(); this.#meekTransport = meekTransport; this.#inited = true;
===================================== toolkit/modules/TorAndroidIntegration.sys.mjs ===================================== @@ -0,0 +1,108 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { ConsoleAPI } from "resource://gre/modules/Console.sys.mjs"; + +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + EventDispatcher: "resource://gre/modules/Messaging.sys.mjs", + TorConnect: "resource://gre/modules/TorConnect.sys.mjs", + TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs", + TorSettings: "resource://gre/modules/TorSettings.sys.mjs", +}); + +const Prefs = Object.freeze({ + useNewBootstrap: "browser.tor_android.use_new_bootstrap", + logLevel: "browser.tor_android.log_level", +}); + +const logger = new ConsoleAPI({ + maxLogLevel: "info", + maxLogLevelPref: Prefs.logLevel, + prefix: "TorAndroidIntegration", +}); + +const ListenedEvents = Object.freeze({ + settingsGet: "GeckoView:Tor:SettingsGet", + settingsSet: "GeckoView:Tor:SettingsSet", + settingsApply: "GeckoView:Tor:SettingsApply", + settingsSave: "GeckoView:Tor:SettingsSave", +}); + +class TorAndroidIntegrationImpl { + #initialized = false; + + init() { + lazy.EventDispatcher.instance.registerListener( + this, + Object.values(ListenedEvents) + ); + + this.#bootstrapMethodReset(); + Services.prefs.addObserver(Prefs.useNewBootstrap, this); + } + + async #initNewBootstrap() { + if (this.#initialized) { + return; + } + this.#initialized = true; + + lazy.TorProviderBuilder.init().finally(() => { + lazy.TorProviderBuilder.firstWindowLoaded(); + }); + try { + await lazy.TorSettings.init(); + await lazy.TorConnect.init(); + } catch (e) { + logger.error("Cannot initialize TorSettings or TorConnect", e); + } + } + + observe(subj, topic, data) { + switch (topic) { + case "nsPref:changed": + if (data === Prefs.useNewBootstrap) { + this.#bootstrapMethodReset(); + } + break; + } + } + + async onEvent(event, data, callback) { + logger.debug(`Received event ${event}`, data); + try { + switch (event) { + case settingsGet: + callback?.onSuccess(lazy.TorSettings.getSettings()); + return; + case settingsSet: + // This does not throw, so we do not have any way to report the error! + lazy.TorSettings.setSettings(data); + break; + case settingsApply: + await lazy.TorSettings.applySettings(); + break; + case settingsSave: + await lazy.TorSettings.saveSettings(); + break; + } + callback?.onSuccess(); + } catch (e) { + logger.error(); + callback?.sendError(e); + } + } + + #bootstrapMethodReset() { + if (Services.prefs.getBoolPref(Prefs.useNewBootstrap, false)) { + this.#initNewBootstrap(); + } else { + Services.prefs.clearUserPref("network.proxy.socks"); + Services.prefs.clearUserPref("network.proxy.socks_port"); + } + } +} + +export const TorAndroidIntegration = new TorAndroidIntegrationImpl();
===================================== toolkit/modules/TorConnect.sys.mjs ===================================== @@ -793,6 +793,9 @@ export const TorConnect = (() => {
TorConnect._errorMessage = errorMessage; TorConnect._errorDetails = errorDetails; + console.error( + `[TorConnect] Entering error state (${errorMessage}, ${errorDetails})` + );
Services.obs.notifyObservers( { message: errorMessage, details: errorDetails },
===================================== toolkit/modules/moz.build ===================================== @@ -215,6 +215,7 @@ EXTRA_JS_MODULES += [ "Sqlite.sys.mjs", "SubDialog.sys.mjs", "Timer.sys.mjs", + "TorAndroidIntegration.sys.mjs", "TorConnect.sys.mjs", "TorSettings.sys.mjs", "TorStrings.sys.mjs",
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/4195dec...