Commit b4d323b9 authored by Matthew Finkel's avatar Matthew Finkel
Browse files

Bug 40028: Implement Tor Service controller

parent e4626405
Loading
Loading
Loading
Loading
+0 −65
Original line number Diff line number Diff line
@@ -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.
+374 −0
Original line number Diff line number Diff line
/* 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
    }
}