- 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) { if (myAccount != null) {
when (myAccount.settings.torSettings.torType.value) { when (myAccount.settings.torSettings.torType.value) {
TorType.INTERNAL -> { TorType.INTERNAL -> {
Log.d("TorManager", "Relays Service Connected ${TorManager.socksPort()}") // Tor's lib will automatically set this port.
HttpClientManager.setDefaultProxyOnPort(TorManager.socksPort()) 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) 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.NostrLnZapPaymentResponseDataSource
import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.location.LocationState 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.service.uploads.FileHeader
import com.vitorpamplona.amethyst.tryAndWait import com.vitorpamplona.amethyst.tryAndWait
import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
@@ -3648,12 +3647,6 @@ class Account(
fun markDonatedInThisVersion() = settings.markDonatedInThisVersion(BuildConfig.VERSION_NAME) 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() = fun shouldUseTorForImageDownload() =
when (settings.torSettings.torType.value) { when (settings.torSettings.torType.value) {
TorType.OFF -> false TorType.OFF -> false
@@ -20,6 +20,7 @@
*/ */
package com.vitorpamplona.amethyst.service.okhttp package com.vitorpamplona.amethyst.service.okhttp
import android.R.attr.port
import android.util.Log import android.util.Log
import com.vitorpamplona.quartz.nip17Dm.files.encryption.NostrCipher import com.vitorpamplona.quartz.nip17Dm.files.encryption.NostrCipher
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@@ -35,6 +36,8 @@ object HttpClientManager {
.followSslRedirects(true) .followSslRedirects(true)
.build() .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_WIFI: Duration = Duration.ofSeconds(10L)
val DEFAULT_TIMEOUT_ON_MOBILE: Duration = Duration.ofSeconds(30L) val DEFAULT_TIMEOUT_ON_MOBILE: Duration = Duration.ofSeconds(30L)
@@ -43,13 +46,13 @@ object HttpClientManager {
private var defaultHttpClientWithoutProxy: OkHttpClient? = null private var defaultHttpClientWithoutProxy: OkHttpClient? = null
private var userAgent: String = "Amethyst" private var userAgent: String = "Amethyst"
private var currentProxy: Proxy? = null private var currentProxy: Proxy? = DEFAULT_TOR_PROXY
private val cache = EncryptionKeyCache() private val cache = EncryptionKeyCache()
fun setDefaultProxy(proxy: Proxy?) { fun setDefaultProxy(proxy: Proxy?) {
if (currentProxy != proxy) { if (currentProxy != proxy) {
Log.d("HttpClient", "Changing proxy to: ${proxy != null}") Log.d("HttpClient", "Changing proxy to: $proxy")
currentProxy = proxy currentProxy = proxy
// recreates singleton // recreates singleton
@@ -117,6 +120,11 @@ object HttpClientManager {
defaultHttpClientWithoutProxy!! defaultHttpClientWithoutProxy!!
} }
fun setProxyNotReady() {
// this blocks all connections unless Orbot is live.
setDefaultProxy(DEFAULT_TOR_PROXY)
}
fun setDefaultProxyOnPort(port: Int) { fun setDefaultProxyOnPort(port: Int) {
setDefaultProxy(Proxy(Proxy.Type.SOCKS, InetSocketAddress("127.0.0.1", port))) 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.AccountScreen
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme 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.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
@@ -112,8 +111,6 @@ class MainActivity : AppCompatActivity() {
checkLanguage(locales.get(0).language) checkLanguage(locales.get(0).language)
} }
TorManager.startTor(this)
Log.d("Lifetime Event", "MainActivity.onResume") Log.d("Lifetime Event", "MainActivity.onResume")
// starts muted every time // starts muted every time
@@ -160,8 +157,6 @@ class MainActivity : AppCompatActivity() {
(getSystemService(ConnectivityManager::class.java) as ConnectivityManager) (getSystemService(ConnectivityManager::class.java) as ConnectivityManager)
.unregisterNetworkCallback(networkCallback) .unregisterNetworkCallback(networkCallback)
TorManager.stopTor(this)
super.onPause() super.onPause()
} }
@@ -50,6 +50,7 @@ import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextFieldDefaults.contentPadding
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -246,7 +247,9 @@ private fun DialogContent(
val writeStoragePermissionState = val writeStoragePermissionState =
rememberPermissionState(Manifest.permission.WRITE_EXTERNAL_STORAGE) { isGranted -> rememberPermissionState(Manifest.permission.WRITE_EXTERNAL_STORAGE) { isGranted ->
if (isGranted) { if (isGranted) {
saveMediaToGallery(myContent, localContext, accountViewModel) scope.launch {
saveMediaToGallery(myContent, localContext, accountViewModel)
}
scope.launch { scope.launch {
Toast Toast
.makeText( .makeText(
@@ -264,7 +267,9 @@ private fun DialogContent(
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ||
writeStoragePermissionState.status.isGranted writeStoragePermissionState.status.isGranted
) { ) {
saveMediaToGallery(myContent, localContext, accountViewModel) scope.launch {
saveMediaToGallery(myContent, localContext, accountViewModel)
}
scope.launch { scope.launch {
Toast Toast
.makeText( .makeText(
@@ -39,13 +39,16 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.ViewModelStore import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
import androidx.lifecycle.viewmodel.compose.viewModel 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.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginOrSignupScreen import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginOrSignupScreen
import com.vitorpamplona.amethyst.ui.stringRes 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 com.vitorpamplona.quartz.nip55AndroidSigner.NostrSignerExternal
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@Composable @Composable
fun AccountScreen( fun AccountScreen(
@@ -136,6 +144,8 @@ fun LoggedInPage(
accountViewModel.restartServices() accountViewModel.restartServices()
} }
ManageTorInstance(accountViewModel)
ListenToExternalSignerIfNeeded(accountViewModel) ListenToExternalSignerIfNeeded(accountViewModel)
AppNavigation( 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 @Composable
private fun ListenToExternalSignerIfNeeded(accountViewModel: AccountViewModel) { private fun ListenToExternalSignerIfNeeded(accountViewModel: AccountViewModel) {
if (accountViewModel.account.signer is NostrSignerExternal) { 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 com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
import org.torproject.jni.TorService import org.torproject.jni.TorService
import org.torproject.jni.TorService.LocalBinder import org.torproject.jni.TorService.LocalBinder
import java.util.concurrent.atomic.AtomicBoolean
object TorManager { object TorManager {
var runningIntent: Intent? = null
var torService: TorService? = null var torService: TorService? = null
// To make sure we don't start two services.
var isConnectingMutex = AtomicBoolean(false)
fun startTorIfNotAlreadyOn(ctx: Context) { fun startTorIfNotAlreadyOn(ctx: Context) {
if (torService == null) { if (runningIntent == null && isConnectingMutex.compareAndSet(false, true)) {
startTor(ctx) 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") Log.d("TorManager", "Binding Tor Service")
val currentIntent = Intent(ctx, TorService::class.java)
runningIntent = currentIntent
ctx.bindService( ctx.bindService(
Intent(ctx, TorService::class.java), currentIntent,
object : ServiceConnection { object : ServiceConnection {
override fun onServiceConnected( override fun onServiceConnected(
name: ComponentName, name: ComponentName,
@@ -66,6 +86,7 @@ object TorManager {
} }
override fun onServiceDisconnected(name: ComponentName) { override fun onServiceDisconnected(name: ComponentName) {
runningIntent = null
torService = null torService = null
Log.d("TorManager", "Tor Service Disconected") 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() = fun isSocksReady() =
torService?.let { torService?.let {
it.socksPort > 0 it.socksPort > 0