- Tor Cleanup:

- Only starts Tor when the "Internal" option is selected on the privacy settings
- Adds a mutex to make sure tor only starts once.
- Fixes bug when setting port before the service is running
- Moves tor start and stop to Compose actions, instead of activity
- Adds a 5 second delay before disconnecting Tor on the App's onPause
This commit is contained in:
Vitor Pamplona
2025-03-31 11:09:04 -04:00
parent 31e961c64f
commit 396831456c
7 changed files with 116 additions and 30 deletions
@@ -103,10 +103,21 @@ class ServiceManager(
if (myAccount != null) {
when (myAccount.settings.torSettings.torType.value) {
TorType.INTERNAL -> {
Log.d("TorManager", "Relays Service Connected ${TorManager.socksPort()}")
HttpClientManager.setDefaultProxyOnPort(TorManager.socksPort())
// Tor's lib will automatically set this port.
if (TorManager.isSocksReady()) {
HttpClientManager.setDefaultProxyOnPort(TorManager.socksPort())
} else {
HttpClientManager.setProxyNotReady()
}
}
TorType.EXTERNAL -> {
val port = myAccount.settings.torSettings.externalSocksPort.value
if (port > 0) {
HttpClientManager.setDefaultProxyOnPort(port)
} else {
HttpClientManager.setProxyNotReady()
}
}
TorType.EXTERNAL -> HttpClientManager.setDefaultProxyOnPort(myAccount.settings.torSettings.externalSocksPort.value)
else -> HttpClientManager.setDefaultProxy(null)
}
@@ -36,7 +36,6 @@ import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.location.LocationState
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
import com.vitorpamplona.amethyst.service.uploads.FileHeader
import com.vitorpamplona.amethyst.tryAndWait
import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
@@ -3648,12 +3647,6 @@ class Account(
fun markDonatedInThisVersion() = settings.markDonatedInThisVersion(BuildConfig.VERSION_NAME)
fun httpClientForCoil() = HttpClientManager.getHttpClient(shouldUseTorForImageDownload())
fun httpClientForRelay(dirtyUrl: String) = HttpClientManager.getHttpClient(shouldUseTorForDirty(dirtyUrl))
fun httpClientForPreviewUrl(url: String) = HttpClientManager.getHttpClient(shouldUseTorForPreviewUrl(url))
fun shouldUseTorForImageDownload() =
when (settings.torSettings.torType.value) {
TorType.OFF -> false
@@ -20,6 +20,7 @@
*/
package com.vitorpamplona.amethyst.service.okhttp
import android.R.attr.port
import android.util.Log
import com.vitorpamplona.quartz.nip17Dm.files.encryption.NostrCipher
import okhttp3.OkHttpClient
@@ -35,6 +36,8 @@ object HttpClientManager {
.followSslRedirects(true)
.build()
val DEFAULT_TOR_PROXY = Proxy(Proxy.Type.SOCKS, InetSocketAddress("127.0.0.1", 9050))
val DEFAULT_TIMEOUT_ON_WIFI: Duration = Duration.ofSeconds(10L)
val DEFAULT_TIMEOUT_ON_MOBILE: Duration = Duration.ofSeconds(30L)
@@ -43,13 +46,13 @@ object HttpClientManager {
private var defaultHttpClientWithoutProxy: OkHttpClient? = null
private var userAgent: String = "Amethyst"
private var currentProxy: Proxy? = null
private var currentProxy: Proxy? = DEFAULT_TOR_PROXY
private val cache = EncryptionKeyCache()
fun setDefaultProxy(proxy: Proxy?) {
if (currentProxy != proxy) {
Log.d("HttpClient", "Changing proxy to: ${proxy != null}")
Log.d("HttpClient", "Changing proxy to: $proxy")
currentProxy = proxy
// recreates singleton
@@ -117,6 +120,11 @@ object HttpClientManager {
defaultHttpClientWithoutProxy!!
}
fun setProxyNotReady() {
// this blocks all connections unless Orbot is live.
setDefaultProxy(DEFAULT_TOR_PROXY)
}
fun setDefaultProxyOnPort(port: Int) {
setDefaultProxy(Proxy(Proxy.Type.SOCKS, InetSocketAddress("127.0.0.1", port)))
}
@@ -46,7 +46,6 @@ import com.vitorpamplona.amethyst.ui.navigation.Route
import com.vitorpamplona.amethyst.ui.screen.AccountScreen
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
import com.vitorpamplona.amethyst.ui.tor.TorManager
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
@@ -112,8 +111,6 @@ class MainActivity : AppCompatActivity() {
checkLanguage(locales.get(0).language)
}
TorManager.startTor(this)
Log.d("Lifetime Event", "MainActivity.onResume")
// starts muted every time
@@ -160,8 +157,6 @@ class MainActivity : AppCompatActivity() {
(getSystemService(ConnectivityManager::class.java) as ConnectivityManager)
.unregisterNetworkCallback(networkCallback)
TorManager.stopTor(this)
super.onPause()
}
@@ -50,6 +50,7 @@ import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextFieldDefaults.contentPadding
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -246,7 +247,9 @@ private fun DialogContent(
val writeStoragePermissionState =
rememberPermissionState(Manifest.permission.WRITE_EXTERNAL_STORAGE) { isGranted ->
if (isGranted) {
saveMediaToGallery(myContent, localContext, accountViewModel)
scope.launch {
saveMediaToGallery(myContent, localContext, accountViewModel)
}
scope.launch {
Toast
.makeText(
@@ -264,7 +267,9 @@ private fun DialogContent(
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ||
writeStoragePermissionState.status.isGranted
) {
saveMediaToGallery(myContent, localContext, accountViewModel)
scope.launch {
saveMediaToGallery(myContent, localContext, accountViewModel)
}
scope.launch {
Toast
.makeText(
@@ -39,13 +39,16 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
import androidx.lifecycle.viewmodel.compose.viewModel
@@ -58,8 +61,13 @@ import com.vitorpamplona.amethyst.ui.navigation.AppNavigation
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginOrSignupScreen
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.tor.TorManager
import com.vitorpamplona.amethyst.ui.tor.TorType
import com.vitorpamplona.quartz.nip55AndroidSigner.NostrSignerExternal
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@Composable
fun AccountScreen(
@@ -136,6 +144,8 @@ fun LoggedInPage(
accountViewModel.restartServices()
}
ManageTorInstance(accountViewModel)
ListenToExternalSignerIfNeeded(accountViewModel)
AppNavigation(
@@ -145,6 +155,55 @@ fun LoggedInPage(
)
}
@Composable
fun ManageTorInstance(accountViewModel: AccountViewModel) {
val torSettings by accountViewModel.account.settings.torSettings.torType
.collectAsStateWithLifecycle()
if (torSettings == TorType.INTERNAL) {
ManageTorInstanceInner(accountViewModel)
}
}
@Composable
fun ManageTorInstanceInner(accountViewModel: AccountViewModel) {
val context = LocalContext.current.applicationContext
val lifeCycleOwner = LocalLifecycleOwner.current
val scope = rememberCoroutineScope()
var job = remember<Job?> { null }
DisposableEffect(key1 = accountViewModel) {
job?.cancel()
job = null
TorManager.startTorIfNotAlreadyOn(context)
val observer =
LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> {
job?.cancel()
job = null
TorManager.startTorIfNotAlreadyOn(context)
}
Lifecycle.Event.ON_PAUSE -> {
job =
scope.launch {
delay(5000) // 5 seconds
TorManager.stopTor(context)
}
}
else -> {}
}
}
lifeCycleOwner.lifecycle.addObserver(observer)
onDispose {
lifeCycleOwner.lifecycle.removeObserver(observer)
TorManager.stopTor(context)
}
}
}
@Composable
private fun ListenToExternalSignerIfNeeded(accountViewModel: AccountViewModel) {
if (accountViewModel.account.signer is NostrSignerExternal) {
@@ -30,20 +30,40 @@ import androidx.appcompat.app.AppCompatActivity.BIND_AUTO_CREATE
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
import org.torproject.jni.TorService
import org.torproject.jni.TorService.LocalBinder
import java.util.concurrent.atomic.AtomicBoolean
object TorManager {
var runningIntent: Intent? = null
var torService: TorService? = null
// To make sure we don't start two services.
var isConnectingMutex = AtomicBoolean(false)
fun startTorIfNotAlreadyOn(ctx: Context) {
if (torService == null) {
startTor(ctx)
if (runningIntent == null && isConnectingMutex.compareAndSet(false, true)) {
try {
startTor(ctx)
} finally {
isConnectingMutex.set(false)
}
}
}
fun startTor(ctx: Context) {
fun stopTor(ctx: Context) {
Log.d("TorManager", "Stopping Tor Service")
runningIntent?.let {
ctx.stopService(runningIntent)
}
runningIntent = null
torService = null
}
private fun startTor(ctx: Context) {
Log.d("TorManager", "Binding Tor Service")
val currentIntent = Intent(ctx, TorService::class.java)
runningIntent = currentIntent
ctx.bindService(
Intent(ctx, TorService::class.java),
currentIntent,
object : ServiceConnection {
override fun onServiceConnected(
name: ComponentName,
@@ -66,6 +86,7 @@ object TorManager {
}
override fun onServiceDisconnected(name: ComponentName) {
runningIntent = null
torService = null
Log.d("TorManager", "Tor Service Disconected")
}
@@ -74,12 +95,6 @@ object TorManager {
)
}
fun stopTor(ctx: Context) {
Log.d("TorManager", "Stopping Tor Service")
torService = null
ctx.stopService(Intent(ctx, TorService::class.java))
}
fun isSocksReady() =
torService?.let {
it.socksPort > 0