From 7c3166a83c5b1cb8adaeef515ee14f6c0f122105 Mon Sep 17 00:00:00 2001 From: Avently <7953703+avently@users.noreply.github.com> Date: Tue, 17 Sep 2024 20:01:25 +0700 Subject: [PATCH] new logic --- .../chat/simplex/common/model/SimpleXAPI.kt | 74 +++++++-- .../views/migration/MigrateFromDevice.kt | 16 +- .../common/views/migration/MigrateToDevice.kt | 50 +++--- .../views/usersettings/NetworkAndServers.kt | 154 +++++------------- 4 files changed, 140 insertions(+), 154 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 61d158a781..e7d1d9a258 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -129,7 +129,22 @@ class AppPreferences { val terminalAlwaysVisible = mkBoolPreference(SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE, false) val networkUseSocksProxy = mkBoolPreference(SHARED_PREFS_NETWORK_USE_SOCKS_PROXY, false) val networkShowSubscriptionPercentage = mkBoolPreference(SHARED_PREFS_NETWORK_SHOW_SUBSCRIPTION_PERCENTAGE, false) - val networkProxyHostPort = mkStrPreference(SHARED_PREFS_NETWORK_PROXY_HOST_PORT, "localhost:9050") + private val _networkProxy = mkStrPreference(SHARED_PREFS_NETWORK_PROXY_HOST_PORT, json.encodeToString(NetworkProxy())) + val networkProxy: SharedPreference = SharedPreference( + get = fun(): NetworkProxy { + val value = _networkProxy.get() ?: return NetworkProxy() + return try { + if (value.startsWith("{")) { + json.decodeFromString(value) + } else { + NetworkProxy(host = value.substringBefore(":").ifBlank { "localhost" }, port = value.substringAfter(":").toIntOrNull() ?: 9050) + } + } catch (e: Throwable) { + NetworkProxy() + } + }, + set = fun(proxy: NetworkProxy) { _networkProxy.set(json.encodeToString(proxy)) } + ) private val _networkSessionMode = mkStrPreference(SHARED_PREFS_NETWORK_SESSION_MODE, TransportSessionMode.default.name) val networkSessionMode: SharedPreference = SharedPreference( get = fun(): TransportSessionMode { @@ -2778,13 +2793,9 @@ object ChatController { fun getNetCfg(): NetCfg { val useSocksProxy = appPrefs.networkUseSocksProxy.get() - val proxyHostPort = appPrefs.networkProxyHostPort.get() + val networkProxy = appPrefs.networkProxy.get() val socksProxy = if (useSocksProxy) { - if (proxyHostPort?.startsWith("localhost:") == true) { - proxyHostPort.removePrefix("localhost") - } else { - proxyHostPort ?: ":9050" - } + networkProxy.toProxyString() } else { null } @@ -2826,7 +2837,7 @@ object ChatController { } /** - * [AppPreferences.networkProxyHostPort] is not changed here, use appPrefs to set it + * [AppPreferences.networkProxy] is not changed here, use appPrefs to set it * */ fun setNetCfg(cfg: NetCfg) { appPrefs.networkUseSocksProxy.set(cfg.useSocksProxy) @@ -3570,13 +3581,8 @@ data class NetCfg( val useSocksProxy: Boolean get() = socksProxy != null val enableKeepAlive: Boolean get() = tcpKeepAlive != null - fun withHostPort(hostPort: String?, default: String? = ":9050"): NetCfg { - val socksProxy = if (hostPort?.startsWith("localhost:") == true) { - hostPort.removePrefix("localhost") - } else { - hostPort ?: default - } - return copy(socksProxy = socksProxy) + fun withProxy(proxy: NetworkProxy?, default: String? = ":9050"): NetCfg { + return copy(socksProxy = proxy?.toProxyString() ?: default) } companion object { @@ -3618,6 +3624,39 @@ data class NetCfg( } } +@Serializable +data class NetworkProxy( + val username: String = "", + val password: String = "", + val auth: NetworkProxyAuth = NetworkProxyAuth.ISOLATE, + val host: String = "localhost", + val port: Int = 9050 +) { + fun toProxyString(): String? { + var res = "" + if (auth == NetworkProxyAuth.USERNAME && (username.isNotBlank() || password.isNotBlank())) { + res += username.trim() + ":" + password.trim() + "@" + } else if (auth == NetworkProxyAuth.USERNAME) { + res += "@" + } + if (host != "localhost") { + res += if (host.contains(':')) "[${host.trim(' ', '[', ']')}]" else host.trim() + } + if (port != 9050 || res.isEmpty()) { + res += ":$port" + } + return res + } +} + +@Serializable +enum class NetworkProxyAuth { + @SerialName("isolate") + ISOLATE, + @SerialName("username") + USERNAME, +} + enum class OnionHosts { NEVER, PREFER, REQUIRED } @@ -6185,6 +6224,7 @@ enum class NotificationsMode() { @Serializable data class AppSettings( var networkConfig: NetCfg? = null, + var networkProxy: NetworkProxy? = null, var privacyEncryptLocalFiles: Boolean? = null, var privacyAskToApproveRelays: Boolean? = null, var privacyAcceptImages: Boolean? = null, @@ -6216,6 +6256,7 @@ data class AppSettings( val empty = AppSettings() val def = defaults if (networkConfig != def.networkConfig) { empty.networkConfig = networkConfig } + if (networkProxy != def.networkProxy) { empty.networkProxy = networkProxy } if (privacyEncryptLocalFiles != def.privacyEncryptLocalFiles) { empty.privacyEncryptLocalFiles = privacyEncryptLocalFiles } if (privacyAskToApproveRelays != def.privacyAskToApproveRelays) { empty.privacyAskToApproveRelays = privacyAskToApproveRelays } if (privacyAcceptImages != def.privacyAcceptImages) { empty.privacyAcceptImages = privacyAcceptImages } @@ -6255,6 +6296,7 @@ data class AppSettings( } setNetCfg(net) } + networkProxy?.let { def.networkProxy.set(it) } privacyEncryptLocalFiles?.let { def.privacyEncryptLocalFiles.set(it) } privacyAskToApproveRelays?.let { def.privacyAskToApproveRelays.set(it) } privacyAcceptImages?.let { def.privacyAcceptImages.set(it) } @@ -6287,6 +6329,7 @@ data class AppSettings( val defaults: AppSettings get() = AppSettings( networkConfig = NetCfg.defaults, + networkProxy = null, privacyEncryptLocalFiles = true, privacyAskToApproveRelays = true, privacyAcceptImages = true, @@ -6320,6 +6363,7 @@ data class AppSettings( val def = appPreferences return defaults.copy( networkConfig = getNetCfg(), + networkProxy = def.networkProxy.get(), privacyEncryptLocalFiles = def.privacyEncryptLocalFiles.get(), privacyAskToApproveRelays = def.privacyAskToApproveRelays.get(), privacyAcceptImages = def.privacyAcceptImages.get(), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt index a71503e315..ce39ab91db 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt @@ -4,7 +4,6 @@ import SectionBottomSpacer import SectionSpacer import SectionTextFooter import SectionView -import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* @@ -17,6 +16,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatController.getNetCfg import chat.simplex.common.model.ChatController.startChat import chat.simplex.common.model.ChatController.startChatWithTemporaryDatabase @@ -38,7 +38,6 @@ import kotlinx.serialization.* import java.io.File import java.net.URLEncoder import kotlin.math.max -import kotlin.math.sqrt @Serializable data class MigrationFileLinkData( @@ -46,16 +45,20 @@ data class MigrationFileLinkData( ) { @Serializable data class NetworkConfig( - val socksProxy: String?, + // Legacy. Remove in 2025 + @SerialName("socksProxy") + val legacySocksProxy: String?, + val networkProxy: NetworkProxy?, val hostMode: HostMode?, val requiredHostMode: Boolean? ) { - fun hasOnionConfigured(): Boolean = socksProxy != null || hostMode == HostMode.Onion + fun hasOnionConfigured(): Boolean = networkProxy != null || legacySocksProxy != null || hostMode == HostMode.Onion fun transformToPlatformSupported(): NetworkConfig { return if (hostMode != null && requiredHostMode != null) { NetworkConfig( - socksProxy = if (hostMode == HostMode.Onion) socksProxy ?: NetCfg.proxyDefaults.socksProxy else socksProxy, + legacySocksProxy = if (hostMode == HostMode.Onion) legacySocksProxy ?: NetCfg.proxyDefaults.socksProxy else legacySocksProxy, + networkProxy = if (hostMode == HostMode.Onion) networkProxy ?: NetworkProxy() else networkProxy, hostMode = if (hostMode == HostMode.Onion) HostMode.OnionViaSocks else hostMode, requiredHostMode = requiredHostMode ) @@ -570,7 +573,8 @@ private fun MutableState.startUploading( val cfg = getNetCfg() val data = MigrationFileLinkData( networkConfig = MigrationFileLinkData.NetworkConfig( - socksProxy = cfg.socksProxy, + legacySocksProxy = null, + networkProxy = if (appPrefs.networkUseSocksProxy.get()) appPrefs.networkProxy.get() else null, hostMode = cfg.hostMode, requiredHostMode = cfg.requiredHostMode ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt index e702fa6327..71cf4613ce 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt @@ -5,16 +5,15 @@ import SectionItemView import SectionSpacer import SectionTextFooter import SectionView -import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalClipboardManager import chat.simplex.common.model.* import chat.simplex.common.model.AppPreferences.Companion.SHARED_PREFS_MIGRATION_TO_STAGE +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatController.getNetCfg import chat.simplex.common.model.ChatController.startChat import chat.simplex.common.model.ChatCtrl @@ -91,7 +90,15 @@ sealed class MigrationToDeviceState { @Serializable sealed class MigrationToState { @Serializable object PasteOrScanLink: MigrationToState() - @Serializable data class Onion(val link: String, val socksProxy: String?, val hostMode: HostMode, val requiredHostMode: Boolean): MigrationToState() + @Serializable data class Onion( + val link: String, + // Legacy, remove in 2025 + @SerialName("socksProxy") + val legacySocksProxy: String?, + val networkProxy: NetworkProxy?, + val hostMode: HostMode, + val requiredHostMode: Boolean + ): MigrationToState() @Serializable data class DatabaseInit(val link: String, val netCfg: NetCfg): MigrationToState() @Serializable data class LinkDownloading(val link: String, val ctrl: ChatCtrl, val user: User, val archivePath: String, val netCfg: NetCfg): MigrationToState() @Serializable data class DownloadProgress(val downloadedBytes: Long, val totalBytes: Long, val fileId: Long, val link: String, val archivePath: String, val netCfg: NetCfg, val ctrl: ChatCtrl?): MigrationToState() @@ -175,7 +182,7 @@ private fun ModalData.SectionByState( when (val s = migrationState.value) { null -> {} is MigrationToState.PasteOrScanLink -> migrationState.PasteOrScanLinkView() - is MigrationToState.Onion -> OnionView(s.link, s.socksProxy, s.hostMode, s.requiredHostMode, migrationState) + is MigrationToState.Onion -> OnionView(s.link, s.legacySocksProxy, s.networkProxy, s.hostMode, s.requiredHostMode, migrationState) is MigrationToState.DatabaseInit -> migrationState.DatabaseInitView(s.link, tempDatabaseFile, s.netCfg) is MigrationToState.LinkDownloading -> migrationState.LinkDownloadingView(s.link, s.ctrl, s.user, s.archivePath, tempDatabaseFile, chatReceiver, s.netCfg) is MigrationToState.DownloadProgress -> DownloadProgressView(s.downloadedBytes, totalBytes = s.totalBytes) @@ -216,21 +223,24 @@ private fun MutableState.PasteLinkView() { } @Composable -private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: HostMode, requiredHostMode: Boolean, state: MutableState) { +private fun ModalData.OnionView(link: String, legacyLinkSocksProxy: String?, linkNetworkProxy: NetworkProxy?, hostMode: HostMode, requiredHostMode: Boolean, state: MutableState) { val onionHosts = remember { stateGetOrPut("onionHosts") { - getNetCfg().copy(socksProxy = socksProxy, hostMode = hostMode, requiredHostMode = requiredHostMode).onionHosts + getNetCfg().copy(socksProxy = linkNetworkProxy?.toProxyString() ?: legacyLinkSocksProxy, hostMode = hostMode, requiredHostMode = requiredHostMode).onionHosts } } - val networkUseSocksProxy = remember { stateGetOrPut("networkUseSocksProxy") { socksProxy != null } } + val networkUseSocksProxy = remember { stateGetOrPut("networkUseSocksProxy") { linkNetworkProxy != null || legacyLinkSocksProxy != null } } val sessionMode = remember { stateGetOrPut("sessionMode") { TransportSessionMode.User} } - val networkProxyHostPort = remember { stateGetOrPut("networkHostProxyPort") { - var proxy = (socksProxy ?: chatModel.controller.appPrefs.networkProxyHostPort.get()) - if (proxy?.startsWith(":") == true) proxy = "localhost$proxy" - proxy - } + val networkProxy = remember { stateGetOrPut("networkProxy") { + linkNetworkProxy + ?: if (legacyLinkSocksProxy != null) { + NetworkProxy(host = legacyLinkSocksProxy.substringBefore(":").ifBlank { "localhost" }, port = legacyLinkSocksProxy.substringAfter(":").toIntOrNull() ?: 9050) + } else { + appPrefs.networkProxy.get() + } + } } val netCfg = rememberSaveable(stateSaver = serializableSaver()) { - mutableStateOf(getNetCfg().withOnionHosts(onionHosts.value).copy(socksProxy = socksProxy, sessionMode = sessionMode.value)) + mutableStateOf(getNetCfg().withOnionHosts(onionHosts.value).copy(socksProxy = linkNetworkProxy?.toProxyString() ?: legacyLinkSocksProxy, sessionMode = sessionMode.value)) } SectionView(stringResource(MR.strings.migrate_to_device_confirm_network_settings).uppercase()) { @@ -241,7 +251,7 @@ private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: Hos click = { val updated = netCfg.value .withOnionHosts(onionHosts.value) - .withHostPort(if (networkUseSocksProxy.value) networkProxyHostPort.value else null, null) + .withProxy(if (networkUseSocksProxy.value) networkProxy.value else null, null) .copy( sessionMode = sessionMode.value ) @@ -255,8 +265,8 @@ private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: Hos SectionSpacer() - val networkProxyHostPortPref = SharedPreference(get = { networkProxyHostPort.value }, set = { - networkProxyHostPort.value = it + val networkProxyPref = SharedPreference(get = { networkProxy.value }, set = { + networkProxy.value = it }) SectionView(stringResource(MR.strings.network_settings_title).uppercase()) { OnionRelatedLayout( @@ -264,7 +274,7 @@ private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: Hos networkUseSocksProxy, onionHosts, sessionMode, - networkProxyHostPortPref, + networkProxyPref, toggleSocksProxy = { enable -> networkUseSocksProxy.value = enable }, @@ -477,12 +487,12 @@ private suspend fun MutableState.checkUserLink(link: String) val networkConfig = data?.networkConfig?.transformToPlatformSupported() // If any of iOS or Android had onion enabled, show onion screen if (hasOnionConfigured && networkConfig?.hostMode != null && networkConfig.requiredHostMode != null) { - state = MigrationToState.Onion(link.trim(), networkConfig.socksProxy, networkConfig.hostMode, networkConfig.requiredHostMode) - MigrationToDeviceState.save(MigrationToDeviceState.Onion(link.trim(), networkConfig.socksProxy, networkConfig.hostMode, networkConfig.requiredHostMode)) + state = MigrationToState.Onion(link.trim(), networkConfig.legacySocksProxy, networkConfig.networkProxy, networkConfig.hostMode, networkConfig.requiredHostMode) + MigrationToDeviceState.save(MigrationToDeviceState.Onion(link.trim(), networkConfig.legacySocksProxy, networkConfig.hostMode, networkConfig.requiredHostMode)) } else { val current = getNetCfg() state = MigrationToState.DatabaseInit(link.trim(), current.copy( - socksProxy = networkConfig?.socksProxy, + socksProxy = networkConfig?.legacySocksProxy, hostMode = networkConfig?.hostMode ?: current.hostMode, requiredHostMode = networkConfig?.requiredHostMode ?: current.requiredHostMode )) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt index 8904904f9c..21db832385 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt @@ -29,7 +29,6 @@ import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.res.MR -import kotlinx.serialization.Serializable @Composable fun NetworkAndServersView() { @@ -38,7 +37,7 @@ fun NetworkAndServersView() { val netCfg = remember { chatModel.controller.getNetCfg() } val networkUseSocksProxy: MutableState = remember { mutableStateOf(netCfg.useSocksProxy) } - val proxyPort = remember { derivedStateOf { ProxyComponents.from(appPrefs.networkProxyHostPort.state.value).port } } + val proxyPort = remember { derivedStateOf { appPrefs.networkProxy.state.value.port } } NetworkAndServersLayout( currentRemoteHost = currentRemoteHost, networkUseSocksProxy = networkUseSocksProxy, @@ -53,7 +52,7 @@ fun NetworkAndServersView() { confirmText = generalGetString(MR.strings.confirm_verb), onConfirm = { withBGApi { - var conf = controller.getNetCfg().withHostPort(controller.appPrefs.networkProxyHostPort.get()) + var conf = controller.getNetCfg().withProxy(controller.appPrefs.networkProxy.get()) if (conf.tcpConnectTimeout == def.tcpConnectTimeout) { conf = conf.copy(tcpConnectTimeout = proxyDef.tcpConnectTimeout) } @@ -123,7 +122,7 @@ fun NetworkAndServersView() { if (currentRemoteHost == null) { UseSocksProxySwitch(networkUseSocksProxy, toggleSocksProxy) - SettingsActionItem(painterResource(MR.images.ic_settings_ethernet), stringResource(MR.strings.network_socks_proxy_settings), { showCustomModal { SocksProxySettings(networkUseSocksProxy.value, appPrefs.networkProxyHostPort, onionHosts, sessionMode = appPrefs.networkSessionMode.get(), false, it) }}) + SettingsActionItem(painterResource(MR.images.ic_settings_ethernet), stringResource(MR.strings.network_socks_proxy_settings), { showCustomModal { SocksProxySettings(networkUseSocksProxy.value, appPrefs.networkProxy, onionHosts, sessionMode = appPrefs.networkSessionMode.get(), false, it) }}) SettingsActionItem(painterResource(MR.images.ic_cable), stringResource(MR.strings.network_settings), { ModalManager.start.showCustomModal { AdvancedNetworkSettingsView(showModal, it) } }) if (networkUseSocksProxy.value) { SectionTextFooter(annotatedStringResource(MR.strings.socks_proxy_setting_limitations)) @@ -157,14 +156,14 @@ fun NetworkAndServersView() { networkUseSocksProxy: MutableState, onionHosts: MutableState, sessionMode: MutableState, - networkProxyHostPort: SharedPreference, + networkProxy: SharedPreference, toggleSocksProxy: (Boolean) -> Unit, updateSessionMode: (TransportSessionMode) -> Unit, ) { val showModal = { it: @Composable ModalData.() -> Unit -> ModalManager.fullscreen.showModal(content = it) } val showCustomModal = { it: @Composable (close: () -> Unit) -> Unit -> ModalManager.fullscreen.showCustomModal { close -> it(close) }} UseSocksProxySwitch(networkUseSocksProxy, toggleSocksProxy) - SettingsActionItem(painterResource(MR.images.ic_settings_ethernet), stringResource(MR.strings.network_socks_proxy_settings), { showCustomModal { SocksProxySettings(networkUseSocksProxy.value, networkProxyHostPort, onionHosts, sessionMode.value, true, it) } }) + SettingsActionItem(painterResource(MR.images.ic_settings_ethernet), stringResource(MR.strings.network_socks_proxy_settings), { showCustomModal { SocksProxySettings(networkUseSocksProxy.value, networkProxy, onionHosts, sessionMode.value, true, it) } }) if (developerTools) { SessionModePicker(sessionMode, showModal, updateSessionMode) } @@ -202,30 +201,28 @@ fun UseSocksProxySwitch( @Composable fun SocksProxySettings( networkUseSocksProxy: Boolean, - networkProxyHostPort: SharedPreference = appPrefs.networkProxyHostPort, + networkProxy: SharedPreference, onionHosts: MutableState, sessionMode: TransportSessionMode, migration: Boolean, close: () -> Unit ) { - val defaultHostPort = remember { "localhost:9050" } - val proxyStringSaved by remember { networkProxyHostPort.state } - val proxyComponentsSaved by remember(proxyStringSaved) { mutableStateOf(ProxyComponents.from(proxyStringSaved)) } + val networkProxySaved by remember { networkProxy.state } val onionHostsSaved = remember { mutableStateOf(onionHosts.value) } val usernameUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) { - mutableStateOf(TextFieldValue(proxyComponentsSaved.usernamePassword.first)) + mutableStateOf(TextFieldValue(networkProxySaved.username)) } val passwordUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) { - mutableStateOf(TextFieldValue(proxyComponentsSaved.usernamePassword.second)) + mutableStateOf(TextFieldValue(networkProxySaved.password)) } val hostUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) { - mutableStateOf(TextFieldValue(proxyComponentsSaved.host)) + mutableStateOf(TextFieldValue(networkProxySaved.host)) } val portUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) { - mutableStateOf(TextFieldValue(proxyComponentsSaved.port.toString())) + mutableStateOf(TextFieldValue(networkProxySaved.port.toString())) } - val proxyAuthRandomUnsaved = rememberSaveable { mutableStateOf(proxyComponentsSaved.authMode == ProxyAuthenticationMode.ISOLATE_BY_AUTH) } + val proxyAuthRandomUnsaved = rememberSaveable { mutableStateOf(networkProxySaved.auth == NetworkProxyAuth.ISOLATE) } LaunchedEffect(proxyAuthRandomUnsaved.value) { if (!proxyAuthRandomUnsaved.value && onionHosts.value != OnionHosts.NEVER) { onionHosts.value = OnionHosts.NEVER @@ -234,24 +231,28 @@ fun SocksProxySettings( val proxyAuthModeUnsaved = remember(proxyAuthRandomUnsaved.value, usernameUnsaved.value.text, passwordUnsaved.value.text) { derivedStateOf { if (proxyAuthRandomUnsaved.value) { - ProxyAuthenticationMode.ISOLATE_BY_AUTH - } else if (usernameUnsaved.value.text.isBlank() && passwordUnsaved.value.text.isBlank()) { - ProxyAuthenticationMode.NO_AUTH + NetworkProxyAuth.ISOLATE } else { - ProxyAuthenticationMode.USERNAME_PASSWORD + NetworkProxyAuth.USERNAME } } } val save: (Boolean) -> Unit = { closeOnSuccess -> - val oldValue = networkProxyHostPort.get() - networkProxyHostPort.set( - ProxyComponents( - usernamePassword = usernameUnsaved.value.text to passwordUnsaved.value.text, + val oldValue = networkProxy.get() + usernameUnsaved.value = usernameUnsaved.value.copy(if (proxyAuthModeUnsaved.value == NetworkProxyAuth.USERNAME) usernameUnsaved.value.text.trim() else "") + passwordUnsaved.value = passwordUnsaved.value.copy(if (proxyAuthModeUnsaved.value == NetworkProxyAuth.USERNAME) passwordUnsaved.value.text.trim() else "") + hostUnsaved.value = hostUnsaved.value.copy(hostUnsaved.value.text.trim()) + portUnsaved.value = portUnsaved.value.copy(portUnsaved.value.text.trim()) + + networkProxy.set( + NetworkProxy( + username = usernameUnsaved.value.text, + password = passwordUnsaved.value.text, host = hostUnsaved.value.text, - port = portUnsaved.value.text.trim().toIntOrNull() ?: 9050, - authMode = proxyAuthModeUnsaved.value - ).toProxyString() + port = portUnsaved.value.text.toIntOrNull() ?: 9050, + auth = proxyAuthModeUnsaved.value + ) ) val oldCfg = controller.getNetCfg() val cfg = oldCfg.withOnionHosts(onionHosts.value) @@ -264,12 +265,6 @@ fun SocksProxySettings( if (networkUseSocksProxy && !migration) { withBGApi { if (controller.apiSetNetworkConfig(cfg, showAlertOnError = false)) { - val comp = ProxyComponents.from(networkProxyHostPort.get()) - usernameUnsaved.value = usernameUnsaved.value.copy(comp.usernamePassword.first) - passwordUnsaved.value = passwordUnsaved.value.copy(comp.usernamePassword.second) - hostUnsaved.value = hostUnsaved.value.copy(comp.host) - portUnsaved.value = portUnsaved.value.copy(comp.port.toString()) - proxyAuthRandomUnsaved.value = comp.authMode == ProxyAuthenticationMode.ISOLATE_BY_AUTH onionHosts.value = cfg.onionHosts onionHostsSaved.value = onionHosts.value if (closeOnSuccess) { @@ -277,7 +272,7 @@ fun SocksProxySettings( } } else { controller.setNetCfg(oldCfg) - networkProxyHostPort.set(oldValue) + networkProxy.set(oldValue) onionHostsSaved.value = oldOnionHosts showWrongProxyConfigAlert() } @@ -286,14 +281,15 @@ fun SocksProxySettings( } val saveDisabled = ( - proxyComponentsSaved.usernamePassword == usernameUnsaved.value.text.trim() to passwordUnsaved.value.text.trim() && - proxyComponentsSaved.host == hostUnsaved.value.text.trim() && - proxyComponentsSaved.port.toString() == portUnsaved.value.text.trim() && - proxyComponentsSaved.authMode == proxyAuthModeUnsaved.value && + networkProxySaved.username == usernameUnsaved.value.text.trim() && + networkProxySaved.password == passwordUnsaved.value.text.trim() && + networkProxySaved.host == hostUnsaved.value.text.trim() && + networkProxySaved.port.toString() == portUnsaved.value.text.trim() && + networkProxySaved.auth == proxyAuthModeUnsaved.value && onionHosts.value == onionHostsSaved.value ) || remember { derivedStateOf { !validPort(portUnsaved.value.text) } }.value - val resetDisabled = hostUnsaved.value.text.trim() + ":" + portUnsaved.value.text.trim() == defaultHostPort && proxyAuthRandomUnsaved.value && onionHosts.value == NetCfg.defaults.onionHosts + val resetDisabled = hostUnsaved.value.text.trim() == "localhost" && portUnsaved.value.text.trim() == "9050" && proxyAuthRandomUnsaved.value && onionHosts.value == NetCfg.defaults.onionHosts ModalView( close = { if (saveDisabled) { @@ -366,17 +362,15 @@ fun SocksProxySettings( ) } } - SectionTextFooter(proxyAuthModeUnsaved.value.text(sessionMode)) + SectionTextFooter(proxyAuthFooter(usernameUnsaved.value.text, passwordUnsaved.value.text, proxyAuthModeUnsaved.value, sessionMode)) } SectionDividerSpaced(maxBottomPadding = false, maxTopPadding = true) SectionView { SectionItemView({ - val newHost = defaultHostPort.split(":").first() - val newPort = defaultHostPort.split(":").last() - hostUnsaved.value = hostUnsaved.value.copy(newHost, TextRange(newHost.length)) - portUnsaved.value = portUnsaved.value.copy(newPort, TextRange(newPort.length)) + hostUnsaved.value = hostUnsaved.value.copy("localhost", TextRange(9)) + portUnsaved.value = portUnsaved.value.copy("9050", TextRange(4)) usernameUnsaved.value = TextFieldValue() passwordUnsaved.value = TextFieldValue() proxyAuthRandomUnsaved.value = true @@ -396,76 +390,10 @@ fun SocksProxySettings( } } -@Serializable -private data class ProxyComponents( - val usernamePassword: Pair, - val host: String, - val port: Int, - val authMode: ProxyAuthenticationMode -) { - fun toProxyString(): String? { - var res = "" - if (authMode == ProxyAuthenticationMode.USERNAME_PASSWORD && (usernamePassword.first.isNotBlank() || usernamePassword.second.isNotBlank())) { - res += usernamePassword.first.trim() + ":" + usernamePassword.second.trim() + "@" - } else if (authMode == ProxyAuthenticationMode.USERNAME_PASSWORD || authMode == ProxyAuthenticationMode.NO_AUTH) { - res += "@" - } - if (host != "localhost") { - res += if (host.contains(':')) "[${host.trim(' ', '[', ']')}]" else host.trim() - } - if (port != 9050) { - res += ":$port" - } - return res.ifBlank { null } - } - companion object { - fun from(proxy: String?): ProxyComponents { - if (proxy == null) { - return ProxyComponents(usernamePassword = "" to "", host = "localhost", port = 9050, authMode = ProxyAuthenticationMode.ISOLATE_BY_AUTH) - } - val username = if (proxy.contains("@")) proxy.substringBefore("@").substringBefore(":") else "" - val password = if (proxy.contains("@")) proxy.substringBefore("@").substringAfter(":") else "" - val hostPort = proxy.substringAfter("@") - val host: String? - val port: Int? - if (hostPort.contains("[") && hostPort.contains("]")) { - // ipv6 with or without port - host = hostPort.substringBefore("]") + "]" - port = hostPort.substringAfter("]").trim(':').toIntOrNull() - } else { - // ipv4 with or without port - host = hostPort.substringBefore(":") - port = hostPort.substringAfter(":").toIntOrNull() - } - return ProxyComponents( - usernamePassword = username to password, - host = host.ifBlank { "localhost" }, - port = port ?: 9050, - authMode = ProxyAuthenticationMode.from(proxy) - ) - } - } -} - -enum class ProxyAuthenticationMode { - ISOLATE_BY_AUTH, - NO_AUTH, - USERNAME_PASSWORD; - - fun text(sessionMode: TransportSessionMode): String = when (this) { - ISOLATE_BY_AUTH -> generalGetString(if (sessionMode == TransportSessionMode.User) MR.strings.network_proxy_auth_mode_isolate_by_auth_user else MR.strings.network_proxy_auth_mode_isolate_by_auth_entity) - NO_AUTH -> generalGetString(MR.strings.network_proxy_auth_mode_no_auth) - USERNAME_PASSWORD -> generalGetString(MR.strings.network_proxy_auth_mode_username_password) - } - - companion object { - fun from(proxy: String?): ProxyAuthenticationMode = when { - proxy.isNullOrEmpty() -> ISOLATE_BY_AUTH - !proxy.contains("@") -> ISOLATE_BY_AUTH - proxy.startsWith("@") -> NO_AUTH - else -> USERNAME_PASSWORD - } - } +private fun proxyAuthFooter(username: String, password: String, auth: NetworkProxyAuth, sessionMode: TransportSessionMode): String = when { + auth == NetworkProxyAuth.ISOLATE -> generalGetString(if (sessionMode == TransportSessionMode.User) MR.strings.network_proxy_auth_mode_isolate_by_auth_user else MR.strings.network_proxy_auth_mode_isolate_by_auth_entity) + username.isBlank() && password.isBlank() -> generalGetString(MR.strings.network_proxy_auth_mode_no_auth) + else -> generalGetString(MR.strings.network_proxy_auth_mode_username_password) } private fun showUnsavedSocksHostPortAlert(confirmText: String, save: () -> Unit, close: () -> Unit) {