From 0a8c8ac1ed7118b5ff743f9c06bbed26ee6b8b78 Mon Sep 17 00:00:00 2001 From: Abdelilah El Aissaoui Date: Sun, 10 Sep 2023 18:26:11 +0200 Subject: [PATCH] Implemented proxy settings logic Fixes #113 --- .../kotlin/com/jetpackduba/gitnuro/App.kt | 78 +++++++++++++- .../gitnuro/preferences/AppSettings.kt | 101 ++++++++++++++++++ .../ui/dialogs/settings/SettingsDialog.kt | 83 ++++++++++---- .../gitnuro/viewmodels/SettingsViewModel.kt | 43 ++++++++ 4 files changed, 284 insertions(+), 21 deletions(-) diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/App.kt b/src/main/kotlin/com/jetpackduba/gitnuro/App.kt index 93e4ccd..b487b19 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/App.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/App.kt @@ -26,6 +26,7 @@ import com.jetpackduba.gitnuro.logging.printError import com.jetpackduba.gitnuro.managers.AppStateManager import com.jetpackduba.gitnuro.managers.TempFilesManager import com.jetpackduba.gitnuro.preferences.AppSettings +import com.jetpackduba.gitnuro.preferences.ProxySettings import com.jetpackduba.gitnuro.system.systemSeparator import com.jetpackduba.gitnuro.theme.AppTheme import com.jetpackduba.gitnuro.theme.Theme @@ -36,11 +37,17 @@ import com.jetpackduba.gitnuro.ui.components.RepositoriesTabPanel import com.jetpackduba.gitnuro.ui.components.TabInformation import com.jetpackduba.gitnuro.ui.components.emptyTabInformation import com.jetpackduba.gitnuro.ui.context_menu.AppPopupMenu +import com.jetpackduba.gitnuro.ui.dialogs.settings.ProxyType +import kotlinx.coroutines.launch import org.eclipse.jgit.lib.GpgSigner import java.io.File +import java.net.Authenticator +import java.net.PasswordAuthentication import java.nio.file.Paths +import java.util.* import javax.inject.Inject + private const val TAG = "App" val LocalTabScope = compositionLocalOf { emptyTabInformation() } @@ -73,6 +80,9 @@ class App { @OptIn(ExperimentalFoundationApi::class) fun start(args: Array) { tabsManager.appComponent = this.appComponent + + initProxySettings() + val windowPlacement = appSettings.windowPlacement.toWindowPlacement val dirToOpen = getDirToOpen(args) @@ -118,7 +128,8 @@ class App { state = windowState, icon = painterResource(AppIcons.LOGO), ) { - val compositionValues: MutableList> = mutableListOf(LocalTextContextMenu provides AppPopupMenu()) + val compositionValues: MutableList> = + mutableListOf(LocalTextContextMenu provides AppPopupMenu()) if (scale != -1f) { compositionValues.add(LocalDensity provides Density(scale, 1f)) @@ -146,6 +157,71 @@ class App { } } + private fun initProxySettings() { + appStateManager.appScope.launch { + appSettings.proxyFlow.collect { proxySettings -> + if (proxySettings.useProxy) { + when (proxySettings.proxyType) { + ProxyType.HTTP -> setHttpProxy(proxySettings) + ProxyType.SOCKS -> setSocksProxy(proxySettings) + } + } else { + clearProxySettings() + } + } + } + } + + private fun clearProxySettings() { + System.setProperty("http.proxyHost", "") + System.setProperty("http.proxyPort", "") + System.setProperty("https.proxyHost", "") + System.setProperty("https.proxyPort", "") + System.setProperty("socksProxyHost", "") + System.setProperty("socksProxyPort", "") + } + + private fun setHttpProxy(proxySettings: ProxySettings) { + System.setProperty("http.proxyHost", proxySettings.hostName) + System.setProperty("http.proxyPort", proxySettings.hostPort.toString()) + System.setProperty("https.proxyHost", proxySettings.hostName) + System.setProperty("https.proxyPort", proxySettings.hostPort.toString()) + + if (proxySettings.useAuth) { + Authenticator.setDefault( + object : Authenticator() { + public override fun getPasswordAuthentication(): PasswordAuthentication { + return PasswordAuthentication(proxySettings.hostUser, proxySettings.hostPassword.toCharArray()) + } + } + ) + + System.setProperty("http.proxyUser", proxySettings.hostUser) + System.setProperty("http.proxyPassword", proxySettings.hostPassword) + System.setProperty("https.proxyUser", proxySettings.hostUser) + System.setProperty("https.proxyPassword", proxySettings.hostPassword) + System.setProperty("jdk.http.auth.tunneling.disabledSchemes", "") + } + } + + private fun setSocksProxy(proxySettings: ProxySettings) { + System.setProperty("socksProxyHost", proxySettings.hostName) + System.setProperty("socksProxyPort", proxySettings.hostPort.toString()) + + if (proxySettings.useAuth) { + Authenticator.setDefault( + object : Authenticator() { + public override fun getPasswordAuthentication(): PasswordAuthentication { + return PasswordAuthentication(proxySettings.hostUser, proxySettings.hostPassword.toCharArray()) + } + } + ) + + System.setProperty("java.net.socks.username", proxySettings.hostUser) + System.setProperty("java.net.socks.password", proxySettings.hostPassword) + } + } + private fun addDirTab(dirToOpen: File) { val absolutePath = dirToOpen.normalize().absolutePath .removeSuffix(systemSeparator) diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/preferences/AppSettings.kt b/src/main/kotlin/com/jetpackduba/gitnuro/preferences/AppSettings.kt index ac4bffe..cb2790a 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/preferences/AppSettings.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/preferences/AppSettings.kt @@ -5,6 +5,7 @@ import com.jetpackduba.gitnuro.system.OS import com.jetpackduba.gitnuro.system.currentOs import com.jetpackduba.gitnuro.theme.ColorsScheme import com.jetpackduba.gitnuro.theme.Theme +import com.jetpackduba.gitnuro.ui.dialogs.settings.ProxyType import com.jetpackduba.gitnuro.viewmodels.TextDiffType import com.jetpackduba.gitnuro.viewmodels.textDiffTypeFromValue import kotlinx.coroutines.flow.* @@ -30,6 +31,13 @@ private const val PREF_DIFF_TYPE = "diffType" private const val PREF_DIFF_FULL_FILE = "diffFullFile" private const val PREF_SWAP_UNCOMMITTED_CHANGES = "inverseUncommittedChanges" private const val PREF_TERMINAL_PATH = "terminalPath" +private const val PREF_USE_PROXY = "useProxy" +private const val PREF_PROXY_TYPE = "proxyType" +private const val PREF_PROXY_HOST_NAME = "proxyHostName" +private const val PREF_PROXY_PORT = "proxyPort" +private const val PREF_PROXY_USE_AUTH = "proxyUseAuth" +private const val PREF_PROXY_USER = "proxyHostUser" +private const val PREF_PROXY_PASSWORD = "proxyHostPassword" private const val PREF_CACHE_CREDENTIALS_IN_MEMORY = "credentialsInMemory" @@ -86,6 +94,20 @@ class AppSettings @Inject constructor() { private val _terminalPathFlow = MutableStateFlow(terminalPath) val terminalPathFlow = _terminalPathFlow.asStateFlow() + private val _proxyFlow = MutableStateFlow( + ProxySettings( + useProxy, + proxyType, + proxyHostName, + proxyPortNumber, + proxyUseAuth, + proxyHostUser, + proxyHostPassword, + ) + ) + + val proxyFlow = _proxyFlow.asStateFlow() + var latestTabsOpened: String get() = preferences.get(PREF_LATEST_REPOSITORIES_TABS_OPENED, "") set(value) { @@ -238,6 +260,62 @@ class AppSettings @Inject constructor() { _terminalPathFlow.value = value } + var useProxy: Boolean + get() { + return preferences.getBoolean(PREF_USE_PROXY, false) + } + set(value) { + preferences.putBoolean(PREF_USE_PROXY, value) + _proxyFlow.value = _proxyFlow.value.copy(useProxy = value) + } + + var proxyUseAuth: Boolean + get() { + return preferences.getBoolean(PREF_PROXY_USE_AUTH, false) + } + set(value) { + preferences.putBoolean(PREF_PROXY_USE_AUTH, value) + _proxyFlow.value = _proxyFlow.value.copy(useAuth = value) + } + + var proxyType: ProxyType + get() { + val value = preferences.getInt(PREF_PROXY_TYPE, ProxyType.HTTP.value) + return ProxyType.fromInt(value) + } + set(value) { + preferences.putInt(PREF_PROXY_TYPE, value.value) + _proxyFlow.value = _proxyFlow.value.copy(proxyType = value) + } + + var proxyHostName: String + get() = preferences.get(PREF_PROXY_HOST_NAME, "") + set(value) { + preferences.put(PREF_PROXY_HOST_NAME, value) + _proxyFlow.value = _proxyFlow.value.copy(hostName = value) + } + + var proxyPortNumber: Int + get() = preferences.getInt(PREF_PROXY_PORT, 80) + set(value) { + preferences.putInt(PREF_PROXY_PORT, value) + _proxyFlow.value = _proxyFlow.value.copy(hostPort = value) + } + + var proxyHostUser: String + get() = preferences.get(PREF_PROXY_USER, "") + set(value) { + preferences.put(PREF_PROXY_USER, value) + _proxyFlow.value = _proxyFlow.value.copy(hostUser = value) + } + + var proxyHostPassword: String + get() = preferences.get(PREF_PROXY_PASSWORD, "") + set(value) { + preferences.put(PREF_PROXY_PASSWORD, value) + _proxyFlow.value = _proxyFlow.value.copy(hostPassword = value) + } + fun saveCustomTheme(filePath: String) { val file = File(filePath) val content = file.readText() @@ -254,8 +332,31 @@ class AppSettings @Inject constructor() { _customThemeFlow.value = Json.decodeFromString(themeJson) } } + + private fun loadProxySettings() { + _proxyFlow.value = ProxySettings( + useProxy, + proxyType, + proxyHostName, + proxyPortNumber, + proxyUseAuth, + proxyHostUser, + proxyHostPassword, + ) + } } +data class ProxySettings( + val useProxy: Boolean, + val proxyType: ProxyType, + val hostName: String, + val hostPort: Int, + val useAuth: Boolean, + val hostUser: String, + val hostPassword: String, +) + + // TODO migrate old prefs path to new one? fun initPreferencesPath() { if (currentOs == OS.LINUX) { diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/settings/SettingsDialog.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/settings/SettingsDialog.kt index 362fd86..2686364 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/settings/SettingsDialog.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/settings/SettingsDialog.kt @@ -47,7 +47,7 @@ val settings = listOf( SettingsEntry.Entry(AppIcons.CLOUD, "Remote actions") { RemoteActions(it) }, SettingsEntry.Section("Network"), - SettingsEntry.Entry(AppIcons.NETWORK, "Proxy") { Proxy() }, + SettingsEntry.Entry(AppIcons.NETWORK, "Proxy") { Proxy(it) }, SettingsEntry.Entry(AppIcons.PASSWORD, "Authentication") { Authentication(it) }, SettingsEntry.Section("Tools"), @@ -55,24 +55,32 @@ val settings = listOf( ) @Composable -fun Proxy() { - var useProxy by remember { mutableStateOf(false) } +fun Proxy(settingsViewModel: SettingsViewModel) { + var useProxy by remember { mutableStateOf(settingsViewModel.useProxy) } - var hostName by remember { mutableStateOf("") } - var portNumber by remember { mutableStateOf(80) } - var login by remember { mutableStateOf("") } - var password by remember { mutableStateOf("") } + var hostName by remember { mutableStateOf(settingsViewModel.proxyHostName) } + var portNumber by remember { mutableStateOf(settingsViewModel.proxyPortNumber) } + + var useAuth by remember { mutableStateOf(settingsViewModel.proxyUseAuth) } + var user by remember { mutableStateOf(settingsViewModel.proxyHostUser) } + var password by remember { mutableStateOf(settingsViewModel.proxyHostPassword) } val proxyTypes = listOf(ProxyType.HTTP, ProxyType.SOCKS) val proxyTypesDropDownOptions = proxyTypes.map { DropDownOption(it, it.name) } - var currentProxyType by remember { mutableStateOf(proxyTypesDropDownOptions.first()) } + + var currentProxyType by remember { + mutableStateOf(proxyTypesDropDownOptions.first { it.value == settingsViewModel.proxyType }) + } Column { SettingToggle( title = "Use proxy", subtitle = "Set up your proxy configuration if needed", value = useProxy, - onValueChanged = { useProxy = it }, + onValueChanged = { + useProxy = it + settingsViewModel.useProxy = it + }, ) SettingDropDown( @@ -80,14 +88,20 @@ fun Proxy() { subtitle = "Pick between HTTP or SOCKS", dropDownOptions = proxyTypesDropDownOptions, currentOption = currentProxyType, - onOptionSelected = { currentProxyType = it } + onOptionSelected = { + currentProxyType = it + settingsViewModel.proxyType = it.value + } ) SettingTextInput( title = "Host name", subtitle = "", value = hostName, - onValueChanged = { hostName = it }, + onValueChanged = { + hostName = it + settingsViewModel.proxyHostName = it + }, enabled = useProxy, ) @@ -95,16 +109,32 @@ fun Proxy() { title = "Port number", subtitle = "", value = portNumber, - onValueChanged = { portNumber = it }, + onValueChanged = { + portNumber = it + settingsViewModel.proxyPortNumber = it + }, enabled = useProxy, ) + SettingToggle( + title = "Proxy authentication", + subtitle = "Use your credentials to provide your identity the proxy server", + value = useAuth, + onValueChanged = { + useAuth = it + settingsViewModel.proxyUseAuth = it + } + ) + SettingTextInput( title = "Login", subtitle = "", - value = login, - onValueChanged = { login = it }, - enabled = useProxy, + value = user, + onValueChanged = { + user = it + settingsViewModel.proxyHostUser = it + }, + enabled = useProxy && useAuth, ) @@ -112,9 +142,12 @@ fun Proxy() { title = "Password", subtitle = "", value = password, - onValueChanged = { password = it }, + onValueChanged = { + password = it + settingsViewModel.proxyHostPassword = it + }, isPassword = true, - enabled = useProxy, + enabled = useProxy && useAuth, ) } @@ -691,7 +724,17 @@ private fun isValidFloat(value: String): Boolean { } } -enum class ProxyType { - HTTP, - SOCKS, +enum class ProxyType(val value: Int) { + HTTP(1), + SOCKS(2); + + companion object { + fun fromInt(value: Int): ProxyType { + return when (value) { + HTTP.value -> HTTP + SOCKS.value -> SOCKS + else -> throw NotImplementedError("Proxy type unknown") + } + } + } } \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SettingsViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SettingsViewModel.kt index 7d77a2e..307aa28 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SettingsViewModel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SettingsViewModel.kt @@ -7,6 +7,7 @@ import com.jetpackduba.gitnuro.preferences.AppSettings import com.jetpackduba.gitnuro.system.OpenFilePickerUseCase import com.jetpackduba.gitnuro.system.PickerType import com.jetpackduba.gitnuro.theme.Theme +import com.jetpackduba.gitnuro.ui.dialogs.settings.ProxyType import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import javax.inject.Inject @@ -84,6 +85,48 @@ class SettingsViewModel @Inject constructor( appSettings.terminalPath = value } + var useProxy: Boolean + get() = appSettings.useProxy + set(value) { + appSettings.useProxy = value + } + + var proxyType: ProxyType + get() = appSettings.proxyType + set(value) { + appSettings.proxyType = value + } + + var proxyHostName: String + get() = appSettings.proxyHostName + set(value) { + appSettings.proxyHostName = value + } + + var proxyPortNumber: Int + get() = appSettings.proxyPortNumber + set(value) { + appSettings.proxyPortNumber = value + } + + var proxyUseAuth: Boolean + get() = appSettings.proxyUseAuth + set(value) { + appSettings.proxyUseAuth = value + } + + var proxyHostUser: String + get() = appSettings.proxyHostUser + set(value) { + appSettings.proxyHostUser = value + } + + var proxyHostPassword: String + get() = appSettings.proxyHostPassword + set(value) { + appSettings.proxyHostPassword = value + } + fun saveCustomTheme(filePath: String): Error? { return try { appSettings.saveCustomTheme(filePath)