Dan Ballard pushed to branch firefox-android-115.2.1-13.5-1 at The Tor Project / Applications / firefox-android
Commits: 6d0a6453 by Dan Ballard at 2024-01-25T16:15:45-08:00 fixup! Add Tor integration and UI
Bug 42252: Make TorController and interface and add a Geckoview implementation
- - - - -
4 changed files:
- fenix/app/src/main/java/org/mozilla/fenix/components/Components.kt - fenix/app/src/main/java/org/mozilla/fenix/tor/TorController.kt - + fenix/app/src/main/java/org/mozilla/fenix/tor/TorControllerGV.kt - + fenix/app/src/main/java/org/mozilla/fenix/tor/TorControllerTAS.kt
Changes:
===================================== fenix/app/src/main/java/org/mozilla/fenix/components/Components.kt ===================================== @@ -43,7 +43,8 @@ import org.mozilla.fenix.perf.StartupActivityLog import org.mozilla.fenix.perf.StartupStateProvider import org.mozilla.fenix.perf.StrictModeManager import org.mozilla.fenix.perf.lazyMonitored -import org.mozilla.fenix.tor.TorController +import org.mozilla.fenix.tor.TorControllerGV +import org.mozilla.fenix.tor.TorControllerTAS import org.mozilla.fenix.utils.ClipboardHandler import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.wifi.WifiConnectionMonitor @@ -201,7 +202,7 @@ class Components(private val context: Context) { ), ) } - val torController by lazyMonitored { TorController(context) } + val torController by lazyMonitored { if (settings.useNewBootstrap) TorControllerGV(context) else TorControllerTAS(context) } }
/**
===================================== fenix/app/src/main/java/org/mozilla/fenix/tor/TorController.kt ===================================== @@ -4,22 +4,7 @@
package org.mozilla.fenix.tor
-import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter import androidx.lifecycle.LifecycleCoroutineScope -import androidx.localbroadcastmanager.content.LocalBroadcastManager - -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.launch -import kotlinx.coroutines.withTimeoutOrNull - -import org.mozilla.fenix.BuildConfig - -import org.torproject.android.service.TorService -import org.torproject.android.service.TorServiceConstants -import org.torproject.android.service.util.Prefs
interface TorEvents { fun onTorConnecting() @@ -28,347 +13,59 @@ interface TorEvents { fun onTorStopped() }
-private enum class TorStatus { - OFF, - STARTING, - ON, - STOPPING, - UNKNOWN; +internal enum class TorStatus(val status: String) { + OFF("OFF"), + STARTING("STARTING"), + ON("ON"), + STOPPING("STOPPING"), + UNKNOWN("UNKNOWN");
- fun getStateFromString(status: String): TorStatus { - return when (status) { - TorServiceConstants.STATUS_ON -> ON - TorServiceConstants.STATUS_STARTING -> STARTING - TorServiceConstants.STATUS_STOPPING -> STOPPING - TorServiceConstants.STATUS_OFF -> OFF - else -> UNKNOWN + companion object { + fun fromString(status: String): TorStatus { + return when (status) { + "ON" -> ON + "STARTING" -> STARTING + "STOPPING" -> STOPPING + "OFF" -> OFF + else -> UNKNOWN + } } }
fun isOff() = this == OFF fun isOn() = this == ON fun isStarting() = this == STARTING - fun isStarted() = ((this == TorStatus.STARTING) || (this == TorStatus.ON)) + fun isStarted() = ((this == STARTING) || (this == ON)) fun isStopping() = this == STOPPING fun isUnknown() = this == UNKNOWN }
-@SuppressWarnings("TooManyFunctions") -class TorController( - private val context: Context -) : TorEvents { - - private val lbm: LocalBroadcastManager = LocalBroadcastManager.getInstance(context) - private val entries = mutableListOf<Pair<String?, String?>>() - val logEntries get() = entries - - private var torListeners = mutableListOf<TorEvents>() - - private var pendingRegisterChangeList = mutableListOf<Pair<TorEvents, Boolean>>() - private var lockTorListenersMutation = false - - private var lastKnownStatus = TorStatus.OFF - private var wasTorBootstrapped = false - private var isTorRestarting = false - - // This may be a lie - private var isTorBootstrapped = false - get() = ((lastKnownStatus == TorStatus.ON) && wasTorBootstrapped) - - val isDebugLoggingEnabled get() = - context - .getSharedPreferences("org.torproject.android_preferences", Context.MODE_PRIVATE) - .getBoolean("pref_enable_logging", false) - - val isStarting get() = lastKnownStatus.isStarting() - val isRestarting get() = isTorRestarting - val isBootstrapped get() = isTorBootstrapped - val isConnected get() = (lastKnownStatus.isStarted() && !isTorRestarting) - +interface TorController: TorEvents { + val logEntries: MutableList<Pair<String?, String?>> + val isStarting: Boolean + val isRestarting: Boolean + val isBootstrapped: Boolean + val isConnected: Boolean var bridgesEnabled: Boolean - get() = Prefs.bridgesEnabled() - set(value) { Prefs.putBridgesEnabled(value) } - var bridgeTransport: TorBridgeTransportConfig - get() { - return TorBridgeTransportConfigUtil.getStringToBridgeTransport( - Prefs.getBridgesList() - ) - } - set(value) { - if (value == TorBridgeTransportConfig.USER_PROVIDED) { - // Don't set the pref when the value is USER_PROVIDED because - // "user_provided" is not a valid bridge or transport type. - // This call should be followed by setting userProvidedBridges. - return - } - Prefs.setBridgesList(value.transportName) - } - var userProvidedBridges: String? - get() { - val bridges = Prefs.getBridgesList() - val bridgeType = - TorBridgeTransportConfigUtil.getStringToBridgeTransport(bridges) - return when (bridgeType) { - TorBridgeTransportConfig.USER_PROVIDED -> bridges - else -> null - } - } - set(value) { - Prefs.setBridgesList(value) - } - - fun start() { - // Register receiver - lbm.registerReceiver( - persistentBroadcastReceiver, - IntentFilter(TorServiceConstants.ACTION_STATUS) - ) - lbm.registerReceiver( - persistentBroadcastReceiver, - IntentFilter(TorServiceConstants.LOCAL_ACTION_LOG) - ) - }
- fun stop() { - lbm.unregisterReceiver(persistentBroadcastReceiver) - } + fun start() + fun stop()
- private val persistentBroadcastReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - if (intent.action == null || - (intent.action != TorServiceConstants.ACTION_STATUS && - intent.action != TorServiceConstants.LOCAL_ACTION_LOG) - ) { - return - } - val action = intent.action + override fun onTorConnecting() + override fun onTorConnected() + override fun onTorStatusUpdate(entry: String?, status: String?) + override fun onTorStopped()
- val logentry: String? - val status: String? - if (action == TorServiceConstants.LOCAL_ACTION_LOG) { - logentry = intent.getExtras() - ?.getCharSequence(TorServiceConstants.LOCAL_EXTRA_LOG) as? String? - } else { - logentry = null - } - - status = intent.getExtras() - ?.getCharSequence(TorServiceConstants.EXTRA_STATUS) as? String? - - if (logentry == null && status == null) { - return - } - - onTorStatusUpdate(logentry, status) - - if (status == null) { - return - } - - val newStatus = lastKnownStatus.getStateFromString(status) - - if (newStatus.isUnknown() && wasTorBootstrapped) { - stopTor() - } - - entries.add(Pair(logentry, status)) - - if (logentry != null && logentry.contains(TorServiceConstants.TOR_CONTROL_PORT_MSG_BOOTSTRAP_DONE)) { - wasTorBootstrapped = true - onTorConnected() - } - - if (lastKnownStatus.isStopping() && newStatus.isOff()) { - if (isTorRestarting) { - initiateTorBootstrap() - } else { - onTorStopped() - } - } - - if (lastKnownStatus.isOff() && newStatus.isStarting()) { - isTorRestarting = false - } - - lastKnownStatus = newStatus - } - } - - override fun onTorConnecting() { - lockTorListenersMutation = true - torListeners.forEach { it.onTorConnecting() } - lockTorListenersMutation = false - - handlePendingRegistrationChanges() - } - - override fun onTorConnected() { - lockTorListenersMutation = true - torListeners.forEach { it.onTorConnected() } - lockTorListenersMutation = false - - handlePendingRegistrationChanges() - } + fun registerTorListener(l: TorEvents) + fun unregisterTorListener(l: TorEvents)
- override fun onTorStatusUpdate(entry: String?, status: String?) { - lockTorListenersMutation = true - torListeners.forEach { it.onTorStatusUpdate(entry, status) } - lockTorListenersMutation = false - - handlePendingRegistrationChanges() - } - - override fun onTorStopped() { - lockTorListenersMutation = true - torListeners.forEach { it.onTorStopped() } - lockTorListenersMutation = false - - handlePendingRegistrationChanges() - } - - fun registerTorListener(l: TorEvents) { - if (torListeners.contains(l)) { - return - } - - if (lockTorListenersMutation) { - pendingRegisterChangeList.add(Pair(l, true)) - } else { - torListeners.add(l) - } - } - - fun unregisterTorListener(l: TorEvents) { - if (!torListeners.contains(l)) { - return - } - - if (lockTorListenersMutation) { - pendingRegisterChangeList.add(Pair(l, false)) - } else { - torListeners.remove(l) - } - } - - private fun handlePendingRegistrationChanges() { - pendingRegisterChangeList.forEach { - if (it.second) { - registerTorListener(it.first) - } else { - unregisterTorListener(it.first) - } - } - - pendingRegisterChangeList.clear() - } - - /** - * Receive the current Tor status. - * - * Send a request for the current status and receive the response. - * Returns true if Tor is running, false otherwise. - * - */ - private suspend fun checkTorIsStarted(): Boolean { - val channel = Channel<Boolean>() - - // Register receiver - val lbm: LocalBroadcastManager = LocalBroadcastManager.getInstance(context) - val localBroadcastReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - val action = intent.action ?: return - // We only want ACTION_STATUS messages - if (action != TorServiceConstants.ACTION_STATUS) { - return - } - // The current status has the EXTRA_STATUS key - val currentStatus = - intent.getStringExtra(TorServiceConstants.EXTRA_STATUS) - channel.trySend(currentStatus === TorServiceConstants.STATUS_ON) - } - } - lbm.registerReceiver( - localBroadcastReceiver, - IntentFilter(TorServiceConstants.ACTION_STATUS) - ) - - // Request service status - sendServiceAction(TorServiceConstants.ACTION_STATUS) - - // Wait for response and unregister receiver - var torIsStarted = false - withTimeoutOrNull(torServiceResponseTimeout) { - torIsStarted = channel.receive() - } - lbm.unregisterReceiver(localBroadcastReceiver) - return torIsStarted - } - - fun initiateTorBootstrap(lifecycleScope: LifecycleCoroutineScope? = null, withDebugLogging: Boolean = false) { - if (BuildConfig.DISABLE_TOR) { - return - } - - context.getSharedPreferences("org.torproject.android_preferences", Context.MODE_PRIVATE) - .edit().putBoolean("pref_enable_logging", withDebugLogging).apply() - - if (lifecycleScope == null) { - sendServiceAction(TorServiceConstants.ACTION_START) - } else { - lifecycleScope.launch { - val torNeedsStart = !checkTorIsStarted() - if (torNeedsStart) { - sendServiceAction(TorServiceConstants.ACTION_START) - } - } - } - } - - fun stopTor() { - if (BuildConfig.DISABLE_TOR) { - return - } - - val torService = Intent(context, TorService::class.java) - context.stopService(torService) - } - - fun setTorStopped() { - lastKnownStatus = TorStatus.OFF - onTorStopped() - } - - fun restartTor() { - // tor-android-service doesn't dynamically update the torrc file, - // and it doesn't use SETCONF, so we completely restart the service. - // However, don't restart if we aren't started and we weren't - // previously started. - if (!lastKnownStatus.isStarted() && !wasTorBootstrapped) { - return - } + fun initiateTorBootstrap(lifecycleScope: LifecycleCoroutineScope? = null, withDebugLogging: Boolean = false) + fun stopTor() + fun setTorStopped() + fun restartTor() +}
- if (!lastKnownStatus.isStarted() && wasTorBootstrapped) { - // If we aren't started, but we were previously bootstrapped, - // then we handle a "restart" request as a "start" restart - initiateTorBootstrap() - } else { - // |isTorRestarting| tracks the state of restart. When we receive an |OFF| state - // from TorService in persistentBroadcastReceiver::onReceive we restart the Tor - // service. - isTorRestarting = true - stopTor() - } - }
- private fun sendServiceAction(action: String) { - val torServiceStatus = Intent(context, TorService::class.java) - torServiceStatus.action = action - context.startService(torServiceStatus) - }
- companion object { - const val torServiceResponseTimeout = 5000L - } -}
===================================== fenix/app/src/main/java/org/mozilla/fenix/tor/TorControllerGV.kt ===================================== @@ -0,0 +1,289 @@ +package org.mozilla.fenix.tor + + +import android.content.Context +import android.util.Log +import androidx.lifecycle.LifecycleCoroutineScope +import mozilla.components.browser.engine.gecko.GeckoEngine +import org.mozilla.fenix.ext.components +import org.mozilla.geckoview.TorIntegrationAndroid +import org.mozilla.geckoview.TorIntegrationAndroid.BootstrapStateChangeListener +import org.mozilla.geckoview.TorSettings +import org.mozilla.geckoview.TorSettings.BridgeBuiltinType +import org.mozilla.geckoview.TorSettings.BridgeSource + +// Enum matching TorConnectState from TorConnect.sys.mjs that we get from onBootstrapStateChange +internal enum class TorConnectState(val state: String) { + Initial("Initial"), + Configuring("Configuring"), + AutoBootstrapping("AutoBootstrapping"), + Bootstrapping("Bootstrapping"), + Error("Error"), + Bootstrapped("Bootstrapped"), + Disabled("Disabled"); + + fun isStarting() = this == Bootstrapping || this == AutoBootstrapping + fun isError() = this == Error + + fun isStarted() = this == Bootstrapped + + fun isOff() = this == Initial || this == Configuring || this == Disabled || this == Error + + + // Convert to TorStatus that firefox-android uses based on tor-android-service + fun toTorStatus(): TorStatus { + return when (this) { + Initial -> TorStatus.OFF + Configuring -> TorStatus.OFF + AutoBootstrapping -> TorStatus.STARTING + Bootstrapping -> TorStatus.STARTING + Error -> TorStatus.UNKNOWN + Bootstrapped -> TorStatus.ON + Disabled -> TorStatus.OFF + } + } +} + +class TorControllerGV( + private val context: Context, +) : TorController, TorEvents, BootstrapStateChangeListener { + + private val TAG = "TorControllerGV" + + private var torListeners = mutableListOf<TorEvents>() + + private var lastKnownStatus = TorConnectState.Initial + private var wasTorBootstrapped = false + private var isTorRestarting = false + + private var isTorBootstrapped = false + get() = ((lastKnownStatus.isStarted()) && wasTorBootstrapped) + + private val entries = mutableListOf<Pair<String?, String?>>() + override val logEntries get() = entries + override val isStarting get() = lastKnownStatus.isStarting() + override val isRestarting get() = isTorRestarting + override val isBootstrapped get() = isTorBootstrapped + override val isConnected get() = (lastKnownStatus.isStarted() && !isTorRestarting) + + private fun getTorIntegration(): TorIntegrationAndroid { + return (context.components.core.engine as GeckoEngine).getTorIntegrationController() + } + + private fun getTorSettings(): TorSettings? { + return getTorIntegration().getSettings() + } + + override var bridgesEnabled: Boolean + get() { + return getTorSettings()?.bridgesEnabled ?: false + } + set(value) { + getTorSettings()?.let { + it.bridgesEnabled = value + getTorIntegration().setSettings(it, true, true) + } + } + + + override var bridgeTransport: TorBridgeTransportConfig + get() { + return when (getTorSettings()?.bridgesSource) { + BridgeSource.BuiltIn -> { + when (getTorSettings()?.bridgesBuiltinType) { + BridgeBuiltinType.Obfs4 -> TorBridgeTransportConfig.BUILTIN_OBFS4 + BridgeBuiltinType.MeekAzure -> TorBridgeTransportConfig.BUILTIN_MEEK_AZURE + BridgeBuiltinType.Snowflake -> TorBridgeTransportConfig.BUILTIN_SNOWFLAKE + else -> TorBridgeTransportConfig.USER_PROVIDED + } + + } + + BridgeSource.UserProvided -> TorBridgeTransportConfig.USER_PROVIDED + else -> TorBridgeTransportConfig.USER_PROVIDED + } + } + set(value) { + getTorSettings()?.let { + if (value == TorBridgeTransportConfig.USER_PROVIDED) { + it.bridgesSource = BridgeSource.BuiltIn + } else { + val bbt: BridgeBuiltinType = when (value) { + TorBridgeTransportConfig.BUILTIN_OBFS4 -> BridgeBuiltinType.Obfs4 + TorBridgeTransportConfig.BUILTIN_MEEK_AZURE -> BridgeBuiltinType.MeekAzure + TorBridgeTransportConfig.BUILTIN_SNOWFLAKE -> BridgeBuiltinType.Snowflake + else -> BridgeBuiltinType.Invalid + } + it.bridgesBuiltinType = bbt + } + getTorIntegration().setSettings(it, true, true) + } + } + + + override var userProvidedBridges: String? + get() { + return getTorSettings()?.bridgeBridgeStrings?.joinToString("\r\n") + } + set(value) { + getTorSettings()?.let { + it.bridgeBridgeStrings = value?.split("\r\n")?.toTypedArray() ?: arrayOf<String>() + getTorIntegration().setSettings(it, true, true) + } + } + + override fun start() { + getTorIntegration().registerBootstrapStateChangeListener(this) + } + + override fun stop() { + getTorIntegration().unregisterBootstrapStateChangeListener(this) + } + + // TorEvents + override fun onTorConnecting() { + synchronized(torListeners) { + torListeners.forEach { it.onTorConnecting() } + } + } + + // TorEvents + override fun onTorConnected() { + synchronized(torListeners) { + torListeners.forEach { it.onTorConnected() } + } + } + + // TorEvents + override fun onTorStatusUpdate(entry: String?, status: String?) { + synchronized(torListeners) { + torListeners.forEach { it.onTorStatusUpdate(entry, status) } + } + } + + // TorEvents + override fun onTorStopped() { + synchronized(torListeners) { + torListeners.forEach { it.onTorStopped() } + } + } + + override fun registerTorListener(l: TorEvents) { + synchronized(torListeners) { + if (torListeners.contains(l)) { + return + } + torListeners.add(l) + } + } + + override fun unregisterTorListener(l: TorEvents) { + synchronized(torListeners) { + if (!torListeners.contains(l)) { + return + } + torListeners.remove(l) + } + } + + override fun initiateTorBootstrap( + lifecycleScope: LifecycleCoroutineScope?, + withDebugLogging: Boolean, + ) { + getTorIntegration().beginBootstrap() + } + + override fun stopTor() { + getTorIntegration().cancelBootstrap() + } + + override fun setTorStopped() { + lastKnownStatus = TorConnectState.Disabled + onTorStopped() + } + + override fun restartTor() { + if (!lastKnownStatus.isStarted() && wasTorBootstrapped) { + // If we aren't started, but we were previously bootstrapped, + // then we handle a "restart" request as a "start" restart + initiateTorBootstrap() + } else { + // |isTorRestarting| tracks the state of restart. When we receive an |OFF| state + // from TorService in persistentBroadcastReceiver::onReceive we restart the Tor + // service. + isTorRestarting = true + stopTor() + } + } + + // TorEventsBootstrapStateChangeListener -> (lastKnowStatus, TorEvents) + // Handle events from GeckoView TorAndroidIntegration and map to TorEvents based events + // and state for firefox-android (designed for tor-android-service) + // fun onTorConnecting() + // fun onTorConnected() + // fun onTorStatusUpdate(entry: String?, status: String?) + // fun onTorStopped() + + // TorEventsBootstrapStateChangeListener + override fun onBootstrapStateChange(newStateVal: String?) { + Log.d(TAG, "onBootstrapStateChange($newStateVal)") + val newState: TorConnectState = TorConnectState.valueOf(newStateVal ?: "Error") + + if (newState.isError() && wasTorBootstrapped) { + stopTor() + } + + if (newState.isStarted()) { + wasTorBootstrapped = true + onTorConnected() + } + + if (wasTorBootstrapped && newState == TorConnectState.Configuring) { + wasTorBootstrapped = false + if (isTorRestarting) { + initiateTorBootstrap() + } else { + onTorStopped() + } + } + + if (lastKnownStatus.isOff() && newState.isStarting()) { + isTorRestarting = false + } + + lastKnownStatus = newState + + } + + // TorEventsBootstrapStateChangeListener + override fun onBootstrapProgress(progress: Double, status: String?, hasWarnings: Boolean) { + Log.d(TAG, "onBootstrapProgress($progress, $status, $hasWarnings)") + if (progress == 100.0) { + lastKnownStatus = TorConnectState.Bootstrapped + wasTorBootstrapped = true + onTorConnected() + } else { + lastKnownStatus = TorConnectState.Bootstrapping + onTorConnecting() + + } + entries.add(Pair(status, lastKnownStatus.toTorStatus().status)) + onTorStatusUpdate(status, lastKnownStatus.toTorStatus().status) + } + + // TorEventsBootstrapStateChangeListener + override fun onBootstrapComplete() { + lastKnownStatus = TorConnectState.Bootstrapped + this.onTorConnected() + } + + // TorEventsBootstrapStateChangeListener + override fun onBootstrapError(message: String?, details: String?) { + lastKnownStatus = TorConnectState.Error + } + + // TorEventsBootstrapStateChangeListener + override fun onSettingsRequested() { + // noop + } +}
===================================== fenix/app/src/main/java/org/mozilla/fenix/tor/TorControllerTAS.kt ===================================== @@ -0,0 +1,332 @@ +package org.mozilla.fenix.tor + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import androidx.lifecycle.LifecycleCoroutineScope +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeoutOrNull +import org.mozilla.fenix.BuildConfig +import org.torproject.android.service.TorService +import org.torproject.android.service.TorServiceConstants +import org.torproject.android.service.util.Prefs + +@SuppressWarnings("TooManyFunctions") +class TorControllerTAS (private val context: Context): TorController { + private val lbm: LocalBroadcastManager = LocalBroadcastManager.getInstance(context) + private val entries = mutableListOf<Pair<String?, String?>>() + override val logEntries get() = entries + + private var torListeners = mutableListOf<TorEvents>() + + private var pendingRegisterChangeList = mutableListOf<Pair<TorEvents, Boolean>>() + private var lockTorListenersMutation = false + + private var lastKnownStatus = TorStatus.OFF + private var wasTorBootstrapped = false + private var isTorRestarting = false + + // This may be a lie + private var isTorBootstrapped = false + get() = ((lastKnownStatus == TorStatus.ON) && wasTorBootstrapped) + + val isDebugLoggingEnabled get() = + context + .getSharedPreferences("org.torproject.android_preferences", Context.MODE_PRIVATE) + .getBoolean("pref_enable_logging", false) + + override val isStarting get() = lastKnownStatus.isStarting() + override val isRestarting get() = isTorRestarting + override val isBootstrapped get() = isTorBootstrapped + override val isConnected get() = (lastKnownStatus.isStarted() && !isTorRestarting) + + override var bridgesEnabled: Boolean + get() = Prefs.bridgesEnabled() + set(value) { Prefs.putBridgesEnabled(value) } + + override var bridgeTransport: TorBridgeTransportConfig + get() { + return TorBridgeTransportConfigUtil.getStringToBridgeTransport( + Prefs.getBridgesList() + ) + } + set(value) { + if (value == TorBridgeTransportConfig.USER_PROVIDED) { + // Don't set the pref when the value is USER_PROVIDED because + // "user_provided" is not a valid bridge or transport type. + // This call should be followed by setting userProvidedBridges. + return + } + Prefs.setBridgesList(value.transportName) + } + + override var userProvidedBridges: String? + get() { + val bridges = Prefs.getBridgesList() + val bridgeType = + TorBridgeTransportConfigUtil.getStringToBridgeTransport(bridges) + return when (bridgeType) { + TorBridgeTransportConfig.USER_PROVIDED -> bridges + else -> null + } + } + set(value) { + Prefs.setBridgesList(value) + } + + override fun start() { + // Register receiver + lbm.registerReceiver( + persistentBroadcastReceiver, + IntentFilter(TorServiceConstants.ACTION_STATUS) + ) + lbm.registerReceiver( + persistentBroadcastReceiver, + IntentFilter(TorServiceConstants.LOCAL_ACTION_LOG) + ) + } + + override fun stop() { + lbm.unregisterReceiver(persistentBroadcastReceiver) + } + + private val persistentBroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == null || + (intent.action != TorServiceConstants.ACTION_STATUS && + intent.action != TorServiceConstants.LOCAL_ACTION_LOG) + ) { + return + } + val action = intent.action + + val logentry: String? + val status: String? + if (action == TorServiceConstants.LOCAL_ACTION_LOG) { + logentry = intent.getExtras() + ?.getCharSequence(TorServiceConstants.LOCAL_EXTRA_LOG) as? String? + } else { + logentry = null + } + + status = intent.getExtras() + ?.getCharSequence(TorServiceConstants.EXTRA_STATUS) as? String? + + if (logentry == null && status == null) { + return + } + + onTorStatusUpdate(logentry, status) + + if (status == null) { + return + } + + val newStatus = TorStatus.fromString(status) + + if (newStatus.isUnknown() && wasTorBootstrapped) { + stopTor() + } + + entries.add(Pair(logentry, status)) + + if (logentry != null && logentry.contains(TorServiceConstants.TOR_CONTROL_PORT_MSG_BOOTSTRAP_DONE)) { + wasTorBootstrapped = true + onTorConnected() + } + + if (lastKnownStatus.isStopping() && newStatus.isOff()) { + if (isTorRestarting) { + initiateTorBootstrap() + } else { + onTorStopped() + } + } + + if (lastKnownStatus.isOff() && newStatus.isStarting()) { + isTorRestarting = false + } + + lastKnownStatus = newStatus + } + } + + override fun onTorConnecting() { + lockTorListenersMutation = true + torListeners.forEach { it.onTorConnecting() } + lockTorListenersMutation = false + + handlePendingRegistrationChanges() + } + + override fun onTorConnected() { + lockTorListenersMutation = true + torListeners.forEach { it.onTorConnected() } + lockTorListenersMutation = false + + handlePendingRegistrationChanges() + } + + override fun onTorStatusUpdate(entry: String?, status: String?) { + lockTorListenersMutation = true + torListeners.forEach { it.onTorStatusUpdate(entry, status) } + lockTorListenersMutation = false + + handlePendingRegistrationChanges() + } + + override fun onTorStopped() { + lockTorListenersMutation = true + torListeners.forEach { it.onTorStopped() } + lockTorListenersMutation = false + + handlePendingRegistrationChanges() + } + + override fun registerTorListener(l: TorEvents) { + if (torListeners.contains(l)) { + return + } + + if (lockTorListenersMutation) { + pendingRegisterChangeList.add(Pair(l, true)) + } else { + torListeners.add(l) + } + } + + override fun unregisterTorListener(l: TorEvents) { + if (!torListeners.contains(l)) { + return + } + + if (lockTorListenersMutation) { + pendingRegisterChangeList.add(Pair(l, false)) + } else { + torListeners.remove(l) + } + } + + private fun handlePendingRegistrationChanges() { + pendingRegisterChangeList.forEach { + if (it.second) { + registerTorListener(it.first) + } else { + unregisterTorListener(it.first) + } + } + + pendingRegisterChangeList.clear() + } + + /** + * Receive the current Tor status. + * + * Send a request for the current status and receive the response. + * Returns true if Tor is running, false otherwise. + * + */ + private suspend fun checkTorIsStarted(): Boolean { + val channel = Channel<Boolean>() + + // Register receiver + val lbm: LocalBroadcastManager = LocalBroadcastManager.getInstance(context) + val localBroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val action = intent.action ?: return + // We only want ACTION_STATUS messages + if (action != TorServiceConstants.ACTION_STATUS) { + return + } + // The current status has the EXTRA_STATUS key + val currentStatus = + intent.getStringExtra(TorServiceConstants.EXTRA_STATUS) + channel.trySend(currentStatus === TorServiceConstants.STATUS_ON) + } + } + lbm.registerReceiver( + localBroadcastReceiver, + IntentFilter(TorServiceConstants.ACTION_STATUS) + ) + + // Request service status + sendServiceAction(TorServiceConstants.ACTION_STATUS) + + // Wait for response and unregister receiver + var torIsStarted = false + withTimeoutOrNull(torServiceResponseTimeout) { + torIsStarted = channel.receive() + } + lbm.unregisterReceiver(localBroadcastReceiver) + return torIsStarted + } + + override fun initiateTorBootstrap(lifecycleScope: LifecycleCoroutineScope?, withDebugLogging: Boolean) { + if (BuildConfig.DISABLE_TOR) { + return + } + + context.getSharedPreferences("org.torproject.android_preferences", Context.MODE_PRIVATE) + .edit().putBoolean("pref_enable_logging", withDebugLogging).apply() + + if (lifecycleScope == null) { + sendServiceAction(TorServiceConstants.ACTION_START) + } else { + lifecycleScope.launch { + val torNeedsStart = !checkTorIsStarted() + if (torNeedsStart) { + sendServiceAction(TorServiceConstants.ACTION_START) + } + } + } + } + + override fun stopTor() { + if (BuildConfig.DISABLE_TOR) { + return + } + + val torService = Intent(context, TorService::class.java) + context.stopService(torService) + } + + override fun setTorStopped() { + lastKnownStatus = TorStatus.OFF + onTorStopped() + } + + override fun restartTor() { + // tor-android-service doesn't dynamically update the torrc file, + // and it doesn't use SETCONF, so we completely restart the service. + // However, don't restart if we aren't started and we weren't + // previously started. + if (!lastKnownStatus.isStarted() && !wasTorBootstrapped) { + return + } + + if (!lastKnownStatus.isStarted() && wasTorBootstrapped) { + // If we aren't started, but we were previously bootstrapped, + // then we handle a "restart" request as a "start" restart + initiateTorBootstrap() + } else { + // |isTorRestarting| tracks the state of restart. When we receive an |OFF| state + // from TorService in persistentBroadcastReceiver::onReceive we restart the Tor + // service. + isTorRestarting = true + stopTor() + } + } + + private fun sendServiceAction(action: String) { + val torServiceStatus = Intent(context, TorService::class.java) + torServiceStatus.action = action + context.startService(torServiceStatus) + } + + companion object { + const val torServiceResponseTimeout = 5000L + } +}
View it on GitLab: https://gitlab.torproject.org/tpo/applications/firefox-android/-/commit/6d0a...