diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/Amber.kt b/app/src/main/java/com/greenart7c3/nostrsigner/Amber.kt index 863716ab..8a33ebba 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/Amber.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/Amber.kt @@ -117,8 +117,25 @@ class Amber : var settings: AmberSettings = AmberSettings() + /** + * False until the first async settings load ([LocalPreferences.reloadApp]) lands. Until + * then [settings] is the data-class default, whose torMode (DISABLED) is a guess, not a + * decision — any dial in that window must route through the proxy client (preset from + * plain prefs in [onCreate]) instead of trusting the default and going to the clearnet. + */ + @Volatile var isSettingsLoaded = false + val factory = OkHttpWebSocket.Builder { url -> - val useProxy = if (isPrivateIp(url.url)) false else settings.torMode != TorMode.DISABLED + val useProxy = when { + isPrivateIp(url.url) -> false + // Settings load asynchronously; until they land, torMode's default (DISABLED) + // is a guess, not a decision. Route through the proxy client — preset from + // plain prefs in onCreate — so a BUILTIN/ORBOT user's pre-Tor dials (network + // callbacks, profile fetches) hit the fail-closed placeholder instead of the + // clearnet. + !isSettingsLoaded -> true + else -> settings.torMode != TorMode.DISABLED + } HttpClientManager.getHttpClient(useProxy) } @@ -354,6 +371,12 @@ class Amber : } instance = this + // Before anything can dial: route the proxy client through the user's SOCKS + // setup (or the fail-closed placeholder for BUILTIN) read synchronously from + // plain prefs. Dials in the window before the async settings load — network + // callbacks, profile fetches — pick this up via the factory's !isSettingsLoaded + // gate; without it that window leaks clearnet traffic for Tor users. + LocalPreferences.presetProxyFromPrefs(this) stats.createNotificationChannel() Thread.setDefaultUncaughtExceptionHandler(UnexpectedCrashSaver(crashReportCache, applicationIOScope)) @@ -654,7 +677,11 @@ class Amber : .build() val coilCallFactory = okhttp3.Call.Factory { request -> val url = request.url.toString() - val useProxy = if (isPrivateIp(url)) false else settings.torMode != TorMode.DISABLED + val useProxy = when { + isPrivateIp(url) -> false + !isSettingsLoaded -> true + else -> settings.torMode != TorMode.DISABLED + } HttpClientManager.getHttpClient(useProxy).newCall(request) } return ImageLoader.Builder(context) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/LocalPreferences.kt b/app/src/main/java/com/greenart7c3/nostrsigner/LocalPreferences.kt index 6ebffb86..3fcdb449 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/LocalPreferences.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/LocalPreferences.kt @@ -244,6 +244,7 @@ object LocalPreferences { accountCache.clear() warmAccountCache(context) context.settings = loadSettingsFromEncryptedStorage(context) + context.isSettingsLoaded = true context.settings.language?.let { AppCompatDelegate.setApplicationLocales( LocaleListCompat.forLanguageTags(it), @@ -251,6 +252,29 @@ object LocalPreferences { } } + /** + * Synchronously presets the OkHttp proxy from plain (unencrypted) prefs, before any + * dial can happen. The full settings load is async; until it lands, the relay/Coil + * factories treat torMode as unknown and route through the proxy client — which only + * protects Tor users if that client already carries the SOCKS proxy (fail-closed + * placeholder for BUILTIN, the configured port for ORBOT). + */ + fun presetProxyFromPrefs(context: Context) { + val prefs = sharedPrefs(context) + val torMode = try { + TorMode.valueOf(prefs.getString(SettingsKeys.TOR_MODE.key, TorMode.DISABLED.name)!!) + } catch (_: IllegalArgumentException) { + TorMode.DISABLED + } + when (torMode) { + TorMode.ORBOT -> HttpClientManager.setDefaultProxyOnPort( + prefs.getInt(SettingsKeys.PROXY_PORT.key, 9050), + ) + TorMode.BUILTIN -> HttpClientManager.setDefaultProxyOnPort(TorManager.socksPort.value) + TorMode.DISABLED -> {} + } + } + fun getStartServiceOnBoot(context: Context): Boolean = sharedPrefs(context).getBoolean(SettingsKeys.START_SERVICE_ON_BOOT.key, true) fun loadSettingsFromEncryptedStorage(context: Context = Amber.instance): AmberSettings {