GitLab is used only for code review, issue tracking and project management. Canonical locations for source code are still https://gitweb.torproject.org/ https://git.torproject.org/ and git-rw.torproject.org.

Commit b4d323b9 authored by Matthew Finkel's avatar Matthew Finkel

Bug 40028: Implement Tor Service controller

parent e4626405
......@@ -40,8 +40,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.coroutines.channels.Channel
import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
import mozilla.components.browser.state.state.SessionState
......@@ -275,63 +273,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
components.appStartupTelemetry.onHomeActivityOnRestart(rootContainer)
}
/**
* 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(this@HomeActivity)
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.offer(currentStatus === TorServiceConstants.STATUS_ON)
}
}
lbm.registerReceiver(
localBroadcastReceiver,
IntentFilter(TorServiceConstants.ACTION_STATUS)
)
// Request service status
val torServiceStatus = Intent(this@HomeActivity, TorService::class.java)
torServiceStatus.action = TorServiceConstants.ACTION_STATUS
startService(torServiceStatus)
// Wait for response and unregister receiver
var torIsStarted = false
withTimeoutOrNull(timeout) {
torIsStarted = channel.receive()
}
lbm.unregisterReceiver(localBroadcastReceiver)
return torIsStarted
}
@CallSuper
override fun onResume() {
if (!BuildConfig.DISABLE_TOR) {
lifecycleScope.launch {
val torNeedsStart = !checkTorIsStarted()
if (torNeedsStart) {
val torServiceStatus = Intent(this@HomeActivity, TorService::class.java)
torServiceStatus.action = TorServiceConstants.ACTION_START
startService(torServiceStatus)
}
}
}
super.onResume()
// Diagnostic breadcrumb for "Display already aquired" crash:
......@@ -437,13 +380,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
)
privateNotificationObserver?.stop()
if (BuildConfig.DISABLE_TOR) {
return
}
val torService = Intent(this, TorService::class.java)
stopService(torService)
}
override fun onConfigurationChanged(newConfig: Configuration) {
......@@ -933,7 +869,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
const val EXTRA_DELETE_PRIVATE_TABS = "notification_delete_and_open"
const val EXTRA_OPENED_FROM_NOTIFICATION = "notification_open"
const val START_IN_RECENTS_SCREEN = "start_in_recents_screen"
const val timeout = 5000L
// PWA must have been used within last 30 days to be considered "recently used" for the
// telemetry purposes.
......
/* 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.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()
fun onTorConnected()
fun onTorStatusUpdate(entry: String?, status: String?)
fun onTorStopped()
}
private enum class TorStatus {
OFF,
STARTING,
ON,
STOPPING,
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
}
}
fun isOff() = this == OFF
fun isOn() = this == ON
fun isStarting() = this == STARTING
fun isStarted() = ((this == TorStatus.STARTING) || (this == TorStatus.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)
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)
}
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 = 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()
}
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.offer(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
}
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
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment