Skip to content
Snippets Groups Projects
Verified Commit c7339f81 authored by Pier Angelo Vendrame's avatar Pier Angelo Vendrame :jack_o_lantern:
Browse files

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.
parent 95321b8b
No related branches found
No related tags found
1 merge request!1453TB 43587: Rebased legacy onto 115.22.0esr
Showing
with 1270 additions and 0 deletions
......@@ -17,6 +17,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 +260,7 @@ class GeckoViewStartup {
"GeckoView:SetLocale",
]);
lazy.TorAndroidIntegration.init();
lazy.TorDomainIsolator.init();
Services.obs.addObserver(this, "browser-idle-startup-tasks-finished");
......
......@@ -224,6 +224,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();
......@@ -478,6 +480,8 @@ public final class GeckoRuntime implements Parcelable {
mScreenChangeListener.enable();
}
mTorIntegration = new TorIntegrationAndroid(context);
mProfilerController.addMarker(
"GeckoView Initialization START", mProfilerController.getProfilerTime());
return true;
......@@ -594,6 +598,10 @@ public final class GeckoRuntime implements Parcelable {
mScreenChangeListener.disable();
}
if (mTorIntegration != null) {
mTorIntegration.shutdown();
}
GeckoThread.forceQuit();
}
......@@ -994,6 +1002,14 @@ public final class GeckoRuntime implements Parcelable {
return mPushController;
}
/**
* Get the Tor integration controller for this runtime.
*/
@UiThread
public @NonNull TorIntegrationAndroid getTorIntegrationController() {
return mTorIntegration;
}
/**
* Appends notes to crash report.
*
......
......@@ -487,6 +487,11 @@ public final class GeckoRuntimeSettings extends RuntimeSettings {
getSettings().mSecurityLevel.set(level);
return this;
}
public @NonNull Builder useNewBootstrap(final boolean flag) {
getSettings().mUseNewBootstrap.set(flag);
return this;
}
}
private GeckoRuntime mRuntime;
......@@ -538,6 +543,8 @@ public final class GeckoRuntimeSettings extends RuntimeSettings {
/* package */ final Pref<Integer> mSpoofEnglish = new Pref<>("privacy.spoof_english", 0);
/* package */ final Pref<Integer> mSecurityLevel =
new Pref<>("browser.security_level.security_slider", 4);
/* package */ final Pref<Boolean> mUseNewBootstrap =
new Pref<>("browser.tor_android.use_new_bootstrap", false);
/* package */ int mPreferredColorScheme = COLOR_SCHEME_SYSTEM;
......@@ -1341,6 +1348,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);
......
......@@ -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
......
package org.mozilla.geckoview;
import android.util.Log;
import org.mozilla.gecko.util.GeckoBundle;
public class TorSettings {
public enum BridgeSource {
Invalid(-1),
BuiltIn(0),
BridgeDB(1),
UserProvided(2);
private int source;
BridgeSource(final int source) {
this.source = source;
}
public static BridgeSource fromInt(int i) {
switch (i) {
case -1: return Invalid;
case 0: return BuiltIn;
case 1: return BridgeDB;
case 2: return UserProvided;
}
return Invalid;
}
public int toInt() {
return this.source;
}
}
public enum ProxyType {
Invalid(-1),
Socks4(0),
Socks5(1),
HTTPS(2);
private int type;
ProxyType(final int type) {
this.type = type;
}
public int toInt() {
return type;
}
public static ProxyType fromInt(int i) {
switch (i) {
case -1: return Invalid;
case 0: return Socks4;
case 1: return Socks5;
case 2: return HTTPS;
}
return Invalid;
}
}
public enum BridgeBuiltinType {
/* TorSettings.sys.mjs ~ln43: string: obfs4|meek-azure|snowflake|etc */
Invalid("invalid"),
Obfs4("obfs4"),
MeekAzure("meek-azure"),
Snowflake("snowflake");
private String type;
BridgeBuiltinType(String type) {
this.type = type;
}
public String toString() {
return type;
}
public static BridgeBuiltinType fromString(String s) {
switch (s) {
case "obfs4": return Obfs4;
case "meek-azure": return MeekAzure;
case "snowflake": return Snowflake;
}
return Invalid;
}
}
private boolean loaded = false;
public boolean enabled = true;
public boolean quickstart = false;
// bridges section
public boolean bridgesEnabled = false;
public BridgeSource bridgesSource = BridgeSource.Invalid;
public BridgeBuiltinType bridgesBuiltinType = BridgeBuiltinType.Invalid;
public String[] bridgeBridgeStrings;
// proxy section
public boolean proxyEnabled = false;
public ProxyType proxyType = ProxyType.Invalid;
public String proxyAddress = "";
public int proxyPort = 0;
public String proxyUsername = "";
public String proxyPassword = "";
// firewall section
public boolean firewallEnabled = false;
public int[] firewallAllowedPorts;
public TorSettings() {
}
public TorSettings(GeckoBundle bundle) {
try {
GeckoBundle qs = bundle.getBundle("quickstart");
GeckoBundle bridges = bundle.getBundle("bridges");
GeckoBundle proxy = bundle.getBundle("proxy");
GeckoBundle firewall = bundle.getBundle("firewall");
bridgesEnabled = bridges.getBoolean("enabled");
bridgesSource = BridgeSource.fromInt(bridges.getInt("source"));
bridgesBuiltinType = BridgeBuiltinType.fromString(bridges.getString("builtin_type"));
bridgeBridgeStrings = bridges.getStringArray("bridge_strings");
quickstart = qs.getBoolean("enabled");
firewallEnabled = firewall.getBoolean("enabled");
firewallAllowedPorts = firewall.getIntArray("allowed_ports");
proxyEnabled = proxy.getBoolean("enabled");
proxyAddress = proxy.getString("address");
proxyUsername = proxy.getString("username");
proxyPassword = proxy.getString("password");
proxyPort = proxy.getInt("port");
proxyType = ProxyType.fromInt(proxy.getInt("type"));
loaded = true;
} catch (Exception e) {
Log.e("TorSettings", "bundle access error: " + e.toString(), e);
}
}
public GeckoBundle asGeckoBundle() {
GeckoBundle bundle = new GeckoBundle();
GeckoBundle qs = new GeckoBundle();
GeckoBundle bridges = new GeckoBundle();
GeckoBundle proxy = new GeckoBundle();
GeckoBundle firewall = new GeckoBundle();
bridges.putBoolean("enabled", bridgesEnabled);
bridges.putInt("source", bridgesSource.toInt());
bridges.putString("builtin_type", bridgesBuiltinType.toString());
bridges.putStringArray("bridge_strings", bridgeBridgeStrings);
qs.putBoolean("enabled", quickstart);
firewall.putBoolean("enabled", firewallEnabled);
firewall.putIntArray("allowed_ports", firewallAllowedPorts);
proxy.putBoolean("enabled", proxyEnabled);
proxy.putString("address", proxyAddress);
proxy.putString("username", proxyUsername);
proxy.putString("password", proxyPassword);
proxy.putInt("port", proxyPort);
proxy.putInt("type", proxyType.toInt());
bundle.putBundle("quickstart", qs);
bundle.putBundle("bridges", bridges);
bundle.putBundle("proxy", proxy);
bundle.putBundle("firewall", firewall);
return bundle;
}
public boolean isLoaded() {
return this.loaded;
}
}
package org.mozilla.geckoview.androidlegacysettings;
import android.content.Context;
import android.content.SharedPreferences;
import org.mozilla.gecko.GeckoAppShell;
import java.util.Locale;
// tor-android-service utils/Prefs.java
/* package */ class Prefs {
private final static String PREF_BRIDGES_ENABLED = "pref_bridges_enabled";
private final static String PREF_BRIDGES_LIST = "pref_bridges_list";
private static SharedPreferences prefs;
// OrbotConstants
private final static String PREF_TOR_SHARED_PREFS = "org.torproject.android_preferences";
// tor-android-service utils/TorServiceUtil.java
private static void setContext() {
if (prefs == null) {
prefs = GeckoAppShell.getApplicationContext().getSharedPreferences(PREF_TOR_SHARED_PREFS,
Context.MODE_MULTI_PROCESS);
}
}
public static boolean getBoolean(String key, boolean def) {
setContext();
return prefs.getBoolean(key, def);
}
public static void putBoolean(String key, boolean value) {
setContext();
prefs.edit().putBoolean(key, value).apply();
}
public static void putString(String key, String value) {
setContext();
prefs.edit().putString(key, value).apply();
}
public static String getString(String key, String def) {
setContext();
return prefs.getString(key, def);
}
public static boolean bridgesEnabled() {
setContext();
// for Locale.getDefault().getLanguage().equals("fa"), bridges were enabled by default (and
// it was meek). This was a default set in 2019 code, but it is not a good default anymore,
// so we removed the check.
return prefs.getBoolean(PREF_BRIDGES_ENABLED, false);
}
public static String getBridgesList() {
setContext();
String list = prefs.getString(PREF_BRIDGES_LIST, "");
// list might be empty if the default PT was used, so check also if bridges are enabled.
if (list.isEmpty() && prefs.getBoolean(PREF_BRIDGES_ENABLED, false)) {
// Even though the check on the fa locale is not good to enable bridges by default, we
// still check it here, because if the list was empty, it was likely that it was the
// choice for users with this locale.
return (Locale.getDefault().getLanguage().equals("fa")) ? "meek": "obfs4";
}
return list;
}
}
package org.mozilla.geckoview.androidlegacysettings;
import org.mozilla.geckoview.TorSettings;
public class TorLegacyAndroidSettings {
private static String PREF_USE_MOZ_PREFS = "tor_use_moz_prefs";
public static boolean unmigrated() {
return !Prefs.getBoolean(PREF_USE_MOZ_PREFS, false);
}
public static void setUnmigrated() {
Prefs.putBoolean(PREF_USE_MOZ_PREFS, false);
}
public static void setMigrated() {
Prefs.putBoolean(PREF_USE_MOZ_PREFS, true);
}
public static TorSettings loadTorSettings() {
TorSettings settings = new TorSettings();
// always true, tor is enabled in TB
settings.enabled = true;
// firefox-android disconnected quick start a while ago so it's untracked
settings.quickstart = false;
settings.bridgesEnabled = Prefs.bridgesEnabled();
// tor-android-service CustomTorInstaller.java
/*
BridgesList is an overloaded field, which can cause some confusion.
The list can be:
1) a filter like obfs4, meek, or snowflake OR
2) it can be a custom bridge
For (1), we just pass back all bridges, the filter will occur
elsewhere in the library.
For (2) we return the bridge list as a raw stream.
If length is greater than 9, then we know this is a custom bridge
*/
String userDefinedBridgeList = Prefs.getBridgesList();
boolean userDefinedBridge = userDefinedBridgeList.length() > 9;
// Terrible hack. Must keep in sync with topl::addBridgesFromResources.
if (!userDefinedBridge) {
settings.bridgesSource = TorSettings.BridgeSource.BuiltIn;
switch (userDefinedBridgeList) {
case "obfs4":
case "snowflake":
settings.bridgesBuiltinType = TorSettings.BridgeBuiltinType.fromString(userDefinedBridgeList);
break;
case "meek":
settings.bridgesBuiltinType = TorSettings.BridgeBuiltinType.MeekAzure;
break;
default:
settings.bridgesSource = TorSettings.BridgeSource.Invalid;
break;
}
} else {
settings.bridgesSource = TorSettings.BridgeSource.UserProvided; // user provided
settings.bridgeBridgeStrings = userDefinedBridgeList.split("\r\n");
}
// Tor Browser Android doesn't take proxy and firewall settings
settings.proxyEnabled = false;
settings.firewallEnabled = false;
settings.firewallAllowedPorts = new int[0];
return settings;
}
}
......@@ -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;
}
}
......
/* 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",
TorConnectTopics: "resource://gre/modules/TorConnect.sys.mjs",
TorSettingsTopics: "resource://gre/modules/TorSettings.sys.mjs",
TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
TorProviderTopics: "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 EmittedEvents = Object.freeze({
settingsReady: "GeckoView:Tor:SettingsReady",
settingsChanged: "GeckoView:Tor:SettingsChanged",
connectStateChanged: "GeckoView:Tor:ConnectStateChanged",
connectError: "GeckoView:Tor:ConnectError",
bootstrapProgress: "GeckoView:Tor:BootstrapProgress",
bootstrapComplete: "GeckoView:Tor:BootstrapComplete",
torLogs: "GeckoView:Tor:Logs",
});
const ListenedEvents = Object.freeze({
settingsGet: "GeckoView:Tor:SettingsGet",
// The data is passed directly to TorSettings.
settingsSet: "GeckoView:Tor:SettingsSet",
settingsApply: "GeckoView:Tor:SettingsApply",
settingsSave: "GeckoView:Tor:SettingsSave",
bootstrapBegin: "GeckoView:Tor:BootstrapBegin",
// Optionally takes a countryCode, as data.countryCode.
bootstrapBeginAuto: "GeckoView:Tor:BootstrapBeginAuto",
bootstrapCancel: "GeckoView:Tor:BootstrapCancel",
bootstrapGetState: "GeckoView:Tor:BootstrapGetState",
});
class TorAndroidIntegrationImpl {
#initialized = false;
init() {
lazy.EventDispatcher.instance.registerListener(
this,
Object.values(ListenedEvents)
);
this.#bootstrapMethodReset();
Services.prefs.addObserver(Prefs.useNewBootstrap, this);
Services.obs.addObserver(this, lazy.TorProviderTopics.TorLog);
for (const topic in lazy.TorConnectTopics) {
Services.obs.addObserver(this, lazy.TorConnectTopics[topic]);
}
for (const topic in lazy.TorSettingsTopics) {
Services.obs.addObserver(this, lazy.TorSettingsTopics[topic]);
}
}
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;
case lazy.TorConnectTopics.StateChange:
lazy.EventDispatcher.instance.sendRequest({
type: EmittedEvents.connectStateChanged,
state: subj.wrappedJSObject.state ?? "",
});
break;
case lazy.TorConnectTopics.BootstrapProgress:
lazy.EventDispatcher.instance.sendRequest({
type: EmittedEvents.bootstrapProgress,
progress: subj.wrappedJSObject.progress ?? 0,
hasWarnings: subj.wrappedJSObject.hasWarnings ?? false,
});
break;
case lazy.TorConnectTopics.BootstrapComplete:
lazy.EventDispatcher.instance.sendRequest({
type: EmittedEvents.bootstrapComplete,
});
break;
case lazy.TorConnectTopics.Error:
lazy.EventDispatcher.instance.sendRequest({
type: EmittedEvents.connectError,
code: subj.wrappedJSObject.code ?? "",
message: subj.wrappedJSObject.message ?? "",
phase: subj.wrappedJSObject.cause?.phase ?? "",
reason: subj.wrappedJSObject.cause?.reason ?? "",
});
break;
case lazy.TorProviderTopics.TorLog:
lazy.EventDispatcher.instance.sendRequest({
type: EmittedEvents.torLogs,
logType: subj.wrappedJSObject.type ?? "",
message: subj.wrappedJSObject.msg ?? "",
});
break;
case lazy.TorSettingsTopics.Ready:
lazy.EventDispatcher.instance.sendRequest({
type: EmittedEvents.settingsReady,
settings: lazy.TorSettings.getSettings(),
});
break;
case lazy.TorSettingsTopics.SettingsChanged:
// For Android we push also the settings object to avoid a round trip on
// the event dispatcher.
lazy.EventDispatcher.instance.sendRequest({
type: EmittedEvents.settingsChanged,
changes: subj.wrappedJSObject.changes ?? [],
settings: lazy.TorSettings.getSettings(),
});
break;
}
}
async onEvent(event, data, callback) {
logger.debug(`Received event ${event}`, data);
try {
switch (event) {
case ListenedEvents.settingsGet:
callback?.onSuccess(lazy.TorSettings.getSettings());
return;
case ListenedEvents.settingsSet:
// This does not throw, so we do not have any way to report the error!
lazy.TorSettings.setSettings(data.settings);
if (data.save) {
lazy.TorSettings.saveToPrefs();
}
if (data.apply) {
await lazy.TorSettings.applySettings();
}
break;
case ListenedEvents.settingsApply:
await lazy.TorSettings.applySettings();
break;
case ListenedEvents.settingsSave:
await lazy.TorSettings.saveToPrefs();
break;
case ListenedEvents.bootstrapBegin:
lazy.TorConnect.beginBootstrap();
break;
case ListenedEvents.bootstrapBeginAuto:
lazy.TorConnect.beginAutoBootstrap(data.countryCode);
break;
case ListenedEvents.bootstrapCancel:
lazy.TorConnect.cancelBootstrap();
break;
case ListenedEvents.bootstrapGetState:
callback?.onSuccess(lazy.TorConnect.state);
return;
}
callback?.onSuccess();
} catch (e) {
logger.error(`Error while handling event ${event}`, e);
callback?.onError(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();
......@@ -214,6 +214,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",
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment