From 378118b82e44490c050e83ad0dd6d63ad40f5da7 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Wed, 31 Aug 2022 00:24:33 +0300 Subject: [PATCH] Options when using .onion hosts (#989) * Options when using .onion hosts * Confirmation alert before applying network settings * Useless new line was removed * Different ordering of options in enum --- .../java/chat/simplex/app/model/SimpleXAPI.kt | 30 ++++ .../views/usersettings/NetworkAndServers.kt | 157 +++++++++++++++++- .../app/src/main/res/values-ru/strings.xml | 4 + .../app/src/main/res/values/strings.xml | 4 + 4 files changed, 188 insertions(+), 7 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 95da5ff79..30da6db48 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -88,6 +88,8 @@ class AppPreferences(val context: Context) { val chatLastStart = mkDatePreference(SHARED_PREFS_CHAT_LAST_START, null) val developerTools = mkBoolPreference(SHARED_PREFS_DEVELOPER_TOOLS, false) val networkUseSocksProxy = mkBoolPreference(SHARED_PREFS_NETWORK_USE_SOCKS_PROXY, false) + val networkHostMode = mkStrPreference(SHARED_PREFS_NETWORK_HOST_MODE, HostMode.OnionViaSocks.name) + val networkRequiredHostMode = mkBoolPreference(SHARED_PREFS_NETWORK_REQUIRED_HOST_MODE, false) val networkTCPConnectTimeout = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT, NetCfg.defaults.tcpConnectTimeout, NetCfg.proxyDefaults.tcpConnectTimeout) val networkTCPTimeout = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_TIMEOUT, NetCfg.defaults.tcpTimeout, NetCfg.proxyDefaults.tcpTimeout) val networkSMPPingInterval = mkLongPreference(SHARED_PREFS_NETWORK_SMP_PING_INTERVAL, NetCfg.defaults.smpPingInterval) @@ -159,6 +161,8 @@ class AppPreferences(val context: Context) { private const val SHARED_PREFS_CHAT_LAST_START = "ChatLastStart" private const val SHARED_PREFS_DEVELOPER_TOOLS = "DeveloperTools" private const val SHARED_PREFS_NETWORK_USE_SOCKS_PROXY = "NetworkUseSocksProxy" + private const val SHARED_PREFS_NETWORK_HOST_MODE = "NetworkHostMode" + private const val SHARED_PREFS_NETWORK_REQUIRED_HOST_MODE = "NetworkRequiredHostMode" private const val SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT = "NetworkTCPConnectTimeout" private const val SHARED_PREFS_NETWORK_TCP_TIMEOUT = "NetworkTCPTimeout" private const val SHARED_PREFS_NETWORK_SMP_PING_INTERVAL = "NetworkSMPPingInterval" @@ -1103,6 +1107,8 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager fun getNetCfg(): NetCfg { val useSocksProxy = appPrefs.networkUseSocksProxy.get() val socksProxy = if (useSocksProxy) ":9050" else null + val hostMode = HostMode.valueOf(appPrefs.networkHostMode.get()!!) + val requiredHostMode = appPrefs.networkRequiredHostMode.get() val tcpConnectTimeout = appPrefs.networkTCPConnectTimeout.get() val tcpTimeout = appPrefs.networkTCPTimeout.get() val smpPingInterval = appPrefs.networkSMPPingInterval.get() @@ -1117,6 +1123,8 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager } return NetCfg( socksProxy = socksProxy, + hostMode = hostMode, + requiredHostMode = requiredHostMode, tcpConnectTimeout = tcpConnectTimeout, tcpTimeout = tcpTimeout, tcpKeepAlive = tcpKeepAlive, @@ -1126,6 +1134,8 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager fun setNetCfg(cfg: NetCfg) { appPrefs.networkUseSocksProxy.set(cfg.useSocksProxy) + appPrefs.networkHostMode.set(cfg.hostMode.name) + appPrefs.networkRequiredHostMode.set(cfg.requiredHostMode) appPrefs.networkTCPConnectTimeout.set(cfg.tcpConnectTimeout) appPrefs.networkTCPTimeout.set(cfg.tcpTimeout) appPrefs.networkSMPPingInterval.set(cfg.smpPingInterval) @@ -1370,6 +1380,26 @@ data class NetCfg( smpPingInterval = 600_000_000 ) } + + val onionHosts: OnionHosts get() = when { + hostMode == HostMode.Public && requiredHostMode -> OnionHosts.NEVER + hostMode == HostMode.OnionViaSocks && !requiredHostMode -> OnionHosts.PREFER + hostMode == HostMode.OnionViaSocks && requiredHostMode -> OnionHosts.REQUIRED + else -> OnionHosts.PREFER + } + + fun withOnionHosts(mode: OnionHosts): NetCfg = when (mode) { + OnionHosts.NEVER -> + this.copy(hostMode = HostMode.Public, requiredHostMode = true) + OnionHosts.PREFER -> + this.copy(hostMode = HostMode.OnionViaSocks, requiredHostMode = false) + OnionHosts.REQUIRED -> + this.copy(hostMode = HostMode.OnionViaSocks, requiredHostMode = true) + } +} + +enum class OnionHosts { + NEVER, PREFER, REQUIRED } @Serializable diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/NetworkAndServers.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/NetworkAndServers.kt index 0ac31464f..a6fd33168 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/NetworkAndServers.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/NetworkAndServers.kt @@ -10,12 +10,14 @@ import androidx.compose.material.icons.outlined.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import chat.simplex.app.R -import chat.simplex.app.model.ChatModel -import chat.simplex.app.model.NetCfg +import chat.simplex.app.model.* import chat.simplex.app.ui.theme.* import chat.simplex.app.views.helpers.* @@ -25,16 +27,19 @@ fun NetworkAndServersView( showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit) ) { - val netCfg: MutableState = remember { mutableStateOf(chatModel.controller.getNetCfg()) } - val networkUseSocksProxy: MutableState = remember { mutableStateOf(netCfg.value.useSocksProxy) } + // It's not a state, just a one-time value. Shouldn't be used in any state-related situations + val netCfg = remember { chatModel.controller.getNetCfg() } + val networkUseSocksProxy: MutableState = remember { mutableStateOf(netCfg.useSocksProxy) } val developerTools = chatModel.controller.appPrefs.developerTools.get() + val onionHosts = remember { mutableStateOf(netCfg.onionHosts) } NetworkAndServersLayout( developerTools = developerTools, networkUseSocksProxy = networkUseSocksProxy, + onionHosts = onionHosts, showModal = showModal, showSettingsModal = showSettingsModal, - toggleSocksProxy = { enable -> + toggleSocksProxy = { enable -> if (enable) { AlertManager.shared.showAlertMsg( title = generalGetString(R.string.network_enable_socks), @@ -45,6 +50,7 @@ fun NetworkAndServersView( chatModel.controller.apiSetNetworkConfig(NetCfg.proxyDefaults) chatModel.controller.setNetCfg(NetCfg.proxyDefaults) networkUseSocksProxy.value = true + onionHosts.value = NetCfg.proxyDefaults.onionHosts } } ) @@ -58,10 +64,29 @@ fun NetworkAndServersView( chatModel.controller.apiSetNetworkConfig(NetCfg.defaults) chatModel.controller.setNetCfg(NetCfg.defaults) networkUseSocksProxy.value = false + onionHosts.value = NetCfg.defaults.onionHosts } } ) } + }, + useOnion = { + val prevValue = onionHosts.value + onionHosts.value = it + updateNetworkSettingsDialog(onDismiss = { + onionHosts.value = prevValue + }) { + withApi { + val newCfg = chatModel.controller.getNetCfg().withOnionHosts(it) + val res = chatModel.controller.apiSetNetworkConfig(newCfg) + if (res) { + chatModel.controller.setNetCfg(newCfg) + onionHosts.value = it + } else { + onionHosts.value = prevValue + } + } + } } ) } @@ -69,9 +94,11 @@ fun NetworkAndServersView( @Composable fun NetworkAndServersLayout( developerTools: Boolean, networkUseSocksProxy: MutableState, + onionHosts: MutableState, showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), - toggleSocksProxy: (Boolean) -> Unit + toggleSocksProxy: (Boolean) -> Unit, + useOnion: (OnionHosts) -> Unit, ) { Column( Modifier.fillMaxWidth(), @@ -89,6 +116,10 @@ fun NetworkAndServersView( SectionItemView { UseSocksProxySwitch(networkUseSocksProxy, toggleSocksProxy) } + SectionDivider() + SectionItemView { + UseOnionHosts(onionHosts, networkUseSocksProxy, useOnion) + } if (developerTools) { SectionDivider() SettingsActionItem(Icons.Outlined.Cable, stringResource(R.string.network_settings), showSettingsModal { AdvancedNetworkSettingsView(it) }) @@ -129,6 +160,116 @@ fun UseSocksProxySwitch( } } +@Composable +private fun UseOnionHosts(onionHosts: MutableState, enabled: State, useOnion: (OnionHosts) -> Unit) { + val values = remember { + OnionHosts.values().map { + when (it) { + OnionHosts.NEVER -> OnionHosts.NEVER to generalGetString(R.string.network_use_onion_hosts_no) + OnionHosts.PREFER -> OnionHosts.PREFER to generalGetString(R.string.network_use_onion_hosts_prefer) + OnionHosts.REQUIRED -> OnionHosts.REQUIRED to generalGetString(R.string.network_use_onion_hosts_required) + } + } + } + ExposedDropDownSettingRow( + generalGetString(R.string.network_use_onion_hosts), + values, + onionHosts, + icon = Icons.Outlined.Security, + enabled = enabled, + onSelected = useOnion + ) +} + +@Composable +fun ExposedDropDownSettingRow( + title: String, + values: List>, + selection: State, + label: String? = null, + icon: ImageVector? = null, + iconTint: Color = HighOrLowlight, + enabled: State = mutableStateOf(true), + onSelected: (T) -> Unit +) { + Row( + Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + var expanded by remember { mutableStateOf(false) } + + if (icon != null) { + Icon( + icon, + "", + Modifier.padding(end = 8.dp), + tint = iconTint + ) + } + Text(title, color = if (enabled.value) Color.Unspecified else HighOrLowlight) + + Spacer(Modifier.fillMaxWidth().weight(1f)) + + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { + expanded = !expanded && enabled.value + } + ) { + Row( + Modifier.padding(start = 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End + ) { + Text( + values.first { it.first == selection.value }.second + (if (label != null) " $label" else ""), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = HighOrLowlight + ) + Spacer(Modifier.size(12.dp)) + Icon( + if (!expanded) Icons.Outlined.ExpandMore else Icons.Outlined.ExpandLess, + generalGetString(R.string.icon_descr_more_button), + tint = HighOrLowlight + ) + } + ExposedDropdownMenu( + modifier = Modifier.widthIn(min = 200.dp), + expanded = expanded, + onDismissRequest = { + expanded = false + } + ) { + values.forEach { selectionOption -> + DropdownMenuItem( + onClick = { + onSelected(selectionOption.first) + expanded = false + } + ) { + Text( + selectionOption.second + (if (label != null) " $label" else ""), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } + } + } +} + +private fun updateNetworkSettingsDialog(onDismiss: () -> Unit, onConfirm: () -> Unit) { + AlertManager.shared.showAlertDialog( + title = generalGetString(R.string.update_network_settings_question), + text = generalGetString(R.string.updating_settings_will_reconnect_client_to_all_servers), + confirmText = generalGetString(R.string.update_network_settings_confirmation), + onDismiss = onDismiss, + onConfirm = onConfirm, + ) +} + @Preview(showBackground = true) @Composable fun PreviewNetworkAndServersLayout() { @@ -138,7 +279,9 @@ fun PreviewNetworkAndServersLayout() { networkUseSocksProxy = remember { mutableStateOf(true) }, showModal = { {} }, showSettingsModal = { {} }, - toggleSocksProxy = {} + toggleSocksProxy = {}, + onionHosts = remember { mutableStateOf(OnionHosts.PREFER) }, + useOnion = {}, ) } } diff --git a/apps/android/app/src/main/res/values-ru/strings.xml b/apps/android/app/src/main/res/values-ru/strings.xml index 960e274fa..928cf18d0 100644 --- a/apps/android/app/src/main/res/values-ru/strings.xml +++ b/apps/android/app/src/main/res/values-ru/strings.xml @@ -297,6 +297,10 @@ Соединяться с серверами через SOCKS прокси через порт 9050? Прокси должен быть запущен до включения этой опции. Использовать прямое соединение с Интернет? Если вы подтвердите, серверы смогут видеть ваш IP адрес, а провайдер - с какими серверами вы соединяетесь. + Использовать .onion хосты + Когда возможно + Нет + Обязательно Интерфейс diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index 5ce4e0556..9e3651b5d 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -301,6 +301,10 @@ Access the servers via SOCKS proxy on port 9050? Proxy must be started before enabling this option. Use direct Internet connection? If you confirm, the messaging servers will be able to see your IP address, and your provider - which servers you are connecting to. + Use .onion hosts + When available + No + Required Appearance