mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-06 03:38:23 +00:00
fix(desktop): keep the Tor session bypass out of the saved settings
Audit follow-ups on the Tor splash escape:
- The "Use regular connection" bypass lived in AppInner's torSettings, which
the in-app Tor settings dialog shows and saves, so saving any Tor toggle
after a bypass persisted torType=OFF for good. The bypass is now a
window-level torSessionBypassFlow; torSettings stays the user's saved
choice and effectiveTorSettings drives the gate and relay routing. Saving
Tor settings re-applies them and ends the bypass.
- Saving Tor settings from the splash no longer calls onRestartApp():
nothing below the gate exists yet, and the rebuild only reset the splash's
escape timer.
- Splash reuses connect_via_tor2 ("Tor Settings") instead of a duplicate key,
uses app_logo for the icon description, and the explainer no longer claims
every connection goes through Tor (the default preset does not).
- Drop the inline fully-qualified DesktopTorPreferences names.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016HNwcGUk22pFsTSK3YqUek
This commit is contained in:
@@ -1865,8 +1865,7 @@
|
||||
<string name="tor_keep_waiting">Keep waiting</string>
|
||||
<string name="tor_splash_connecting">Connecting to Tor…</string>
|
||||
<string name="tor_splash_error">Tor error: %1$s</string>
|
||||
<string name="tor_splash_explainer">Amethyst sends your connections through Tor to hide your IP address. Starting Tor can take a minute and needs internet access.</string>
|
||||
<string name="tor_splash_settings">Tor settings</string>
|
||||
<string name="tor_splash_explainer">Amethyst uses Tor to hide your IP address from relays. Starting Tor can take a minute and needs internet access.</string>
|
||||
<string name="notification_settings_categories_explainer">Tap a category to open Android notification settings for it — sound, importance, badges and Do Not Disturb live there.</string>
|
||||
<string name="notification_channel_status_on">On</string>
|
||||
<string name="notification_channel_status_silent">Silent</string>
|
||||
|
||||
@@ -347,14 +347,11 @@ fun main(args: Array<String>) {
|
||||
val feedSearchActiveState = remember { mutableStateOf(false) }
|
||||
|
||||
// Tor state at Window level — survives key() app rebuild
|
||||
var torSettings by remember {
|
||||
mutableStateOf(
|
||||
com.vitorpamplona.amethyst.desktop.tor.DesktopTorPreferences
|
||||
.load(),
|
||||
)
|
||||
}
|
||||
val torSettings = remember { DesktopTorPreferences.load() }
|
||||
val torTypeFlow = remember { kotlinx.coroutines.flow.MutableStateFlow(torSettings.torType) }
|
||||
val externalPortFlow = remember { kotlinx.coroutines.flow.MutableStateFlow(torSettings.externalSocksPort) }
|
||||
// "Use regular connection" on the Tor splash: Tor off for this run only, never saved.
|
||||
val torSessionBypassFlow = remember { kotlinx.coroutines.flow.MutableStateFlow(false) }
|
||||
val windowScope = rememberCoroutineScope()
|
||||
val torManager =
|
||||
remember {
|
||||
@@ -739,6 +736,7 @@ fun main(args: Array<String>) {
|
||||
torManager = torManager,
|
||||
torTypeFlow = torTypeFlow,
|
||||
externalPortFlow = externalPortFlow,
|
||||
torSessionBypassFlow = torSessionBypassFlow,
|
||||
initialTorSettings = torSettings,
|
||||
onNavigateToScreen = { navigateToScreen = it },
|
||||
)
|
||||
@@ -775,6 +773,7 @@ fun App(
|
||||
torManager: com.vitorpamplona.amethyst.commons.tor.ITorManager,
|
||||
torTypeFlow: kotlinx.coroutines.flow.MutableStateFlow<com.vitorpamplona.amethyst.commons.tor.TorType>,
|
||||
externalPortFlow: kotlinx.coroutines.flow.MutableStateFlow<Int>,
|
||||
torSessionBypassFlow: kotlinx.coroutines.flow.MutableStateFlow<Boolean>,
|
||||
initialTorSettings: com.vitorpamplona.amethyst.commons.tor.TorSettings,
|
||||
onNavigateToScreen: ((DeckColumnType) -> Unit) -> Unit = {},
|
||||
testOverrides: LaunchTestOverrides? = null,
|
||||
@@ -831,6 +830,7 @@ fun App(
|
||||
torManager = torManager,
|
||||
torTypeFlow = torTypeFlow,
|
||||
externalPortFlow = externalPortFlow,
|
||||
torSessionBypassFlow = torSessionBypassFlow,
|
||||
initialTorSettings = initialTorSettings,
|
||||
onNavigateToScreen = onNavigateToScreen,
|
||||
testOverrides = testOverrides,
|
||||
@@ -866,6 +866,7 @@ private fun AppInner(
|
||||
torManager: com.vitorpamplona.amethyst.commons.tor.ITorManager,
|
||||
torTypeFlow: kotlinx.coroutines.flow.MutableStateFlow<com.vitorpamplona.amethyst.commons.tor.TorType>,
|
||||
externalPortFlow: kotlinx.coroutines.flow.MutableStateFlow<Int>,
|
||||
torSessionBypassFlow: kotlinx.coroutines.flow.MutableStateFlow<Boolean>,
|
||||
initialTorSettings: com.vitorpamplona.amethyst.commons.tor.TorSettings,
|
||||
onNavigateToScreen: ((DeckColumnType) -> Unit) -> Unit,
|
||||
testOverrides: LaunchTestOverrides?,
|
||||
@@ -878,28 +879,26 @@ private fun AppInner(
|
||||
}
|
||||
|
||||
// Always reload from prefs — after key() rebuild, prefs have the latest saved settings.
|
||||
// The mode comes from the window-level torTypeFlow instead, so a session-only "continue
|
||||
// without Tor" (which never touches prefs) survives the rebuild.
|
||||
// Tests can short-circuit the prefs read via `testOverrides.torSettingsOverride` so the
|
||||
// Tor splash gate (below) does not block them behind a real kmp-tor runtime.
|
||||
// torSettings is the user's saved choice (what the settings UI shows and saves);
|
||||
// effectiveTorSettings is what this session actually routes by.
|
||||
var torSettings by remember {
|
||||
mutableStateOf(
|
||||
testOverrides?.torSettingsOverride
|
||||
?: DesktopTorPreferences.load().copy(torType = torTypeFlow.value),
|
||||
)
|
||||
mutableStateOf(testOverrides?.torSettingsOverride ?: DesktopTorPreferences.load())
|
||||
}
|
||||
val torSessionBypass by torSessionBypassFlow.collectAsState()
|
||||
val effectiveTorSettings = if (torSessionBypass) torSettings.copy(torType = TorType.OFF) else torSettings
|
||||
|
||||
// Gate: block EVERYTHING until Tor proxy is ready (when Tor expected)
|
||||
// This must be before any OkHttpClient/Coil/relay creation
|
||||
val torStatus by torManager.status.collectAsState()
|
||||
val isTorExpected = torSettings.torType != TorType.OFF
|
||||
val isTorExpected = effectiveTorSettings.torType != TorType.OFF
|
||||
if (isTorExpected && torStatus !is TorServiceStatus.Active) {
|
||||
var showTorSettings by remember { mutableStateOf(false) }
|
||||
TorConnectingSplash(
|
||||
status = torStatus,
|
||||
onContinueWithoutTor = {
|
||||
// Session only: prefs keep the user's Tor choice for the next launch.
|
||||
torSettings = torSettings.copy(torType = TorType.OFF)
|
||||
torSessionBypassFlow.value = true
|
||||
torTypeFlow.value = TorType.OFF
|
||||
},
|
||||
onOpenTorSettings = { showTorSettings = true },
|
||||
@@ -909,11 +908,12 @@ private fun AppInner(
|
||||
currentSettings = torSettings,
|
||||
torStatus = torStatus,
|
||||
onSettingsChanged = { newSettings ->
|
||||
// No onRestartApp(): nothing below the gate has been built yet, and a
|
||||
// rebuild would only restart the splash (and its escape timer).
|
||||
torSettings = newSettings
|
||||
DesktopTorPreferences.save(newSettings)
|
||||
torTypeFlow.value = newSettings.torType
|
||||
externalPortFlow.value = newSettings.externalSocksPort
|
||||
onRestartApp()
|
||||
},
|
||||
onDismiss = { showTorSettings = false },
|
||||
)
|
||||
@@ -977,15 +977,15 @@ private fun AppInner(
|
||||
|
||||
// Build TorRelayEvaluation for per-relay routing
|
||||
val torRelayEvaluation =
|
||||
remember(torSettings) {
|
||||
remember(effectiveTorSettings) {
|
||||
com.vitorpamplona.amethyst.commons.tor.TorRelayEvaluation(
|
||||
torSettings =
|
||||
com.vitorpamplona.amethyst.commons.tor.TorRelaySettings(
|
||||
torType = torSettings.torType,
|
||||
onionRelaysViaTor = torSettings.onionRelaysViaTor,
|
||||
dmRelaysViaTor = torSettings.dmRelaysViaTor,
|
||||
newRelaysViaTor = torSettings.newRelaysViaTor,
|
||||
trustedRelaysViaTor = torSettings.trustedRelaysViaTor,
|
||||
torType = effectiveTorSettings.torType,
|
||||
onionRelaysViaTor = effectiveTorSettings.onionRelaysViaTor,
|
||||
dmRelaysViaTor = effectiveTorSettings.dmRelaysViaTor,
|
||||
newRelaysViaTor = effectiveTorSettings.newRelaysViaTor,
|
||||
trustedRelaysViaTor = effectiveTorSettings.trustedRelaysViaTor,
|
||||
),
|
||||
// TODO: populate from account relay lists
|
||||
classification =
|
||||
@@ -1491,9 +1491,11 @@ private fun AppInner(
|
||||
status = currentTorStatus,
|
||||
settings = torSettings,
|
||||
onSettingsChanged = { newSettings ->
|
||||
// Saving re-applies the saved choice, ending a
|
||||
// session bypass from the splash.
|
||||
torSessionBypassFlow.value = false
|
||||
torSettings = newSettings
|
||||
com.vitorpamplona.amethyst.desktop.tor.DesktopTorPreferences
|
||||
.save(newSettings)
|
||||
DesktopTorPreferences.save(newSettings)
|
||||
torTypeFlow.value = newSettings.torType
|
||||
externalPortFlow.value = newSettings.externalSocksPort
|
||||
// Rebuild app to apply Tor changes
|
||||
|
||||
+4
-3
@@ -47,11 +47,12 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_logo
|
||||
import com.vitorpamplona.amethyst.commons.resources.connect_via_tor2
|
||||
import com.vitorpamplona.amethyst.commons.resources.tor_continue_without_for_session
|
||||
import com.vitorpamplona.amethyst.commons.resources.tor_splash_connecting
|
||||
import com.vitorpamplona.amethyst.commons.resources.tor_splash_error
|
||||
import com.vitorpamplona.amethyst.commons.resources.tor_splash_explainer
|
||||
import com.vitorpamplona.amethyst.commons.resources.tor_splash_settings
|
||||
import com.vitorpamplona.amethyst.commons.tor.TorServiceStatus
|
||||
import com.vitorpamplona.amethyst.desktop.platform.IconResources
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -112,7 +113,7 @@ fun TorConnectingSplash(
|
||||
Spacer(Modifier.height(24.dp))
|
||||
Icon(
|
||||
painter = IconResources.rawBitmapPainter,
|
||||
contentDescription = "Amethyst",
|
||||
contentDescription = stringResource(Res.string.app_logo),
|
||||
modifier = Modifier.size(96.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
@@ -120,7 +121,7 @@ fun TorConnectingSplash(
|
||||
Spacer(Modifier.height(24.dp))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
OutlinedButton(onClick = onOpenTorSettings) {
|
||||
Text(stringResource(Res.string.tor_splash_settings))
|
||||
Text(stringResource(Res.string.connect_via_tor2))
|
||||
}
|
||||
Button(onClick = onContinueWithoutTor) {
|
||||
Text(stringResource(Res.string.tor_continue_without_for_session))
|
||||
|
||||
+4
@@ -129,6 +129,7 @@ class AppStateMachineTest {
|
||||
torManager = torManager,
|
||||
torTypeFlow = MutableStateFlow(TorType.OFF),
|
||||
externalPortFlow = MutableStateFlow(9050),
|
||||
torSessionBypassFlow = MutableStateFlow(false),
|
||||
initialTorSettings = OFF_TOR_SETTINGS,
|
||||
testOverrides =
|
||||
LaunchTestOverrides(
|
||||
@@ -190,6 +191,7 @@ class AppStateMachineTest {
|
||||
torManager = torManager,
|
||||
torTypeFlow = MutableStateFlow(TorType.OFF),
|
||||
externalPortFlow = MutableStateFlow(9050),
|
||||
torSessionBypassFlow = MutableStateFlow(false),
|
||||
initialTorSettings = OFF_TOR_SETTINGS,
|
||||
testOverrides =
|
||||
LaunchTestOverrides(
|
||||
@@ -266,6 +268,7 @@ class AppStateMachineTest {
|
||||
torManager = torManager,
|
||||
torTypeFlow = MutableStateFlow(TorType.OFF),
|
||||
externalPortFlow = MutableStateFlow(9050),
|
||||
torSessionBypassFlow = MutableStateFlow(false),
|
||||
initialTorSettings = OFF_TOR_SETTINGS,
|
||||
testOverrides =
|
||||
LaunchTestOverrides(
|
||||
@@ -342,6 +345,7 @@ class AppStateMachineTest {
|
||||
torManager = torManager,
|
||||
torTypeFlow = MutableStateFlow(TorType.OFF),
|
||||
externalPortFlow = MutableStateFlow(9050),
|
||||
torSessionBypassFlow = MutableStateFlow(false),
|
||||
initialTorSettings = OFF_TOR_SETTINGS,
|
||||
testOverrides =
|
||||
LaunchTestOverrides(
|
||||
|
||||
Reference in New Issue
Block a user