diff --git a/desktop/app/build.gradle.kts b/desktop/app/build.gradle.kts index ee4df54..877cb8c 100644 --- a/desktop/app/build.gradle.kts +++ b/desktop/app/build.gradle.kts @@ -49,10 +49,9 @@ dependencies { implementation(libs.osThemeDetector) { exclude(group = "net.java.dev.jna") } - - // at the moment I don't use jna but some libraries does - // filekit and osThemeDetector both use jna but with different versions - // I excluded jna from both of them and add it here! + implementation(libs.proxyVole) { + exclude(group = "net.java.dev.jna") + } implementation(libs.jna.core) implementation(libs.jna.platform) diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/di/Di.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/di/Di.kt index d12aa71..cfe7118 100644 --- a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/di/Di.kt +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/di/Di.kt @@ -18,6 +18,9 @@ import com.abdownloadmanager.shared.utils.ui.theme.ISystemThemeDetector import com.abdownloadmanager.desktop.utils.* import com.abdownloadmanager.desktop.utils.native_messaging.NativeMessaging import com.abdownloadmanager.desktop.utils.native_messaging.NativeMessagingManifestApplier +import com.abdownloadmanager.desktop.utils.proxy.AutoConfigurableProxyProviderForDesktop +import com.abdownloadmanager.desktop.utils.proxy.DesktopSystemProxySelectorProvider +import com.abdownloadmanager.desktop.utils.proxy.ProxyCachingConfig import com.arkivanov.decompose.DefaultComponentContext import com.arkivanov.essenty.lifecycle.LifecycleRegistry import ir.amirab.downloader.DownloadManagerMinimalControl @@ -55,7 +58,9 @@ import com.abdownloadmanager.shared.utils.ui.IMyIcons import com.abdownloadmanager.shared.utils.proxy.IProxyStorage import com.abdownloadmanager.shared.utils.proxy.ProxyData import com.abdownloadmanager.shared.utils.proxy.ProxyManager +import ir.amirab.downloader.connection.proxy.AutoConfigurableProxyProvider import ir.amirab.downloader.connection.proxy.ProxyStrategyProvider +import ir.amirab.downloader.connection.proxy.SystemProxySelectorProvider import ir.amirab.downloader.monitor.IDownloadMonitor import ir.amirab.downloader.utils.EmptyFileCreator import ir.amirab.util.compose.localizationmanager.LanguageManager @@ -112,6 +117,15 @@ val downloaderModule = module { get() ) }.bind() + single { + ProxyCachingConfig.default() + } + single { + AutoConfigurableProxyProviderForDesktop(get()) + } + single { + DesktopSystemProxySelectorProvider(get()) + } single { OkHttpDownloaderClient( OkHttpClient @@ -121,7 +135,9 @@ val downloaderModule = module { maxRequests = Int.MAX_VALUE maxRequestsPerHost = Int.MAX_VALUE }).build(), - get() + get(), + get(), + get(), ) } single { @@ -341,4 +357,4 @@ object Di : KoinComponent { modules(appModule) } } -} \ No newline at end of file +} diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/settings/SettingsComponent.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/settings/SettingsComponent.kt index 67f5ad0..f4d7a66 100644 --- a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/settings/SettingsComponent.kt +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/settings/SettingsComponent.kt @@ -231,6 +231,15 @@ fun proxyConfig(proxyManager: ProxyManager, scope: CoroutineScope): ProxyConfigu value = it.proxyWithRules.proxy.run { "$type $host:$port" } ) ) + + ProxyMode.Pac -> { + Res.string.settings_use_proxy_describe_pac_proxy + .asStringSourceWithARgs( + Res.string.settings_use_proxy_describe_pac_proxy_createArgs( + value = it.pac.uri + ) + ) + } } } ) @@ -461,4 +470,4 @@ class SettingsComponent( val configurables by derivedStateOf { allConfigs[currentPage] } -} \ No newline at end of file +} diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/settings/configurable/widgets/Proxy.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/settings/configurable/widgets/Proxy.kt index 8d2dafd..fe4b7c3 100644 --- a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/settings/configurable/widgets/Proxy.kt +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/pages/settings/configurable/widgets/Proxy.kt @@ -21,13 +21,14 @@ import com.abdownloadmanager.shared.utils.ui.myColors import com.abdownloadmanager.shared.utils.ui.theme.myTextSizes import com.abdownloadmanager.shared.utils.div import com.abdownloadmanager.resources.Res +import com.abdownloadmanager.shared.utils.isValidUrl +import com.abdownloadmanager.shared.utils.proxy.* import com.abdownloadmanager.shared.utils.ui.LocalContentColor import com.abdownloadmanager.shared.utils.ui.widget.MyIcon -import com.abdownloadmanager.shared.utils.proxy.ProxyMode -import com.abdownloadmanager.shared.utils.proxy.ProxyRules -import com.abdownloadmanager.shared.utils.proxy.ProxyWithRules import ir.amirab.downloader.connection.proxy.Proxy import ir.amirab.downloader.connection.proxy.ProxyType +import ir.amirab.util.compose.StringSource +import ir.amirab.util.compose.asStringSource import ir.amirab.util.compose.resources.myStringResource import ir.amirab.util.desktop.DesktopUtils @@ -43,82 +44,50 @@ fun RenderProxyConfig(cfg: ProxyConfigurable, modifier: Modifier) { TitleAndDescription(cfg, true) }, value = { - RenderSpinner( - enabled = enabled, - possibleValues = ProxyMode.usableValues(), - value = value.proxyMode, - onSelect = { - setValue( - value.copy( - proxyMode = it - ) - ) - }, - modifier = Modifier.widthIn(min = 120.dp), - render = { - val text = myStringResource( - when (it) { - ProxyMode.Direct -> Res.string.proxy_no - ProxyMode.UseSystem -> Res.string.proxy_system - ProxyMode.Manual -> Res.string.proxy_manual - } - ) - Text(text) - }, + RenderChangeProxyConfig( + proxyWithRules = value, + setProxyWithRules = { setValue(it) } ) }, - nestedContent = { - AnimatedContent(value.proxyMode.takeIf { enabled }) { - when (it) { - ProxyMode.Direct -> {} - ProxyMode.UseSystem -> { - ActionButton( - myStringResource(Res.string.proxy_open_system_proxy_settings), - onClick = { - DesktopUtils.openSystemProxySettings() - }, - ) - } - - ProxyMode.Manual -> { - RenderManualProxyConfig( - proxyWithRules = value.proxyWithRules, - setProxyWithRules = { - setValue( - value.copy( - proxyWithRules = it - ) - ) - } - ) - } - - null -> {} - } - } - } ) } @Stable private class ProxyEditState( - private val proxyWithRules: ProxyWithRules, - private val setProxyWithRules: (ProxyWithRules) -> Unit, + private val proxyData: ProxyData, + private val setProxyData: (ProxyData) -> Unit, ) { - var proxyType = mutableStateOf(proxyWithRules.proxy.type) + var proxyMode = mutableStateOf(proxyData.proxyMode) - var proxyHost = mutableStateOf(proxyWithRules.proxy.host) - var proxyPort = mutableStateOf(proxyWithRules.proxy.port) + //pac + var pacURL = mutableStateOf(proxyData.pac.uri) - var useAuth = mutableStateOf(proxyWithRules.proxy.username != null) - var proxyUsername = mutableStateOf(proxyWithRules.proxy.username.orEmpty()) - var proxyPassword = mutableStateOf(proxyWithRules.proxy.password.orEmpty()) + //manual + var proxyType = mutableStateOf(proxyData.proxyWithRules.proxy.type) - var excludeURLPatterns = mutableStateOf(proxyWithRules.rules.excludeURLPatterns.joinToString(" ")) + var proxyHost = mutableStateOf(proxyData.proxyWithRules.proxy.host) + var proxyPort = mutableStateOf(proxyData.proxyWithRules.proxy.port) + + var useAuth = mutableStateOf(proxyData.proxyWithRules.proxy.username != null) + var proxyUsername = mutableStateOf(proxyData.proxyWithRules.proxy.username.orEmpty()) + var proxyPassword = mutableStateOf(proxyData.proxyWithRules.proxy.password.orEmpty()) + + var excludeURLPatterns = mutableStateOf(proxyData.proxyWithRules.rules.excludeURLPatterns.joinToString(" ")) val canSave: Boolean by derivedStateOf { - val hostValid = proxyHost.value.isNotBlank() - hostValid + when (proxyMode.value) { + ProxyMode.Direct -> true + ProxyMode.UseSystem -> true + ProxyMode.Manual -> { + val hostValid = proxyHost.value.isNotBlank() + hostValid + } + + ProxyMode.Pac -> { + isValidUrl(pacURL.value) + } + } + } fun save() { @@ -126,20 +95,24 @@ private class ProxyEditState( if (!canSave) { return } - setProxyWithRules( - proxyWithRules.copy( - proxy = Proxy( - type = proxyType.value, - host = proxyHost.value.trim(), - port = proxyPort.value, - username = proxyUsername.value.takeIf { it.isNotEmpty() && useAuth }, - password = proxyPassword.value.takeIf { it.isNotEmpty() && useAuth }, - ), - rules = ProxyRules( - excludeURLPatterns = excludeURLPatterns.value - .split(" ") - .map { it.trim() } - .filterNot { it.isEmpty() }, + setProxyData( + proxyData.copy( + proxyMode = proxyMode.value, + pac = proxyData.pac.copy(pacURL.value), + proxyWithRules = proxyData.proxyWithRules.copy( + proxy = Proxy( + type = proxyType.value, + host = proxyHost.value.trim(), + port = proxyPort.value, + username = proxyUsername.value.takeIf { it.isNotEmpty() && useAuth }, + password = proxyPassword.value.takeIf { it.isNotEmpty() && useAuth }, + ), + rules = ProxyRules( + excludeURLPatterns = excludeURLPatterns.value + .split(" ") + .map { it.trim() } + .filterNot { it.isEmpty() }, + ) ) ) ) @@ -147,27 +120,27 @@ private class ProxyEditState( } @Composable -fun RenderManualProxyConfig( - proxyWithRules: ProxyWithRules, - setProxyWithRules: (ProxyWithRules) -> Unit, +fun RenderChangeProxyConfig( + proxyWithRules: ProxyData, + setProxyWithRules: (ProxyData) -> Unit, ) { - var showManualProxyConfig by remember { + var showProxyConfig by remember { mutableStateOf(false) } ActionButton( myStringResource(Res.string.change_proxy), onClick = { - showManualProxyConfig = true + showProxyConfig = true }, ) - if (showManualProxyConfig) { + if (showProxyConfig) { val dismiss = { - showManualProxyConfig = false + showProxyConfig = false } val state = remember(setProxyWithRules) { ProxyEditState( - proxyWithRules = proxyWithRules, - setProxyWithRules = { + proxyData = proxyWithRules, + setProxyData = { setProxyWithRules(it) dismiss() } @@ -177,6 +150,7 @@ fun RenderManualProxyConfig( } } + @Composable private fun ProxyEditDialog( state: ProxyEditState, @@ -185,142 +159,74 @@ private fun ProxyEditDialog( Dialog( onDismissRequest = (onDismiss), content = { - val (type, setType) = state.proxyType - val (host, setHost) = state.proxyHost - val (port, setPort) = state.proxyPort - val (useAuth, setUseAuth) = state.useAuth - val (username, setUsername) = state.proxyUsername - val (password, setPassword) = state.proxyPassword - val (excludeURLPatterns, setExcludeURLPatterns) = state.excludeURLPatterns - + val (mode, setMode) = state.proxyMode SettingsDialog( headerTitle = myStringResource(Res.string.proxy_change_title), onDismiss = onDismiss, content = { + val shape = RoundedCornerShape(6.dp) Column( Modifier .verticalScroll(rememberScrollState()) ) { - val spacer = @Composable { - Spacer(Modifier.height(8.dp)) - } - DialogConfigItem( - modifier = Modifier, - title = { - Text(myStringResource(Res.string.proxy_type)) - }, - value = { - Multiselect( - selections = ProxyType.entries.toList(), - selectedItem = type, - onSelectionChange = setType, - modifier = Modifier, - render = { - Text( - it.name, - modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp), - ) - }, - selectedColor = LocalContentColor.current / 15, - unselectedAlpha = 0.8f, - ) - } - ) - spacer() - DialogConfigItem( - modifier = Modifier, - title = { - Text(myStringResource(Res.string.address_and_port)) - }, - value = { + Accordion( + possibleValues = ProxyMode.usableValues(), + selectedItem = mode, + renderHeader = { + val selected = it == mode Row( - verticalAlignment = Alignment.CenterVertically, + Modifier + .fillMaxWidth() + .clip(shape) + .clickable { setMode(it) } + .padding(8.dp) ) { - MyTextField( - text = host, - onTextChange = setHost, - placeholder = "127.0.0.1", - modifier = Modifier.weight(1f), - ) - Text(":", Modifier.padding(horizontal = 8.dp)) - IntTextField( - value = port, - onValueChange = setPort, - placeholder = myStringResource(Res.string.port), - range = 1..65535, - modifier = Modifier.width(96.dp), - keyboardOptions = KeyboardOptions(), - textPadding = PaddingValues(8.dp), - shape = RoundedCornerShape(12.dp), + RadioButton( + value = selected, + onValueChange = {}, ) + Spacer(Modifier.width(8.dp)) + Text(it.asStringSource().rememberString()) } - } - ) - spacer() - DialogConfigItem( - modifier = Modifier, - title = { - Row( - modifier = Modifier.onClick { - setUseAuth(!useAuth) + }, + renderContent = { + val cm = Modifier + .fillMaxWidth() + .clip(shape) + .border(1.dp, myColors.onBackground / 0.15f, shape) + .background(myColors.background / 25) + .padding(8.dp) + when (it) { + ProxyMode.Direct -> { + + } + + ProxyMode.UseSystem -> { + Column(cm) { + ActionButton( + myStringResource(Res.string.proxy_open_system_proxy_settings), + onClick = { + DesktopUtils.openSystemProxySettings() + }, + ) + } + } + + ProxyMode.Manual -> { + Column(cm) { + RenderManualConfig(state) + } + } + + ProxyMode.Pac -> { + Column(cm) { + RenderPACConfig(state) + } } - ) { - CheckBox( - value = useAuth, - onValueChange = setUseAuth, - size = 16.dp - ) - Spacer(Modifier.width(8.dp)) - Text(myStringResource(Res.string.use_authentication)) - } - }, - value = { - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - MyTextField( - text = username, - onTextChange = setUsername, - placeholder = myStringResource(Res.string.username), - modifier = Modifier.weight(1f), - enabled = useAuth, - ) - Spacer(Modifier.width(8.dp)) - MyTextField( - text = password, - onTextChange = setPassword, - placeholder = myStringResource(Res.string.password), - modifier = Modifier.weight(1f), - enabled = useAuth, - ) - } - } - ) - spacer() - DialogConfigItem( - modifier = Modifier, - title = { - Row { - Text(myStringResource(Res.string.proxy_do_not_use_proxy_for)) - Spacer(Modifier.width(8.dp)) - com.abdownloadmanager.shared.ui.widget.Help( - myStringResource(Res.string.proxy_do_not_use_proxy_for_description) - ) - } - }, - value = { - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - MyTextField( - text = excludeURLPatterns, - onTextChange = setExcludeURLPatterns, - placeholder = "example.com 192.168.1.*", - modifier = Modifier, - ) } } ) + ProxyConfigSpacer() } }, actions = { @@ -340,6 +246,163 @@ private fun ProxyEditDialog( ) } +@Composable +private fun RenderPACConfig( + state: ProxyEditState, +) { + Column { + val (url, setPacUrl) = state.pacURL + DialogConfigItem( + modifier = Modifier, + title = { + Text(myStringResource(Res.string.proxy_pac_url)) + }, + value = { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + MyTextField( + text = url, + onTextChange = setPacUrl, + placeholder = "http://path/to/file.pac", + modifier = Modifier.weight(1f), + ) + } + } + ) + } +} + +@Composable +private fun RenderManualConfig( + state: ProxyEditState, +) { + val (type, setType) = state.proxyType + val (host, setHost) = state.proxyHost + val (port, setPort) = state.proxyPort + val (useAuth, setUseAuth) = state.useAuth + val (username, setUsername) = state.proxyUsername + val (password, setPassword) = state.proxyPassword + val (excludeURLPatterns, setExcludeURLPatterns) = state.excludeURLPatterns + DialogConfigItem( + modifier = Modifier, + title = { + Text(myStringResource(Res.string.proxy_type)) + }, + value = { + Multiselect( + selections = ProxyType.entries.toList(), + selectedItem = type, + onSelectionChange = setType, + modifier = Modifier, + render = { + Text( + it.name, + modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp), + ) + }, + selectedColor = LocalContentColor.current / 15, + unselectedAlpha = 0.8f, + ) + } + ) + ProxyConfigSpacer() + DialogConfigItem( + modifier = Modifier, + title = { + Text(myStringResource(Res.string.address_and_port)) + }, + value = { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + MyTextField( + text = host, + onTextChange = setHost, + placeholder = "127.0.0.1", + modifier = Modifier.weight(1f), + ) + Text(":", Modifier.padding(horizontal = 8.dp)) + IntTextField( + value = port, + onValueChange = setPort, + placeholder = myStringResource(Res.string.port), + range = 1..65535, + modifier = Modifier.width(96.dp), + keyboardOptions = KeyboardOptions(), + textPadding = PaddingValues(8.dp), + shape = RoundedCornerShape(12.dp), + ) + } + } + ) + ProxyConfigSpacer() + DialogConfigItem( + modifier = Modifier, + title = { + Row( + modifier = Modifier.onClick { + setUseAuth(!useAuth) + } + ) { + CheckBox( + value = useAuth, + onValueChange = setUseAuth, + size = 16.dp + ) + Spacer(Modifier.width(8.dp)) + Text(myStringResource(Res.string.use_authentication)) + } + }, + value = { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + MyTextField( + text = username, + onTextChange = setUsername, + placeholder = myStringResource(Res.string.username), + modifier = Modifier.weight(1f), + enabled = useAuth, + ) + Spacer(Modifier.width(8.dp)) + MyTextField( + text = password, + onTextChange = setPassword, + placeholder = myStringResource(Res.string.password), + modifier = Modifier.weight(1f), + enabled = useAuth, + ) + } + } + ) + ProxyConfigSpacer() + DialogConfigItem( + modifier = Modifier, + title = { + Row { + Text(myStringResource(Res.string.proxy_do_not_use_proxy_for)) + Spacer(Modifier.width(8.dp)) + com.abdownloadmanager.shared.ui.widget.Help( + myStringResource(Res.string.proxy_do_not_use_proxy_for_description) + ) + } + }, + value = { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + MyTextField( + text = excludeURLPatterns, + onTextChange = setExcludeURLPatterns, + placeholder = "example.com 192.168.1.*", + modifier = Modifier, + ) + } + } + ) +} + @Composable private fun SettingsDialog( headerTitle: String, @@ -362,7 +425,6 @@ private fun SettingsDialog( ) .padding(16.dp) .width(450.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -385,7 +447,9 @@ private fun SettingsDialog( ) } Spacer(Modifier.height(8.dp)) - content() + Box(Modifier.weight(1f, false)) { + content() + } actions?.let { Spacer(Modifier.height(8.dp)) Row( @@ -398,6 +462,11 @@ private fun SettingsDialog( } } +@Composable +private fun ProxyConfigSpacer() { + Spacer(Modifier.height(8.dp)) +} + @Composable private fun DialogConfigItem( modifier: Modifier, @@ -426,4 +495,36 @@ private fun DialogConfigItem( } } } -} \ No newline at end of file +} + +private fun ProxyMode.asStringSource(): StringSource { + return when (this) { + ProxyMode.Direct -> Res.string.proxy_no + ProxyMode.UseSystem -> Res.string.proxy_system + ProxyMode.Manual -> Res.string.proxy_manual + ProxyMode.Pac -> Res.string.proxy_pac + }.asStringSource() +} + +@Composable +private fun Accordion( + possibleValues: List, + selectedItem: T, + renderHeader: @Composable (T) -> Unit, + renderContent: @Composable (T) -> Unit, +) { + Column { + possibleValues.forEach { + ExpandableItem( + modifier = Modifier, + isExpanded = selectedItem == it, + header = { + renderHeader(it) + }, + body = { + renderContent(it) + }, + ) + } + } +} diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/utils/proxy/AutoConfigurableProxyProviderForDesktop.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/utils/proxy/AutoConfigurableProxyProviderForDesktop.kt new file mode 100644 index 0000000..f7b7ba8 --- /dev/null +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/utils/proxy/AutoConfigurableProxyProviderForDesktop.kt @@ -0,0 +1,51 @@ +package com.abdownloadmanager.desktop.utils.proxy + +import com.github.markusbernhardt.proxy.selector.misc.BufferedProxySelector +import com.github.markusbernhardt.proxy.selector.misc.ProxyListFallbackSelector +import com.github.markusbernhardt.proxy.selector.pac.PacProxySelector +import com.github.markusbernhardt.proxy.selector.pac.UrlPacScriptSource +import ir.amirab.downloader.connection.proxy.AutoConfigurableProxyProvider +import java.net.ProxySelector + +class AutoConfigurableProxyProviderForDesktop( + private val proxyCachingConfig: ProxyCachingConfig +) : AutoConfigurableProxyProvider { + @Volatile + private var packProxySelector: ProxySelector? = null + + @Volatile + private var lastUsedUri: String? = null + override fun getAutoConfigurableProxy(uri: String): ProxySelector? { + if (lastUsedUri == uri) { + val o = packProxySelector + return o ?: createAndInitializePacProxySelector(uri) + } else { + return createAndInitializePacProxySelector(uri) + } + } + + private fun createAndInitializePacProxySelector(uri: String): ProxySelector { + synchronized(this) { + val s = installBufferingAndFallbackBehaviour(PacProxySelector(UrlPacScriptSource(uri))) + lastUsedUri = uri + packProxySelector = s + return s + } + } + + private fun installBufferingAndFallbackBehaviour(selector: ProxySelector): ProxySelector { + var selector = selector + if (selector is PacProxySelector) { + if (proxyCachingConfig.pacCacheSize > 0) { + selector = BufferedProxySelector( + proxyCachingConfig.pacCacheSize, + proxyCachingConfig.pacCacheTTL, + selector, + proxyCachingConfig.pacCacheScope + ) + } + selector = ProxyListFallbackSelector(selector) + } + return selector + } +} diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/utils/proxy/DesktopSystemProxySelectorProvider.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/utils/proxy/DesktopSystemProxySelectorProvider.kt new file mode 100644 index 0000000..b3010de --- /dev/null +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/utils/proxy/DesktopSystemProxySelectorProvider.kt @@ -0,0 +1,27 @@ +package com.abdownloadmanager.desktop.utils.proxy + +import com.github.markusbernhardt.proxy.ProxySearch +import ir.amirab.downloader.connection.proxy.SystemProxySelectorProvider +import java.net.ProxySelector + +class DesktopSystemProxySelectorProvider( + private val proxyCachingConfig: ProxyCachingConfig +) : SystemProxySelectorProvider { + private val proxySearch by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { + createProxySearch() + } + + private fun createProxySearch(): ProxySearch { + return ProxySearch.getDefaultProxySearch().apply { + setPacCacheSettings( + proxyCachingConfig.pacCacheSize, + proxyCachingConfig.pacCacheTTL, + proxyCachingConfig.pacCacheScope + ) + } + } + + override fun getSystemProxySelector(): ProxySelector? { + return proxySearch.proxySelector + } +} diff --git a/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/utils/proxy/ProxyCachingConfig.kt b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/utils/proxy/ProxyCachingConfig.kt new file mode 100644 index 0000000..ceae377 --- /dev/null +++ b/desktop/app/src/main/kotlin/com/abdownloadmanager/desktop/utils/proxy/ProxyCachingConfig.kt @@ -0,0 +1,17 @@ +package com.abdownloadmanager.desktop.utils.proxy + +import com.github.markusbernhardt.proxy.selector.misc.BufferedProxySelector + +class ProxyCachingConfig( + val pacCacheSize: Int, + val pacCacheTTL: Long, + val pacCacheScope: BufferedProxySelector.CacheScope +) { + companion object { + fun default() = ProxyCachingConfig( + pacCacheSize = 10, + pacCacheTTL = 60 * 60 * 1000, + pacCacheScope = BufferedProxySelector.CacheScope.CACHE_SCOPE_URL + ) + } +} diff --git a/downloader/core/src/main/kotlin/ir/amirab/downloader/connection/OkHttpDownloaderClient.kt b/downloader/core/src/main/kotlin/ir/amirab/downloader/connection/OkHttpDownloaderClient.kt index 04b1ffa..9aec0a2 100644 --- a/downloader/core/src/main/kotlin/ir/amirab/downloader/connection/OkHttpDownloaderClient.kt +++ b/downloader/core/src/main/kotlin/ir/amirab/downloader/connection/OkHttpDownloaderClient.kt @@ -1,8 +1,6 @@ package ir.amirab.downloader.connection -import ir.amirab.downloader.connection.proxy.ProxyStrategy -import ir.amirab.downloader.connection.proxy.ProxyStrategyProvider -import ir.amirab.downloader.connection.proxy.ProxyType +import ir.amirab.downloader.connection.proxy.* import ir.amirab.downloader.connection.response.ResponseInfo import ir.amirab.downloader.downloaditem.IDownloadCredentials import ir.amirab.downloader.utils.await @@ -14,6 +12,8 @@ import java.net.ProxySelector class OkHttpDownloaderClient( private val okHttpClient: OkHttpClient, private val proxyStrategyProvider: ProxyStrategyProvider, + private val systemProxySelectorProvider: SystemProxySelectorProvider, + private val autoConfigurableProxyProvider: AutoConfigurableProxyProvider, ) : DownloaderClient() { private fun newCall( downloadCredentials: IDownloadCredentials, @@ -67,10 +67,22 @@ class OkHttpDownloaderClient( ProxyStrategy.Direct -> return this ProxyStrategy.UseSystem -> { newBuilder() - .proxySelector(ProxySelector.getDefault()) + .proxySelector( + systemProxySelectorProvider.getSystemProxySelector() + ?: ProxySelector.getDefault() + ) .build() } - + is ProxyStrategy.ByScript -> { + val proxySelector = autoConfigurableProxyProvider.getAutoConfigurableProxy(strategy.scriptPath) + if (proxySelector != null) { + newBuilder() + .proxySelector(proxySelector) + .build() + } else { + this + } + } is ProxyStrategy.ManualProxy -> { val proxy = strategy.proxy return newBuilder() @@ -158,4 +170,4 @@ class OkHttpDownloaderClient( responseInfo = createFileInfo(response) ) } -} \ No newline at end of file +} diff --git a/downloader/core/src/main/kotlin/ir/amirab/downloader/connection/proxy/AutoConfigurableProxyProvider.kt b/downloader/core/src/main/kotlin/ir/amirab/downloader/connection/proxy/AutoConfigurableProxyProvider.kt new file mode 100644 index 0000000..63b1f76 --- /dev/null +++ b/downloader/core/src/main/kotlin/ir/amirab/downloader/connection/proxy/AutoConfigurableProxyProvider.kt @@ -0,0 +1,10 @@ +package ir.amirab.downloader.connection.proxy + +import java.net.ProxySelector +import java.net.URI + +interface AutoConfigurableProxyProvider { + fun getAutoConfigurableProxy( + uri: String + ): ProxySelector? +} diff --git a/downloader/core/src/main/kotlin/ir/amirab/downloader/connection/proxy/ProxyStrategy.kt b/downloader/core/src/main/kotlin/ir/amirab/downloader/connection/proxy/ProxyStrategy.kt index 3eb9438..df0993f 100644 --- a/downloader/core/src/main/kotlin/ir/amirab/downloader/connection/proxy/ProxyStrategy.kt +++ b/downloader/core/src/main/kotlin/ir/amirab/downloader/connection/proxy/ProxyStrategy.kt @@ -4,4 +4,5 @@ sealed interface ProxyStrategy { data object Direct : ProxyStrategy data object UseSystem : ProxyStrategy data class ManualProxy(val proxy: Proxy) : ProxyStrategy -} \ No newline at end of file + data class ByScript(val scriptPath: String) : ProxyStrategy +} diff --git a/downloader/core/src/main/kotlin/ir/amirab/downloader/connection/proxy/SystemProxySelectorProvider.kt b/downloader/core/src/main/kotlin/ir/amirab/downloader/connection/proxy/SystemProxySelectorProvider.kt new file mode 100644 index 0000000..903ad89 --- /dev/null +++ b/downloader/core/src/main/kotlin/ir/amirab/downloader/connection/proxy/SystemProxySelectorProvider.kt @@ -0,0 +1,7 @@ +package ir.amirab.downloader.connection.proxy + +import java.net.ProxySelector + +interface SystemProxySelectorProvider { + fun getSystemProxySelector(): ProxySelector? +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c1fa5ec..f6c380c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -29,6 +29,7 @@ kotlinFileWatcher = "1.3.0" markdownRenderer = "0.27.0" autoServiceKsp = "1.2.0" autoService = "1.1.1" +proxyVole = "1.1.6" [libraries] @@ -129,6 +130,7 @@ autoService-ksp = { module = "dev.zacsweers.autoservice:auto-service-ksp", versi autoService-annoations = { module = "com.google.auto.service:auto-service-annotations", version.ref = "autoService" } systemTray = "com.dorkbox:SystemTray:4.4" +proxyVole = { module = "org.bidib.com.github.markusbernhardt:proxy-vole", version.ref = "proxyVole" } [plugins] kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } diff --git a/shared/app-utils/src/main/kotlin/com/abdownloadmanager/shared/utils/proxy/Proxy.kt b/shared/app-utils/src/main/kotlin/com/abdownloadmanager/shared/utils/proxy/Proxy.kt index 27c866d..ed2cfe2 100644 --- a/shared/app-utils/src/main/kotlin/com/abdownloadmanager/shared/utils/proxy/Proxy.kt +++ b/shared/app-utils/src/main/kotlin/com/abdownloadmanager/shared/utils/proxy/Proxy.kt @@ -15,6 +15,15 @@ data class ProxyWithRules( val rules: ProxyRules, ) +@Serializable +data class PACProxy( + val uri: String,// an uri to get script path of the PAC +) { + companion object { + fun default() = PACProxy("http://localhost/some.pac") + } +} + enum class ProxyMode { @SerialName("direct") Direct, @@ -23,7 +32,10 @@ enum class ProxyMode { UseSystem, @SerialName("manual") - Manual; + Manual, + + @SerialName("pac") + Pac; companion object { fun usableValues(): List { @@ -31,6 +43,8 @@ enum class ProxyMode { // so we filter it for now. return listOf( Direct, + UseSystem, + Pac, Manual, ) } @@ -41,7 +55,10 @@ enum class ProxyMode { @Serializable data class ProxyData( val proxyMode: ProxyMode, + //manual proxy config val proxyWithRules: ProxyWithRules, + //configuration script config + val pac: PACProxy, ) { companion object { fun default() = ProxyData( @@ -49,7 +66,8 @@ data class ProxyData( proxyWithRules = ProxyWithRules( proxy = Proxy.default(), rules = ProxyRules(emptyList()) - ) + ), + pac = PACProxy.default() ) } -} \ No newline at end of file +} diff --git a/shared/app-utils/src/main/kotlin/com/abdownloadmanager/shared/utils/proxy/ProxyManager.kt b/shared/app-utils/src/main/kotlin/com/abdownloadmanager/shared/utils/proxy/ProxyManager.kt index 3b6b49b..5c6562e 100644 --- a/shared/app-utils/src/main/kotlin/com/abdownloadmanager/shared/utils/proxy/ProxyManager.kt +++ b/shared/app-utils/src/main/kotlin/com/abdownloadmanager/shared/utils/proxy/ProxyManager.kt @@ -1,5 +1,6 @@ package com.abdownloadmanager.shared.utils.proxy +import com.abdownloadmanager.shared.utils.isValidUrl import ir.amirab.downloader.connection.proxy.Proxy import ir.amirab.downloader.connection.proxy.ProxyStrategy import ir.amirab.downloader.connection.proxy.ProxyStrategyProvider @@ -18,6 +19,9 @@ class ProxyManager( Authenticator.setDefault(mySocksProxyAuthenticator) } + /** + * I don't like this it's better to improve this later + */ private fun getProxyModeForThisURL(url: String): ProxyStrategy { val usingProxy = proxyData.value return when (usingProxy.proxyMode) { @@ -31,6 +35,14 @@ class ProxyManager( ProxyStrategy.Direct } } + ProxyMode.Pac -> { + val pacURI = usingProxy.pac.uri + if (isValidUrl(pacURI)) { + ProxyStrategy.ByScript(pacURI) + } else { + ProxyStrategy.Direct + } + } } } @@ -69,4 +81,4 @@ private class MySocksProxyAuthenticator( } return null } -} \ No newline at end of file +} diff --git a/shared/app/src/main/kotlin/com/abdownloadmanager/shared/ui/widget/RadioButton.kt b/shared/app/src/main/kotlin/com/abdownloadmanager/shared/ui/widget/RadioButton.kt new file mode 100644 index 0000000..de7855f --- /dev/null +++ b/shared/app/src/main/kotlin/com/abdownloadmanager/shared/ui/widget/RadioButton.kt @@ -0,0 +1,89 @@ +package com.abdownloadmanager.shared.ui.widget + +import com.abdownloadmanager.shared.utils.ui.LocalContentColor +import com.abdownloadmanager.shared.utils.ui.widget.MyIcon +import com.abdownloadmanager.shared.utils.ui.icon.MyIcons +import com.abdownloadmanager.shared.utils.ui.myColors +import ir.amirab.util.ifThen +import com.abdownloadmanager.shared.utils.div +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.selection.triStateToggleable +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.state.ToggleableState +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Composable +fun RadioButton( + value: Boolean, + onValueChange: (Boolean) -> Unit, + enabled: Boolean = true, + modifier: Modifier = Modifier, + size: Dp = 18.dp, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + uncheckedAlpha: Float = 0.25f, +) { + val shape = CircleShape + Box( + modifier + .ifThen(!enabled) { + alpha(0.5f) + } + .size(size) + .clip(shape) + .triStateToggleable( + state = ToggleableState(value), + enabled = enabled, + role = Role.RadioButton, + interactionSource = interactionSource, + indication = null, + onClick = { onValueChange(!value) }, + ) + ) { + Spacer( + Modifier.matchParentSize() + .border( + 1.dp, + if (value) { + myColors.primaryGradient + } else { + SolidColor(LocalContentColor.current / uncheckedAlpha) + }, + shape + ) + ) + AnimatedContent( + value, + transitionSpec = { + val tween = tween(220) + fadeIn(tween) togetherWith fadeOut(tween) + } + ) { + val m = Modifier + .fillMaxSize() + .alpha(animateFloatAsState(if (value) 1f else 0f).value) + .padding(4.dp) + .clip(shape) + .background(myColors.primaryGradient) + Spacer(m) + } + } +} diff --git a/shared/resources/src/main/resources/com/abdownloadmanager/resources/locales/en_US.properties b/shared/resources/src/main/resources/com/abdownloadmanager/resources/locales/en_US.properties index baf6161..a11622c 100644 --- a/shared/resources/src/main/resources/com/abdownloadmanager/resources/locales/en_US.properties +++ b/shared/resources/src/main/resources/com/abdownloadmanager/resources/locales/en_US.properties @@ -195,6 +195,7 @@ settings_use_proxy_description=Use proxy for downloading files settings_use_proxy_describe_no_proxy=No Proxy will be used settings_use_proxy_describe_system_proxy=System Proxy will be used settings_use_proxy_describe_manual_proxy="{{value}}" will be used +settings_use_proxy_describe_pac_proxy=pac file "{{value}}" will be used settings_track_deleted_files_on_disk=Track Deleted Files On Disk settings_track_deleted_files_on_disk_description=Automatically remove files from the list when they are deleted or moved from the download directory. settings_download_speed_unit=Download Speed Unit @@ -317,6 +318,8 @@ change_proxy=Change Proxy proxy_no=No Proxy proxy_system=System Proxy proxy_manual=Manual Proxy +proxy_pac=Proxy Auto Configuration +proxy_pac_url=Proxy Auto Configuration URL address=Address port=Port address_and_port=Address & Port