mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-05-26 07:34:39 +00:00
android, desktop: proxy configuration includes credentials
This commit is contained in:
+8
-6
@@ -531,7 +531,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 +976,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
|
||||
}
|
||||
}
|
||||
|
||||
+3
-29
@@ -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 = {},
|
||||
|
||||
+240
-84
@@ -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
|
||||
@@ -29,6 +29,7 @@ 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() {
|
||||
@@ -41,6 +42,7 @@ fun NetworkAndServersView() {
|
||||
NetworkAndServersLayout(
|
||||
currentRemoteHost = currentRemoteHost,
|
||||
networkUseSocksProxy = networkUseSocksProxy,
|
||||
onionHosts = remember { mutableStateOf(netCfg.onionHosts) },
|
||||
toggleSocksProxy = { enable ->
|
||||
val def = NetCfg.defaults
|
||||
val proxyDef = NetCfg.proxyDefaults
|
||||
@@ -104,6 +106,7 @@ fun NetworkAndServersView() {
|
||||
@Composable fun NetworkAndServersLayout(
|
||||
currentRemoteHost: RemoteHostInfo?,
|
||||
networkUseSocksProxy: MutableState<Boolean>,
|
||||
onionHosts: MutableState<OnionHosts>,
|
||||
toggleSocksProxy: (Boolean) -> Unit,
|
||||
) {
|
||||
val m = chatModel
|
||||
@@ -120,14 +123,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.networkProxyHostPort, onionHosts, 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()
|
||||
@@ -166,8 +165,8 @@ fun NetworkAndServersView() {
|
||||
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, networkProxyHostPort, onionHosts, true, it) } })
|
||||
UseOnionHosts(onionHosts, networkUseSocksProxy, useOnion)
|
||||
if (developerTools) {
|
||||
SessionModePicker(sessionMode, showModal, updateSessionMode)
|
||||
}
|
||||
@@ -206,45 +205,94 @@ fun UseSocksProxySwitch(
|
||||
fun SocksProxySettings(
|
||||
networkUseSocksProxy: Boolean,
|
||||
networkProxyHostPort: SharedPreference<String?> = appPrefs.networkProxyHostPort,
|
||||
onionHosts: MutableState<OnionHosts>,
|
||||
migration: Boolean,
|
||||
close: () -> Unit
|
||||
) {
|
||||
val defaultHostPort = remember { "localhost:9050" }
|
||||
val hostPortSaved by remember { networkProxyHostPort.state }
|
||||
val proxyStringSaved by remember { networkProxyHostPort.state }
|
||||
val proxyComponentsSaved by remember(proxyStringSaved) { mutableStateOf(ProxyComponents.from(proxyStringSaved)) }
|
||||
val onionHostsSaved = remember { mutableStateOf(onionHosts.value) }
|
||||
|
||||
val usernameUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
||||
mutableStateOf(TextFieldValue(proxyComponentsSaved.usernamePassword.first))
|
||||
}
|
||||
val passwordUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
||||
mutableStateOf(TextFieldValue(proxyComponentsSaved.usernamePassword.second))
|
||||
}
|
||||
val hostUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
||||
mutableStateOf(TextFieldValue(hostPortSaved?.split(":")?.firstOrNull() ?: "localhost"))
|
||||
mutableStateOf(TextFieldValue(proxyComponentsSaved.host))
|
||||
}
|
||||
val portUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
||||
mutableStateOf(TextFieldValue(hostPortSaved?.split(":")?.lastOrNull() ?: "9050"))
|
||||
mutableStateOf(TextFieldValue(proxyComponentsSaved.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(proxyComponentsSaved.authMode == ProxyAuthenticationMode.ISOLATE_BY_AUTH) }
|
||||
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) {
|
||||
ProxyAuthenticationMode.ISOLATE_BY_AUTH
|
||||
} else if (usernameUnsaved.value.text.isBlank() && passwordUnsaved.value.text.isBlank()) {
|
||||
ProxyAuthenticationMode.NO_AUTH
|
||||
} else {
|
||||
ProxyAuthenticationMode.USERNAME_PASSWORD
|
||||
}
|
||||
}
|
||||
}
|
||||
val saveAndClose = {
|
||||
|
||||
val save: (Boolean) -> Unit = { closeOnSuccess ->
|
||||
val oldValue = networkProxyHostPort.get()
|
||||
networkProxyHostPort.set(hostUnsaved.value.text + ":" + portUnsaved.value.text)
|
||||
networkProxyHostPort.set(
|
||||
ProxyComponents(
|
||||
usernamePassword = usernameUnsaved.value.text to passwordUnsaved.value.text,
|
||||
host = hostUnsaved.value.text,
|
||||
port = portUnsaved.value.text.trim().toIntOrNull() ?: 9050,
|
||||
authMode = proxyAuthModeUnsaved.value
|
||||
).toProxyString()
|
||||
)
|
||||
val oldCfg = controller.getNetCfg()
|
||||
val cfg = oldCfg.withOnionHosts(onionHosts.value)
|
||||
|
||||
if (!migration) {
|
||||
controller.setNetCfg(cfg)
|
||||
}
|
||||
if (networkUseSocksProxy && !migration) {
|
||||
withBGApi {
|
||||
if (controller.apiSetNetworkConfig(controller.getNetCfg())) {
|
||||
close()
|
||||
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) {
|
||||
close()
|
||||
}
|
||||
} else {
|
||||
controller.setNetCfg(oldCfg)
|
||||
networkProxyHostPort.set(oldValue)
|
||||
onionHosts.value = oldCfg.onionHosts
|
||||
showWrongProxyConfigAlert()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val saveDisabled = hostPortSaved == (hostUnsaved.value.text + ":" + portUnsaved.value.text) ||
|
||||
remember { derivedStateOf { !validHost(hostUnsaved.value.text) } }.value ||
|
||||
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 &&
|
||||
onionHosts.value == onionHostsSaved.value
|
||||
) ||
|
||||
remember { derivedStateOf { !validPort(portUnsaved.value.text) } }.value
|
||||
val resetDisabled = hostUnsaved.value.text + ":" + portUnsaved.value.text == defaultHostPort
|
||||
val resetDisabled = hostUnsaved.value.text.trim() + ":" + portUnsaved.value.text.trim() == defaultHostPort && 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,26 +311,64 @@ 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 = { true },
|
||||
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 = { !it.contains(':') && !it.contains('@') },
|
||||
keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }),
|
||||
keyboardType = KeyboardType.Text,
|
||||
)
|
||||
DefaultConfigurableTextField(
|
||||
passwordUnsaved,
|
||||
stringResource(MR.strings.network_proxy_password),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
isValid = { !it.contains(':') && !it.contains('@') },
|
||||
keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }),
|
||||
keyboardType = KeyboardType.Password,
|
||||
)
|
||||
}
|
||||
}
|
||||
SectionTextFooter(proxyAuthModeUnsaved.value.text)
|
||||
}
|
||||
|
||||
SectionDividerSpaced(maxBottomPadding = false, maxTopPadding = true)
|
||||
|
||||
SectionView {
|
||||
SectionItemView({
|
||||
@@ -290,11 +376,15 @@ fun SocksProxySettings(
|
||||
val newPort = defaultHostPort.split(":").last()
|
||||
hostUnsaved.value = hostUnsaved.value.copy(newHost, TextRange(newHost.length))
|
||||
portUnsaved.value = portUnsaved.value.copy(newPort, TextRange(newPort.length))
|
||||
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 +395,79 @@ fun SocksProxySettings(
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private data class ProxyComponents(
|
||||
val usernamePassword: Pair<String, String>,
|
||||
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;
|
||||
|
||||
val text: String
|
||||
get() = when (this) {
|
||||
ISOLATE_BY_AUTH -> generalGetString(if (appPrefs.networkSessionMode.get() == 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 showUnsavedSocksHostPortAlert(confirmText: String, save: () -> Unit, close: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialogStacked(
|
||||
title = generalGetString(MR.strings.update_network_settings_question),
|
||||
@@ -319,7 +482,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 +493,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,19 +553,19 @@ 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)
|
||||
}
|
||||
|
||||
// https://ihateregex.io/expr/port/
|
||||
fun validPort(s: String): Boolean {
|
||||
val validPort = Regex("^(6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4})$")
|
||||
return s.isNotBlank() && s.matches(validPort)
|
||||
}
|
||||
|
||||
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 +590,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 auth</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>
|
||||
|
||||
Reference in New Issue
Block a user