android, desktop: proxy configuration includes credentials (#4892)

* android, desktop: proxy configuration includes credentials

* migration

* changes for disabled socks

* migration

* port

* new logic

* migration

* check validity of fields

* validity of host

* import changes proxy just in case

* send port always

* non-nullable

* Revert "send port always"

This reverts commit 14dd066d80.

* string

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
Stanislav Dmitrenko
2024-09-24 17:07:39 +01:00
committed by Evgeny Poberezkin
co-authored by Evgeny Poberezkin
parent 93ab3076d4
commit 5261886b31
6 changed files with 377 additions and 234 deletions
@@ -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<NetworkProxy> = 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<TransportSessionMode> = SharedPreference(
get = fun(): TransportSessionMode {
@@ -531,7 +546,7 @@ object ChatController {
suspend fun startChatWithTemporaryDatabase(ctrl: ChatCtrl, netCfg: NetCfg): User? {
Log.d(TAG, "startChatWithTemporaryDatabase")
val migrationActiveUser = apiGetActiveUser(null, ctrl) ?: apiCreateActiveUser(null, Profile(displayName = "Temp", fullName = ""), ctrl = ctrl)
if (!apiSetNetworkConfig(netCfg, ctrl)) {
if (!apiSetNetworkConfig(netCfg, ctrl = ctrl)) {
Log.e(TAG, "Error setting network config, stopping migration")
return null
}
@@ -976,16 +991,18 @@ object ChatController {
throw Exception("failed to set chat item TTL: ${r.responseType} ${r.details}")
}
suspend fun apiSetNetworkConfig(cfg: NetCfg, ctrl: ChatCtrl? = null): Boolean {
suspend fun apiSetNetworkConfig(cfg: NetCfg, showAlertOnError: Boolean = true, ctrl: ChatCtrl? = null): Boolean {
val r = sendCmd(null, CC.APISetNetworkConfig(cfg), ctrl)
return when (r) {
is CR.CmdOk -> true
else -> {
Log.e(TAG, "apiSetNetworkConfig bad response: ${r.responseType} ${r.details}")
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.error_setting_network_config),
"${r.responseType}: ${r.details}"
)
if (showAlertOnError) {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.error_setting_network_config),
"${r.responseType}: ${r.details}"
)
}
false
}
}
@@ -2745,13 +2762,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
}
@@ -2793,7 +2806,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)
@@ -3529,13 +3542,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 {
@@ -3577,6 +3585,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
}
@@ -6139,6 +6180,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,
@@ -6170,6 +6212,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 }
@@ -6207,8 +6250,12 @@ data class AppSettings(
if (net.hostMode == HostMode.Onion) {
net = net.copy(hostMode = HostMode.Public, requiredHostMode = true)
}
if (net.socksProxy != null) {
net = net.copy(socksProxy = networkProxy?.toProxyString())
}
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) }
@@ -6241,6 +6288,7 @@ data class AppSettings(
val defaults: AppSettings
get() = AppSettings(
networkConfig = NetCfg.defaults,
networkProxy = null,
privacyEncryptLocalFiles = true,
privacyAskToApproveRelays = true,
privacyAcceptImages = true,
@@ -6274,6 +6322,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(),
@@ -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 hasProxyConfigured(): 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<MigrationFromState>.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
)
@@ -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
@@ -41,10 +40,10 @@ import kotlin.math.max
@Serializable
sealed class MigrationToDeviceState {
@Serializable @SerialName("onion") data class Onion(val link: String, val socksProxy: String?, val hostMode: HostMode, val requiredHostMode: Boolean): MigrationToDeviceState()
@Serializable @SerialName("downloadProgress") data class DownloadProgress(val link: String, val archiveName: String, val netCfg: NetCfg): MigrationToDeviceState()
@Serializable @SerialName("archiveImport") data class ArchiveImport(val archiveName: String, val netCfg: NetCfg): MigrationToDeviceState()
@Serializable @SerialName("passphrase") data class Passphrase(val netCfg: NetCfg): MigrationToDeviceState()
@Serializable @SerialName("onion") data class Onion(val link: String, val socksProxy: String?, val networkProxy: NetworkProxy?, val hostMode: HostMode, val requiredHostMode: Boolean): MigrationToDeviceState()
@Serializable @SerialName("downloadProgress") data class DownloadProgress(val link: String, val archiveName: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToDeviceState()
@Serializable @SerialName("archiveImport") data class ArchiveImport(val archiveName: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToDeviceState()
@Serializable @SerialName("passphrase") data class Passphrase(val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToDeviceState()
companion object {
// Here we check whether it's needed to show migration process after app restart or not
@@ -66,10 +65,10 @@ sealed class MigrationToDeviceState {
null
} else {
val archivePath = File(getMigrationTempFilesDirectory(), state.archiveName)
MigrationToState.ArchiveImportFailed(archivePath.absolutePath, state.netCfg)
MigrationToState.ArchiveImportFailed(archivePath.absolutePath, state.netCfg, state.networkProxy)
}
}
is Passphrase -> MigrationToState.Passphrase("", state.netCfg)
is Passphrase -> MigrationToState.Passphrase("", state.netCfg, state.networkProxy)
}
if (initial == null) {
settings.remove(SHARED_PREFS_MIGRATION_TO_STAGE)
@@ -91,16 +90,24 @@ 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 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()
@Serializable data class DownloadFailed(val totalBytes: Long, val link: String, val archivePath: String, val netCfg: NetCfg): MigrationToState()
@Serializable data class ArchiveImport(val archivePath: String, val netCfg: NetCfg): MigrationToState()
@Serializable data class ArchiveImportFailed(val archivePath: String, val netCfg: NetCfg): MigrationToState()
@Serializable data class Passphrase(val passphrase: String, val netCfg: NetCfg): MigrationToState()
@Serializable data class MigrationConfirmation(val status: DBMigrationResult, val passphrase: String, val useKeychain: Boolean, val netCfg: NetCfg): MigrationToState()
@Serializable data class Migration(val passphrase: String, val confirmation: chat.simplex.common.views.helpers.MigrationConfirmation, val useKeychain: Boolean, val netCfg: NetCfg): 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, val networkProxy: NetworkProxy?): MigrationToState()
@Serializable data class LinkDownloading(val link: String, val ctrl: ChatCtrl, val user: User, val archivePath: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState()
@Serializable data class DownloadProgress(val downloadedBytes: Long, val totalBytes: Long, val fileId: Long, val link: String, val archivePath: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?, val ctrl: ChatCtrl?): MigrationToState()
@Serializable data class DownloadFailed(val totalBytes: Long, val link: String, val archivePath: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState()
@Serializable data class ArchiveImport(val archivePath: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState()
@Serializable data class ArchiveImportFailed(val archivePath: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState()
@Serializable data class Passphrase(val passphrase: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState()
@Serializable data class MigrationConfirmation(val status: DBMigrationResult, val passphrase: String, val useKeychain: Boolean, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState()
@Serializable data class Migration(val passphrase: String, val confirmation: chat.simplex.common.views.helpers.MigrationConfirmation, val useKeychain: Boolean, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState()
}
private var MutableState<MigrationToState?>.state: MigrationToState?
@@ -175,16 +182,16 @@ 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.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.Onion -> OnionView(s.link, s.legacySocksProxy, s.networkProxy, s.hostMode, s.requiredHostMode, migrationState)
is MigrationToState.DatabaseInit -> migrationState.DatabaseInitView(s.link, tempDatabaseFile, s.netCfg, s.networkProxy)
is MigrationToState.LinkDownloading -> migrationState.LinkDownloadingView(s.link, s.ctrl, s.user, s.archivePath, tempDatabaseFile, chatReceiver, s.netCfg, s.networkProxy)
is MigrationToState.DownloadProgress -> DownloadProgressView(s.downloadedBytes, totalBytes = s.totalBytes)
is MigrationToState.DownloadFailed -> migrationState.DownloadFailedView(s.link, chatReceiver.value, s.archivePath, s.netCfg)
is MigrationToState.ArchiveImport -> migrationState.ArchiveImportView(s.archivePath, s.netCfg)
is MigrationToState.ArchiveImportFailed -> migrationState.ArchiveImportFailedView(s.archivePath, s.netCfg)
is MigrationToState.Passphrase -> migrationState.PassphraseEnteringView(currentKey = s.passphrase, s.netCfg)
is MigrationToState.MigrationConfirmation -> migrationState.MigrationConfirmationView(s.status, s.passphrase, s.useKeychain, s.netCfg)
is MigrationToState.Migration -> MigrationView(s.passphrase, s.confirmation, s.useKeychain, s.netCfg, close)
is MigrationToState.DownloadFailed -> migrationState.DownloadFailedView(s.link, chatReceiver.value, s.archivePath, s.netCfg, s.networkProxy)
is MigrationToState.ArchiveImport -> migrationState.ArchiveImportView(s.archivePath, s.netCfg, s.networkProxy)
is MigrationToState.ArchiveImportFailed -> migrationState.ArchiveImportFailedView(s.archivePath, s.netCfg, s.networkProxy)
is MigrationToState.Passphrase -> migrationState.PassphraseEnteringView(currentKey = s.passphrase, s.netCfg, s.networkProxy)
is MigrationToState.MigrationConfirmation -> migrationState.MigrationConfirmationView(s.status, s.passphrase, s.useKeychain, s.netCfg, s.networkProxy)
is MigrationToState.Migration -> MigrationView(s.passphrase, s.confirmation, s.useKeychain, s.netCfg, s.networkProxy, close)
}
}
@@ -216,21 +223,24 @@ private fun MutableState<MigrationToState?>.PasteLinkView() {
}
@Composable
private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: HostMode, requiredHostMode: Boolean, state: MutableState<MigrationToState?>) {
private fun ModalData.OnionView(link: String, legacyLinkSocksProxy: String?, linkNetworkProxy: NetworkProxy?, hostMode: HostMode, requiredHostMode: Boolean, state: MutableState<MigrationToState?>) {
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,12 +251,12 @@ 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
)
withBGApi {
state.value = MigrationToState.DatabaseInit(link, updated)
state.value = MigrationToState.DatabaseInit(link, updated, if (networkUseSocksProxy.value) networkProxy.value else null)
}
}
){}
@@ -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,13 +274,10 @@ private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: Hos
networkUseSocksProxy,
onionHosts,
sessionMode,
networkProxyHostPortPref,
networkProxyPref,
toggleSocksProxy = { enable ->
networkUseSocksProxy.value = enable
},
useOnion = {
onionHosts.value = it
},
updateSessionMode = {
sessionMode.value = it
}
@@ -279,13 +286,13 @@ private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: Hos
}
@Composable
private fun MutableState<MigrationToState?>.DatabaseInitView(link: String, tempDatabaseFile: File, netCfg: NetCfg) {
private fun MutableState<MigrationToState?>.DatabaseInitView(link: String, tempDatabaseFile: File, netCfg: NetCfg, networkProxy: NetworkProxy?) {
Box {
SectionView(stringResource(MR.strings.migrate_to_device_database_init).uppercase()) {}
ProgressView()
}
LaunchedEffect(Unit) {
prepareDatabase(link, tempDatabaseFile, netCfg)
prepareDatabase(link, tempDatabaseFile, netCfg, networkProxy)
}
}
@@ -297,14 +304,15 @@ private fun MutableState<MigrationToState?>.LinkDownloadingView(
archivePath: String,
tempDatabaseFile: File,
chatReceiver: MutableState<MigrationToChatReceiver?>,
netCfg: NetCfg
netCfg: NetCfg,
networkProxy: NetworkProxy?
) {
Box {
SectionView(stringResource(MR.strings.migrate_to_device_downloading_details).uppercase()) {}
ProgressView()
}
LaunchedEffect(Unit) {
startDownloading(0, ctrl, user, tempDatabaseFile, chatReceiver, link, archivePath, netCfg)
startDownloading(0, ctrl, user, tempDatabaseFile, chatReceiver, link, archivePath, netCfg, networkProxy)
}
}
@@ -319,14 +327,14 @@ private fun DownloadProgressView(downloadedBytes: Long, totalBytes: Long) {
}
@Composable
private fun MutableState<MigrationToState?>.DownloadFailedView(link: String, chatReceiver: MigrationToChatReceiver?, archivePath: String, netCfg: NetCfg) {
private fun MutableState<MigrationToState?>.DownloadFailedView(link: String, chatReceiver: MigrationToChatReceiver?, archivePath: String, netCfg: NetCfg, networkProxy: NetworkProxy?) {
SectionView(stringResource(MR.strings.migrate_to_device_download_failed).uppercase()) {
SettingsActionItemWithContent(
icon = painterResource(MR.images.ic_download),
text = stringResource(MR.strings.migrate_to_device_repeat_download),
textColor = MaterialTheme.colors.primary,
click = {
state = MigrationToState.DatabaseInit(link, netCfg)
state = MigrationToState.DatabaseInit(link, netCfg, networkProxy)
}
) {}
SectionTextFooter(stringResource(MR.strings.migrate_to_device_try_again))
@@ -339,25 +347,25 @@ private fun MutableState<MigrationToState?>.DownloadFailedView(link: String, cha
}
@Composable
private fun MutableState<MigrationToState?>.ArchiveImportView(archivePath: String, netCfg: NetCfg) {
private fun MutableState<MigrationToState?>.ArchiveImportView(archivePath: String, netCfg: NetCfg, networkProxy: NetworkProxy?) {
Box {
SectionView(stringResource(MR.strings.migrate_to_device_importing_archive).uppercase()) {}
ProgressView()
}
LaunchedEffect(Unit) {
importArchive(archivePath, netCfg)
importArchive(archivePath, netCfg, networkProxy)
}
}
@Composable
private fun MutableState<MigrationToState?>.ArchiveImportFailedView(archivePath: String, netCfg: NetCfg) {
private fun MutableState<MigrationToState?>.ArchiveImportFailedView(archivePath: String, netCfg: NetCfg, networkProxy: NetworkProxy?) {
SectionView(stringResource(MR.strings.migrate_to_device_import_failed).uppercase()) {
SettingsActionItemWithContent(
icon = painterResource(MR.images.ic_download),
text = stringResource(MR.strings.migrate_to_device_repeat_import),
textColor = MaterialTheme.colors.primary,
click = {
state = MigrationToState.ArchiveImport(archivePath, netCfg)
state = MigrationToState.ArchiveImport(archivePath, netCfg, networkProxy)
}
) {}
SectionTextFooter(stringResource(MR.strings.migrate_to_device_try_again))
@@ -365,7 +373,7 @@ private fun MutableState<MigrationToState?>.ArchiveImportFailedView(archivePath:
}
@Composable
private fun MutableState<MigrationToState?>.PassphraseEnteringView(currentKey: String, netCfg: NetCfg) {
private fun MutableState<MigrationToState?>.PassphraseEnteringView(currentKey: String, netCfg: NetCfg, networkProxy: NetworkProxy?) {
val currentKey = rememberSaveable { mutableStateOf(currentKey) }
val verifyingPassphrase = rememberSaveable { mutableStateOf(false) }
val useKeychain = rememberSaveable { mutableStateOf(appPreferences.storeDBPassphrase.get()) }
@@ -395,9 +403,9 @@ private fun MutableState<MigrationToState?>.PassphraseEnteringView(currentKey: S
val (status, _) = chatInitTemporaryDatabase(dbAbsolutePrefixPath, key = currentKey.value, confirmation = MigrationConfirmation.YesUp)
val success = status == DBMigrationResult.OK || status == DBMigrationResult.InvalidConfirmation
if (success) {
state = MigrationToState.Migration(currentKey.value, MigrationConfirmation.YesUp, useKeychain.value, netCfg)
state = MigrationToState.Migration(currentKey.value, MigrationConfirmation.YesUp, useKeychain.value, netCfg, networkProxy)
} else if (status is DBMigrationResult.ErrorMigration) {
state = MigrationToState.MigrationConfirmation(status, currentKey.value, useKeychain.value, netCfg)
state = MigrationToState.MigrationConfirmation(status, currentKey.value, useKeychain.value, netCfg, networkProxy)
} else {
showErrorOnMigrationIfNeeded(status)
}
@@ -414,7 +422,7 @@ private fun MutableState<MigrationToState?>.PassphraseEnteringView(currentKey: S
}
@Composable
private fun MutableState<MigrationToState?>.MigrationConfirmationView(status: DBMigrationResult, passphrase: String, useKeychain: Boolean, netCfg: NetCfg) {
private fun MutableState<MigrationToState?>.MigrationConfirmationView(status: DBMigrationResult, passphrase: String, useKeychain: Boolean, netCfg: NetCfg, networkProxy: NetworkProxy?) {
data class Tuple4<A,B,C,D>(val a: A, val b: B, val c: C, val d: D)
val (header: String, button: String?, footer: String, confirmation: MigrationConfirmation?) = when (status) {
is DBMigrationResult.ErrorMigration -> when (val err = status.migrationError) {
@@ -449,7 +457,7 @@ private fun MutableState<MigrationToState?>.MigrationConfirmationView(status: DB
text = button,
textColor = MaterialTheme.colors.primary,
click = {
state = MigrationToState.Migration(passphrase, confirmation, useKeychain, netCfg)
state = MigrationToState.Migration(passphrase, confirmation, useKeychain, netCfg, networkProxy)
}
) {}
}
@@ -458,13 +466,13 @@ private fun MutableState<MigrationToState?>.MigrationConfirmationView(status: DB
}
@Composable
private fun MigrationView(passphrase: String, confirmation: MigrationConfirmation, useKeychain: Boolean, netCfg: NetCfg, close: () -> Unit) {
private fun MigrationView(passphrase: String, confirmation: MigrationConfirmation, useKeychain: Boolean, netCfg: NetCfg, networkProxy: NetworkProxy?, close: () -> Unit) {
Box {
SectionView(stringResource(MR.strings.migrate_to_device_migrating).uppercase()) {}
ProgressView()
}
LaunchedEffect(Unit) {
startChat(passphrase, confirmation, useKeychain, netCfg, close)
startChat(passphrase, confirmation, useKeychain, netCfg, networkProxy, close)
}
}
@@ -476,19 +484,21 @@ private fun ProgressView() {
private suspend fun MutableState<MigrationToState?>.checkUserLink(link: String) {
if (strHasSimplexFileLink(link.trim())) {
val data = MigrationFileLinkData.readFromLink(link)
val hasOnionConfigured = data?.networkConfig?.hasOnionConfigured() ?: false
val hasProxyConfigured = data?.networkConfig?.hasProxyConfigured() ?: false
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))
if (hasProxyConfigured && networkConfig?.hostMode != null && networkConfig.requiredHostMode != null) {
state = MigrationToState.Onion(link.trim(), networkConfig.legacySocksProxy, networkConfig.networkProxy, networkConfig.hostMode, networkConfig.requiredHostMode)
MigrationToDeviceState.save(MigrationToDeviceState.Onion(link.trim(), networkConfig.legacySocksProxy, networkConfig.networkProxy, networkConfig.hostMode, networkConfig.requiredHostMode))
} else {
val current = getNetCfg()
state = MigrationToState.DatabaseInit(link.trim(), current.copy(
socksProxy = networkConfig?.socksProxy,
socksProxy = null,
hostMode = networkConfig?.hostMode ?: current.hostMode,
requiredHostMode = networkConfig?.requiredHostMode ?: current.requiredHostMode
))
),
networkProxy = null
)
}
} else {
AlertManager.shared.showAlertMsg(
@@ -502,6 +512,7 @@ private fun MutableState<MigrationToState?>.prepareDatabase(
link: String,
tempDatabaseFile: File,
netCfg: NetCfg,
networkProxy: NetworkProxy?
) {
withLongRunningApi {
val ctrlAndUser = initTemporaryDatabase(tempDatabaseFile, netCfg)
@@ -513,7 +524,7 @@ private fun MutableState<MigrationToState?>.prepareDatabase(
}
val (ctrl, user) = ctrlAndUser
state = MigrationToState.LinkDownloading(link, ctrl, user, archivePath(), netCfg)
state = MigrationToState.LinkDownloading(link, ctrl, user, archivePath(), netCfg, networkProxy)
}
}
@@ -526,13 +537,14 @@ private fun MutableState<MigrationToState?>.startDownloading(
link: String,
archivePath: String,
netCfg: NetCfg,
networkProxy: NetworkProxy?
) {
withBGApi {
chatReceiver.value = MigrationToChatReceiver(ctrl, tempDatabaseFile) { msg ->
when (msg) {
is CR.RcvFileProgressXFTP -> {
state = MigrationToState.DownloadProgress(msg.receivedSize, msg.totalSize, msg.rcvFileTransfer.fileId, link, archivePath, netCfg, ctrl)
MigrationToDeviceState.save(MigrationToDeviceState.DownloadProgress(link, File(archivePath).name, netCfg))
state = MigrationToState.DownloadProgress(msg.receivedSize, msg.totalSize, msg.rcvFileTransfer.fileId, link, archivePath, netCfg, networkProxy, ctrl)
MigrationToDeviceState.save(MigrationToDeviceState.DownloadProgress(link, File(archivePath).name, netCfg, networkProxy))
}
is CR.RcvStandaloneFileComplete -> {
delay(500)
@@ -540,8 +552,8 @@ private fun MutableState<MigrationToState?>.startDownloading(
if (state == null) {
MigrationToDeviceState.save(null)
} else {
state = MigrationToState.ArchiveImport(archivePath, netCfg)
MigrationToDeviceState.save(MigrationToDeviceState.ArchiveImport(File(archivePath).name, netCfg))
state = MigrationToState.ArchiveImport(archivePath, netCfg, networkProxy)
MigrationToDeviceState.save(MigrationToDeviceState.ArchiveImport(File(archivePath).name, netCfg, networkProxy))
}
}
is CR.RcvFileError -> {
@@ -549,7 +561,7 @@ private fun MutableState<MigrationToState?>.startDownloading(
generalGetString(MR.strings.migrate_to_device_download_failed),
generalGetString(MR.strings.migrate_to_device_file_delete_or_link_invalid)
)
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg)
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg, networkProxy)
}
is CR.ChatRespError -> {
if (msg.chatError is ChatError.ChatErrorChat && msg.chatError.errorType is ChatErrorType.NoRcvFileUser) {
@@ -557,7 +569,7 @@ private fun MutableState<MigrationToState?>.startDownloading(
generalGetString(MR.strings.migrate_to_device_download_failed),
generalGetString(MR.strings.migrate_to_device_file_delete_or_link_invalid)
)
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg)
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg, networkProxy)
} else {
Log.d(TAG, "unsupported error: ${msg.responseType}, ${json.encodeToString(msg.chatError)}")
}
@@ -569,7 +581,7 @@ private fun MutableState<MigrationToState?>.startDownloading(
val (res, error) = controller.downloadStandaloneFile(user, link, CryptoFile.plain(File(archivePath).path), ctrl)
if (res == null) {
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg)
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg, networkProxy)
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.migrate_to_device_error_downloading_archive),
error
@@ -578,7 +590,7 @@ private fun MutableState<MigrationToState?>.startDownloading(
}
}
private fun MutableState<MigrationToState?>.importArchive(archivePath: String, netCfg: NetCfg) {
private fun MutableState<MigrationToState?>.importArchive(archivePath: String, netCfg: NetCfg, networkProxy: NetworkProxy?) {
withLongRunningApi {
try {
if (ChatController.ctrl == null || ChatController.ctrl == -1L) {
@@ -592,14 +604,14 @@ private fun MutableState<MigrationToState?>.importArchive(archivePath: String, n
if (archiveErrors.isNotEmpty()) {
showArchiveImportedWithErrorsAlert(archiveErrors)
}
state = MigrationToState.Passphrase("", netCfg)
MigrationToDeviceState.save(MigrationToDeviceState.Passphrase(netCfg))
state = MigrationToState.Passphrase("", netCfg, networkProxy)
MigrationToDeviceState.save(MigrationToDeviceState.Passphrase(netCfg, networkProxy))
} catch (e: Exception) {
state = MigrationToState.ArchiveImportFailed(archivePath, netCfg)
state = MigrationToState.ArchiveImportFailed(archivePath, netCfg, networkProxy)
AlertManager.shared.showAlertMsg (generalGetString(MR.strings.error_importing_database), e.stackTraceToString())
}
} catch (e: Exception) {
state = MigrationToState.ArchiveImportFailed(archivePath, netCfg)
state = MigrationToState.ArchiveImportFailed(archivePath, netCfg, networkProxy)
AlertManager.shared.showAlertMsg (generalGetString(MR.strings.error_deleting_database), e.stackTraceToString())
}
}
@@ -609,7 +621,7 @@ private suspend fun stopArchiveDownloading(fileId: Long, ctrl: ChatCtrl) {
controller.apiCancelFile(null, fileId, ctrl)
}
private fun startChat(passphrase: String, confirmation: MigrationConfirmation, useKeychain: Boolean, netCfg: NetCfg, close: () -> Unit) {
private fun startChat(passphrase: String, confirmation: MigrationConfirmation, useKeychain: Boolean, netCfg: NetCfg, networkProxy: NetworkProxy?, close: () -> Unit) {
if (useKeychain) {
ksDatabasePassword.set(passphrase)
} else {
@@ -621,7 +633,8 @@ private fun startChat(passphrase: String, confirmation: MigrationConfirmation, u
try {
initChatController(useKey = passphrase, confirmMigrations = confirmation) { CompletableDeferred(false) }
val appSettings = controller.apiGetAppSettings(AppSettings.current.prepareForExport()).copy(
networkConfig = netCfg
networkConfig = netCfg,
networkProxy = networkProxy
)
finishMigration(appSettings, close)
} catch (e: Exception) {
@@ -1,10 +1,10 @@
package chat.simplex.common.views.usersettings
import SectionBottomSpacer
import SectionCustomFooter
import SectionDividerSpaced
import SectionItemView
import SectionItemWithValue
import SectionTextFooter
import SectionView
import SectionViewSelectableCards
import androidx.compose.desktop.ui.tooling.preview.Preview
@@ -40,12 +40,10 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U
val currentCfg = remember { stateGetOrPut("currentCfg") { controller.getNetCfg() } }
val currentCfgVal = currentCfg.value // used only on initialization
val onionHosts = remember { mutableStateOf(currentCfgVal.onionHosts) }
val sessionMode = remember { mutableStateOf(currentCfgVal.sessionMode) }
val smpProxyMode = remember { mutableStateOf(currentCfgVal.smpProxyMode) }
val smpProxyFallback = remember { mutableStateOf(currentCfgVal.smpProxyFallback) }
val networkUseSocksProxy: MutableState<Boolean> = remember { mutableStateOf(currentCfgVal.useSocksProxy) }
val networkTCPConnectTimeout = remember { mutableStateOf(currentCfgVal.tcpConnectTimeout) }
val networkTCPTimeout = remember { mutableStateOf(currentCfgVal.tcpTimeout) }
val networkTCPTimeoutPerKb = remember { mutableStateOf(currentCfgVal.tcpTimeoutPerKb) }
@@ -90,11 +88,10 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U
tcpKeepAlive = tcpKeepAlive,
smpPingInterval = networkSMPPingInterval.value,
smpPingCount = networkSMPPingCount.value
).withOnionHosts(onionHosts.value)
).withOnionHosts(currentCfg.value.onionHosts)
}
fun updateView(cfg: NetCfg) {
onionHosts.value = cfg.onionHosts
sessionMode.value = cfg.sessionMode
smpProxyMode.value = cfg.smpProxyMode
smpProxyFallback.value = cfg.smpProxyFallback
@@ -148,10 +145,7 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U
) {
AdvancedNetworkSettingsLayout(
currentRemoteHost = currentRemoteHost,
networkUseSocksProxy = networkUseSocksProxy,
developerTools = developerTools,
onionHosts = onionHosts,
useOnion = { onionHosts.value = it; currentCfg.value = currentCfg.value.withOnionHosts(it) },
sessionMode = sessionMode,
smpProxyMode = smpProxyMode,
smpProxyFallback = smpProxyFallback,
@@ -183,10 +177,7 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U
@Composable fun AdvancedNetworkSettingsLayout(
currentRemoteHost: RemoteHostInfo?,
networkUseSocksProxy: State<Boolean>,
developerTools: Boolean,
onionHosts: MutableState<OnionHosts>,
useOnion: (OnionHosts) -> Unit,
sessionMode: MutableState<TransportSessionMode>,
smpProxyMode: MutableState<SMPProxyMode>,
smpProxyFallback: MutableState<SMPProxyFallback>,
@@ -223,21 +214,7 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U
SMPProxyFallbackPicker(smpProxyFallback, showModal, updateSMPProxyFallback, enabled = remember { derivedStateOf { smpProxyMode.value != SMPProxyMode.Never } })
SettingsPreferenceItem(painterResource(MR.images.ic_arrow_forward), stringResource(MR.strings.private_routing_show_message_status), chatModel.controller.appPrefs.showSentViaProxy)
}
SectionCustomFooter {
Text(stringResource(MR.strings.private_routing_explanation))
}
SectionDividerSpaced(maxTopPadding = true)
}
if (currentRemoteHost == null && networkUseSocksProxy.value) {
SectionView(stringResource(MR.strings.network_socks_proxy).uppercase()) {
UseOnionHosts(onionHosts, networkUseSocksProxy, showModal, useOnion)
SectionCustomFooter {
Column {
Text(annotatedStringResource(MR.strings.disable_onion_hosts_when_not_supported))
}
}
}
SectionTextFooter(stringResource(MR.strings.private_routing_explanation))
SectionDividerSpaced(maxTopPadding = true)
}
@@ -562,7 +539,6 @@ fun PreviewAdvancedNetworkSettingsLayout() {
SimpleXTheme {
AdvancedNetworkSettingsLayout(
currentRemoteHost = null,
networkUseSocksProxy = remember { mutableStateOf(false) },
developerTools = false,
sessionMode = remember { mutableStateOf(TransportSessionMode.User) },
smpProxyMode = remember { mutableStateOf(SMPProxyMode.Never) },
@@ -577,8 +553,6 @@ fun PreviewAdvancedNetworkSettingsLayout() {
networkTCPKeepIdle = remember { mutableStateOf(10) },
networkTCPKeepIntvl = remember { mutableStateOf(10) },
networkTCPKeepCnt = remember { mutableStateOf(10) },
onionHosts = remember { mutableStateOf(OnionHosts.PREFER) },
useOnion = {},
updateSessionMode = {},
updateSMPProxyMode = {},
updateSMPProxyFallback = {},
@@ -1,10 +1,10 @@
package chat.simplex.common.views.usersettings
import SectionBottomSpacer
import SectionCustomFooter
import SectionDividerSpaced
import SectionItemView
import SectionItemWithValue
import SectionTextFooter
import SectionView
import SectionViewSelectable
import TextIconSpaced
@@ -37,10 +37,11 @@ fun NetworkAndServersView() {
val netCfg = remember { chatModel.controller.getNetCfg() }
val networkUseSocksProxy: MutableState<Boolean> = remember { mutableStateOf(netCfg.useSocksProxy) }
val proxyPort = remember { derivedStateOf { chatModel.controller.appPrefs.networkProxyHostPort.state.value?.split(":")?.lastOrNull()?.toIntOrNull() ?: 9050 } }
val proxyPort = remember { derivedStateOf { appPrefs.networkProxy.state.value.port } }
NetworkAndServersLayout(
currentRemoteHost = currentRemoteHost,
networkUseSocksProxy = networkUseSocksProxy,
onionHosts = remember { mutableStateOf(netCfg.onionHosts) },
toggleSocksProxy = { enable ->
val def = NetCfg.defaults
val proxyDef = NetCfg.proxyDefaults
@@ -51,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)
}
@@ -104,6 +105,7 @@ fun NetworkAndServersView() {
@Composable fun NetworkAndServersLayout(
currentRemoteHost: RemoteHostInfo?,
networkUseSocksProxy: MutableState<Boolean>,
onionHosts: MutableState<OnionHosts>,
toggleSocksProxy: (Boolean) -> Unit,
) {
val m = chatModel
@@ -120,14 +122,10 @@ 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, 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) {
SectionCustomFooter {
Column {
Text(annotatedStringResource(MR.strings.socks_proxy_setting_limitations))
}
}
SectionTextFooter(annotatedStringResource(MR.strings.socks_proxy_setting_limitations))
SectionDividerSpaced(maxTopPadding = true)
} else {
SectionDividerSpaced()
@@ -158,16 +156,14 @@ fun NetworkAndServersView() {
networkUseSocksProxy: MutableState<Boolean>,
onionHosts: MutableState<OnionHosts>,
sessionMode: MutableState<TransportSessionMode>,
networkProxyHostPort: SharedPreference<String?>,
networkProxy: SharedPreference<NetworkProxy>,
toggleSocksProxy: (Boolean) -> Unit,
useOnion: (OnionHosts) -> 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, true, it) } })
UseOnionHosts(onionHosts, networkUseSocksProxy, showModal, useOnion)
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)
}
@@ -205,46 +201,98 @@ fun UseSocksProxySwitch(
@Composable
fun SocksProxySettings(
networkUseSocksProxy: Boolean,
networkProxyHostPort: SharedPreference<String?> = appPrefs.networkProxyHostPort,
networkProxy: SharedPreference<NetworkProxy>,
onionHosts: MutableState<OnionHosts>,
sessionMode: TransportSessionMode,
migration: Boolean,
close: () -> Unit
) {
val defaultHostPort = remember { "localhost:9050" }
val hostPortSaved by remember { networkProxyHostPort.state }
val networkProxySaved by remember { networkProxy.state }
val onionHostsSaved = remember { mutableStateOf(onionHosts.value) }
val usernameUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue(networkProxySaved.username))
}
val passwordUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue(networkProxySaved.password))
}
val hostUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue(hostPortSaved?.split(":")?.firstOrNull() ?: "localhost"))
mutableStateOf(TextFieldValue(networkProxySaved.host))
}
val portUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue(hostPortSaved?.split(":")?.lastOrNull() ?: "9050"))
mutableStateOf(TextFieldValue(networkProxySaved.port.toString()))
}
val save = {
val oldValue = networkProxyHostPort.get()
networkProxyHostPort.set(hostUnsaved.value.text + ":" + portUnsaved.value.text)
if (networkUseSocksProxy && !migration) {
withBGApi {
if (!controller.apiSetNetworkConfig(controller.getNetCfg())) {
networkProxyHostPort.set(oldValue)
}
val proxyAuthRandomUnsaved = rememberSaveable { mutableStateOf(networkProxySaved.auth == NetworkProxyAuth.ISOLATE) }
LaunchedEffect(proxyAuthRandomUnsaved.value) {
if (!proxyAuthRandomUnsaved.value && onionHosts.value != OnionHosts.NEVER) {
onionHosts.value = OnionHosts.NEVER
}
}
val proxyAuthModeUnsaved = remember(proxyAuthRandomUnsaved.value, usernameUnsaved.value.text, passwordUnsaved.value.text) {
derivedStateOf {
if (proxyAuthRandomUnsaved.value) {
NetworkProxyAuth.ISOLATE
} else {
NetworkProxyAuth.USERNAME
}
}
}
val saveAndClose = {
val oldValue = networkProxyHostPort.get()
networkProxyHostPort.set(hostUnsaved.value.text + ":" + portUnsaved.value.text)
val save: (Boolean) -> Unit = { closeOnSuccess ->
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.toIntOrNull() ?: 9050,
auth = proxyAuthModeUnsaved.value
)
)
val oldCfg = controller.getNetCfg()
val cfg = oldCfg.withOnionHosts(onionHosts.value)
val oldOnionHosts = onionHostsSaved.value
onionHostsSaved.value = onionHosts.value
if (!migration) {
controller.setNetCfg(cfg)
}
if (networkUseSocksProxy && !migration) {
withBGApi {
if (controller.apiSetNetworkConfig(controller.getNetCfg())) {
close()
if (controller.apiSetNetworkConfig(cfg, showAlertOnError = false)) {
onionHosts.value = cfg.onionHosts
onionHostsSaved.value = onionHosts.value
if (closeOnSuccess) {
close()
}
} else {
networkProxyHostPort.set(oldValue)
controller.setNetCfg(oldCfg)
networkProxy.set(oldValue)
onionHostsSaved.value = oldOnionHosts
showWrongProxyConfigAlert()
}
}
}
}
val saveDisabled = hostPortSaved == (hostUnsaved.value.text + ":" + portUnsaved.value.text) ||
remember { derivedStateOf { !validHost(hostUnsaved.value.text) } }.value ||
remember { derivedStateOf { !validPort(portUnsaved.value.text) } }.value
val resetDisabled = hostUnsaved.value.text + ":" + portUnsaved.value.text == defaultHostPort
val saveDisabled =
(
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
) ||
!validCredential(usernameUnsaved.value.text) ||
!validCredential(passwordUnsaved.value.text) ||
!validHost(hostUnsaved.value.text) ||
!validPort(portUnsaved.value.text)
val resetDisabled = hostUnsaved.value.text.trim() == "localhost" && portUnsaved.value.text.trim() == "9050" && proxyAuthRandomUnsaved.value && onionHosts.value == NetCfg.defaults.onionHosts
ModalView(
close = {
if (saveDisabled) {
@@ -252,7 +300,7 @@ fun SocksProxySettings(
} else {
showUnsavedSocksHostPortAlert(
confirmText = generalGetString(if (networkUseSocksProxy && !migration) MR.strings.network_options_save_and_reconnect else MR.strings.network_options_save),
save = saveAndClose,
save = { save(true) },
close = close
)
}
@@ -263,38 +311,78 @@ fun SocksProxySettings(
.fillMaxWidth()
) {
AppBarTitle(generalGetString(MR.strings.network_socks_proxy_settings))
SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
DefaultConfigurableTextField(
hostUnsaved,
stringResource(MR.strings.host_verb),
modifier = Modifier.fillMaxWidth(),
isValid = ::validHost,
keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }),
keyboardType = KeyboardType.Text,
)
DefaultConfigurableTextField(
portUnsaved,
stringResource(MR.strings.port_verb),
modifier = Modifier.fillMaxWidth(),
isValid = ::validPort,
keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done); save() }),
keyboardType = KeyboardType.Number,
)
SectionView(stringResource(MR.strings.network_socks_proxy).uppercase()) {
Column(Modifier.padding(horizontal = DEFAULT_PADDING)) {
DefaultConfigurableTextField(
hostUnsaved,
stringResource(MR.strings.host_verb),
modifier = Modifier.fillMaxWidth(),
isValid = ::validHost,
keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }),
keyboardType = KeyboardType.Text,
)
DefaultConfigurableTextField(
portUnsaved,
stringResource(MR.strings.port_verb),
modifier = Modifier.fillMaxWidth(),
isValid = ::validPort,
keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done); save(false) }),
keyboardType = KeyboardType.Number,
)
}
UseOnionHosts(onionHosts, rememberUpdatedState(networkUseSocksProxy && proxyAuthRandomUnsaved.value)) {
onionHosts.value = it
}
SectionTextFooter(annotatedStringResource(MR.strings.disable_onion_hosts_when_not_supported))
}
SectionDividerSpaced(maxBottomPadding = false)
SectionDividerSpaced(maxTopPadding = true)
SectionView(stringResource(MR.strings.network_proxy_auth).uppercase()) {
PreferenceToggle(
stringResource(MR.strings.network_proxy_random_credentials),
checked = proxyAuthRandomUnsaved.value,
onChange = { proxyAuthRandomUnsaved.value = it }
)
if (!proxyAuthRandomUnsaved.value) {
Column(Modifier.padding(horizontal = DEFAULT_PADDING)) {
DefaultConfigurableTextField(
usernameUnsaved,
stringResource(MR.strings.network_proxy_username),
modifier = Modifier.fillMaxWidth(),
isValid = ::validCredential,
keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }),
keyboardType = KeyboardType.Text,
)
DefaultConfigurableTextField(
passwordUnsaved,
stringResource(MR.strings.network_proxy_password),
modifier = Modifier.fillMaxWidth(),
isValid = ::validCredential,
keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }),
keyboardType = KeyboardType.Password,
)
}
}
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
onionHosts.value = NetCfg.defaults.onionHosts
}, disabled = resetDisabled) {
Text(stringResource(MR.strings.network_options_reset_to_defaults), color = if (resetDisabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
}
SectionItemView(
click = { if (networkUseSocksProxy && !migration) showUpdateNetworkSettingsDialog { save() } else save() },
click = { if (networkUseSocksProxy && !migration) showUpdateNetworkSettingsDialog { save(false) } else save(false) },
disabled = saveDisabled
) {
Text(stringResource(if (networkUseSocksProxy && !migration) MR.strings.network_options_save_and_reconnect else MR.strings.network_options_save), color = if (saveDisabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
@@ -305,6 +393,12 @@ fun SocksProxySettings(
}
}
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) {
AlertManager.shared.showAlertDialogStacked(
title = generalGetString(MR.strings.update_network_settings_question),
@@ -319,7 +413,6 @@ private fun showUnsavedSocksHostPortAlert(confirmText: String, save: () -> Unit,
fun UseOnionHosts(
onionHosts: MutableState<OnionHosts>,
enabled: State<Boolean>,
showModal: (@Composable ModalData.() -> Unit) -> Unit,
useOnion: (OnionHosts) -> Unit,
) {
val values = remember {
@@ -331,36 +424,29 @@ fun UseOnionHosts(
}
}
}
val onSelected = {
showModal {
ColumnWithScrollBar(
Modifier.fillMaxWidth(),
) {
AppBarTitle(stringResource(MR.strings.network_use_onion_hosts))
SectionViewSelectable(null, onionHosts, values, useOnion)
}
}
}
if (enabled.value) {
SectionItemWithValue(
generalGetString(MR.strings.network_use_onion_hosts),
onionHosts,
values,
icon = painterResource(MR.images.ic_security),
enabled = enabled,
onSelected = onSelected
)
} else {
// In reality, when socks proxy is disabled, this option acts like NEVER regardless of what was chosen before
SectionItemWithValue(
generalGetString(MR.strings.network_use_onion_hosts),
remember { mutableStateOf(OnionHosts.NEVER) },
listOf(ValueTitleDesc(OnionHosts.NEVER, generalGetString(MR.strings.network_use_onion_hosts_no), AnnotatedString(generalGetString(MR.strings.network_use_onion_hosts_no_desc)))),
icon = painterResource(MR.images.ic_security),
enabled = enabled,
onSelected = {}
)
Column {
if (enabled.value) {
ExposedDropDownSettingRow(
generalGetString(MR.strings.network_use_onion_hosts),
values.map { it.value to it.title },
onionHosts,
icon = painterResource(MR.images.ic_security),
enabled = enabled,
onSelected = useOnion
)
} else {
// In reality, when socks proxy is disabled, this option acts like NEVER regardless of what was chosen before
ExposedDropDownSettingRow(
generalGetString(MR.strings.network_use_onion_hosts),
listOf(OnionHosts.NEVER to generalGetString(MR.strings.network_use_onion_hosts_no)),
remember { mutableStateOf(OnionHosts.NEVER) },
icon = painterResource(MR.images.ic_security),
enabled = enabled,
onSelected = {}
)
}
SectionTextFooter(values.first { it.value == onionHosts.value }.description)
}
}
@@ -398,12 +484,8 @@ fun SessionModePicker(
)
}
// https://stackoverflow.com/a/106223
private fun validHost(s: String): Boolean {
val validIp = Regex("^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])[.]){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$")
val validHostname = Regex("^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])[.])*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$");
return s.matches(validIp) || s.matches(validHostname)
}
private fun validHost(s: String): Boolean =
!s.contains('@')
// https://ihateregex.io/expr/port/
fun validPort(s: String): Boolean {
@@ -411,6 +493,16 @@ fun validPort(s: String): Boolean {
return s.isNotBlank() && s.matches(validPort)
}
private fun validCredential(s: String): Boolean =
!s.contains(':') && !s.contains('@')
fun showWrongProxyConfigAlert() {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.network_proxy_incorrect_config_title),
text = generalGetString(MR.strings.network_proxy_incorrect_config_desc),
)
}
fun showUpdateNetworkSettingsDialog(
title: String,
startsWith: String = "",
@@ -435,6 +527,7 @@ fun PreviewNetworkAndServersLayout() {
NetworkAndServersLayout(
currentRemoteHost = null,
networkUseSocksProxy = remember { mutableStateOf(true) },
onionHosts = remember { mutableStateOf(OnionHosts.PREFER) },
toggleSocksProxy = {},
)
}
@@ -765,7 +765,17 @@
<string name="network_socks_proxy">SOCKS proxy</string>
<string name="network_socks_proxy_settings">SOCKS proxy settings</string>
<string name="network_socks_toggle_use_socks_proxy">Use SOCKS proxy</string>
<string name="network_proxy_auth">Proxy authentication</string>
<string name="network_proxy_random_credentials">Use random credentials</string>
<string name="network_proxy_auth_mode_isolate_by_auth_user">Use different proxy credentials for each profile.</string>
<string name="network_proxy_auth_mode_isolate_by_auth_entity">Use different proxy credentials for each connection.</string>
<string name="network_proxy_auth_mode_no_auth">Do not use credentials with proxy.</string>
<string name="network_proxy_auth_mode_username_password">Your credentials may be sent unencrypted.</string>
<string name="network_proxy_username">Username</string>
<string name="network_proxy_password">Password</string>
<string name="network_proxy_port">port %d</string>
<string name="network_proxy_incorrect_config_title">Error saving proxy</string>
<string name="network_proxy_incorrect_config_desc">Make sure proxy configuration is correct.</string>
<string name="host_verb">Host</string>
<string name="port_verb">Port</string>
<string name="network_enable_socks">Use SOCKS proxy?</string>