feature/system proxy and pac support (#371)

* add system proxy support

* add proxy pac support
This commit is contained in:
AmirHossein Abdolmotallebi 2025-01-17 00:18:02 +03:30 committed by GitHub
parent f50e3e10e8
commit 3db6306177
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 604 additions and 230 deletions

View File

@ -49,10 +49,9 @@ dependencies {
implementation(libs.osThemeDetector) { implementation(libs.osThemeDetector) {
exclude(group = "net.java.dev.jna") exclude(group = "net.java.dev.jna")
} }
implementation(libs.proxyVole) {
// at the moment I don't use jna but some libraries does exclude(group = "net.java.dev.jna")
// filekit and osThemeDetector both use jna but with different versions }
// I excluded jna from both of them and add it here!
implementation(libs.jna.core) implementation(libs.jna.core)
implementation(libs.jna.platform) implementation(libs.jna.platform)

View File

@ -18,6 +18,9 @@ import com.abdownloadmanager.shared.utils.ui.theme.ISystemThemeDetector
import com.abdownloadmanager.desktop.utils.* import com.abdownloadmanager.desktop.utils.*
import com.abdownloadmanager.desktop.utils.native_messaging.NativeMessaging import com.abdownloadmanager.desktop.utils.native_messaging.NativeMessaging
import com.abdownloadmanager.desktop.utils.native_messaging.NativeMessagingManifestApplier 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.decompose.DefaultComponentContext
import com.arkivanov.essenty.lifecycle.LifecycleRegistry import com.arkivanov.essenty.lifecycle.LifecycleRegistry
import ir.amirab.downloader.DownloadManagerMinimalControl 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.IProxyStorage
import com.abdownloadmanager.shared.utils.proxy.ProxyData import com.abdownloadmanager.shared.utils.proxy.ProxyData
import com.abdownloadmanager.shared.utils.proxy.ProxyManager 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.ProxyStrategyProvider
import ir.amirab.downloader.connection.proxy.SystemProxySelectorProvider
import ir.amirab.downloader.monitor.IDownloadMonitor import ir.amirab.downloader.monitor.IDownloadMonitor
import ir.amirab.downloader.utils.EmptyFileCreator import ir.amirab.downloader.utils.EmptyFileCreator
import ir.amirab.util.compose.localizationmanager.LanguageManager import ir.amirab.util.compose.localizationmanager.LanguageManager
@ -112,6 +117,15 @@ val downloaderModule = module {
get() get()
) )
}.bind<ProxyStrategyProvider>() }.bind<ProxyStrategyProvider>()
single {
ProxyCachingConfig.default()
}
single<AutoConfigurableProxyProvider> {
AutoConfigurableProxyProviderForDesktop(get())
}
single<SystemProxySelectorProvider> {
DesktopSystemProxySelectorProvider(get())
}
single<DownloaderClient> { single<DownloaderClient> {
OkHttpDownloaderClient( OkHttpDownloaderClient(
OkHttpClient OkHttpClient
@ -121,7 +135,9 @@ val downloaderModule = module {
maxRequests = Int.MAX_VALUE maxRequests = Int.MAX_VALUE
maxRequestsPerHost = Int.MAX_VALUE maxRequestsPerHost = Int.MAX_VALUE
}).build(), }).build(),
get() get(),
get(),
get(),
) )
} }
single { single {
@ -341,4 +357,4 @@ object Di : KoinComponent {
modules(appModule) modules(appModule)
} }
} }
} }

View File

@ -231,6 +231,15 @@ fun proxyConfig(proxyManager: ProxyManager, scope: CoroutineScope): ProxyConfigu
value = it.proxyWithRules.proxy.run { "$type $host:$port" } 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 { val configurables by derivedStateOf {
allConfigs[currentPage] allConfigs[currentPage]
} }
} }

View File

@ -21,13 +21,14 @@ import com.abdownloadmanager.shared.utils.ui.myColors
import com.abdownloadmanager.shared.utils.ui.theme.myTextSizes import com.abdownloadmanager.shared.utils.ui.theme.myTextSizes
import com.abdownloadmanager.shared.utils.div import com.abdownloadmanager.shared.utils.div
import com.abdownloadmanager.resources.Res 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.LocalContentColor
import com.abdownloadmanager.shared.utils.ui.widget.MyIcon 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.Proxy
import ir.amirab.downloader.connection.proxy.ProxyType 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.compose.resources.myStringResource
import ir.amirab.util.desktop.DesktopUtils import ir.amirab.util.desktop.DesktopUtils
@ -43,82 +44,50 @@ fun RenderProxyConfig(cfg: ProxyConfigurable, modifier: Modifier) {
TitleAndDescription(cfg, true) TitleAndDescription(cfg, true)
}, },
value = { value = {
RenderSpinner( RenderChangeProxyConfig(
enabled = enabled, proxyWithRules = value,
possibleValues = ProxyMode.usableValues(), setProxyWithRules = { setValue(it) }
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)
},
) )
}, },
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 @Stable
private class ProxyEditState( private class ProxyEditState(
private val proxyWithRules: ProxyWithRules, private val proxyData: ProxyData,
private val setProxyWithRules: (ProxyWithRules) -> Unit, private val setProxyData: (ProxyData) -> Unit,
) { ) {
var proxyType = mutableStateOf(proxyWithRules.proxy.type) var proxyMode = mutableStateOf(proxyData.proxyMode)
var proxyHost = mutableStateOf(proxyWithRules.proxy.host) //pac
var proxyPort = mutableStateOf(proxyWithRules.proxy.port) var pacURL = mutableStateOf(proxyData.pac.uri)
var useAuth = mutableStateOf(proxyWithRules.proxy.username != null) //manual
var proxyUsername = mutableStateOf(proxyWithRules.proxy.username.orEmpty()) var proxyType = mutableStateOf(proxyData.proxyWithRules.proxy.type)
var proxyPassword = mutableStateOf(proxyWithRules.proxy.password.orEmpty())
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 canSave: Boolean by derivedStateOf {
val hostValid = proxyHost.value.isNotBlank() when (proxyMode.value) {
hostValid ProxyMode.Direct -> true
ProxyMode.UseSystem -> true
ProxyMode.Manual -> {
val hostValid = proxyHost.value.isNotBlank()
hostValid
}
ProxyMode.Pac -> {
isValidUrl(pacURL.value)
}
}
} }
fun save() { fun save() {
@ -126,20 +95,24 @@ private class ProxyEditState(
if (!canSave) { if (!canSave) {
return return
} }
setProxyWithRules( setProxyData(
proxyWithRules.copy( proxyData.copy(
proxy = Proxy( proxyMode = proxyMode.value,
type = proxyType.value, pac = proxyData.pac.copy(pacURL.value),
host = proxyHost.value.trim(), proxyWithRules = proxyData.proxyWithRules.copy(
port = proxyPort.value, proxy = Proxy(
username = proxyUsername.value.takeIf { it.isNotEmpty() && useAuth }, type = proxyType.value,
password = proxyPassword.value.takeIf { it.isNotEmpty() && useAuth }, host = proxyHost.value.trim(),
), port = proxyPort.value,
rules = ProxyRules( username = proxyUsername.value.takeIf { it.isNotEmpty() && useAuth },
excludeURLPatterns = excludeURLPatterns.value password = proxyPassword.value.takeIf { it.isNotEmpty() && useAuth },
.split(" ") ),
.map { it.trim() } rules = ProxyRules(
.filterNot { it.isEmpty() }, excludeURLPatterns = excludeURLPatterns.value
.split(" ")
.map { it.trim() }
.filterNot { it.isEmpty() },
)
) )
) )
) )
@ -147,27 +120,27 @@ private class ProxyEditState(
} }
@Composable @Composable
fun RenderManualProxyConfig( fun RenderChangeProxyConfig(
proxyWithRules: ProxyWithRules, proxyWithRules: ProxyData,
setProxyWithRules: (ProxyWithRules) -> Unit, setProxyWithRules: (ProxyData) -> Unit,
) { ) {
var showManualProxyConfig by remember { var showProxyConfig by remember {
mutableStateOf(false) mutableStateOf(false)
} }
ActionButton( ActionButton(
myStringResource(Res.string.change_proxy), myStringResource(Res.string.change_proxy),
onClick = { onClick = {
showManualProxyConfig = true showProxyConfig = true
}, },
) )
if (showManualProxyConfig) { if (showProxyConfig) {
val dismiss = { val dismiss = {
showManualProxyConfig = false showProxyConfig = false
} }
val state = remember(setProxyWithRules) { val state = remember(setProxyWithRules) {
ProxyEditState( ProxyEditState(
proxyWithRules = proxyWithRules, proxyData = proxyWithRules,
setProxyWithRules = { setProxyData = {
setProxyWithRules(it) setProxyWithRules(it)
dismiss() dismiss()
} }
@ -177,6 +150,7 @@ fun RenderManualProxyConfig(
} }
} }
@Composable @Composable
private fun ProxyEditDialog( private fun ProxyEditDialog(
state: ProxyEditState, state: ProxyEditState,
@ -185,142 +159,74 @@ private fun ProxyEditDialog(
Dialog( Dialog(
onDismissRequest = (onDismiss), onDismissRequest = (onDismiss),
content = { content = {
val (type, setType) = state.proxyType val (mode, setMode) = state.proxyMode
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
SettingsDialog( SettingsDialog(
headerTitle = myStringResource(Res.string.proxy_change_title), headerTitle = myStringResource(Res.string.proxy_change_title),
onDismiss = onDismiss, onDismiss = onDismiss,
content = { content = {
val shape = RoundedCornerShape(6.dp)
Column( Column(
Modifier Modifier
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
val spacer = @Composable { Accordion(
Spacer(Modifier.height(8.dp)) possibleValues = ProxyMode.usableValues(),
} selectedItem = mode,
DialogConfigItem( renderHeader = {
modifier = Modifier, val selected = it == mode
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 = {
Row( Row(
verticalAlignment = Alignment.CenterVertically, Modifier
.fillMaxWidth()
.clip(shape)
.clickable { setMode(it) }
.padding(8.dp)
) { ) {
MyTextField( RadioButton(
text = host, value = selected,
onTextChange = setHost, onValueChange = {},
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),
) )
Spacer(Modifier.width(8.dp))
Text(it.asStringSource().rememberString())
} }
} },
) renderContent = {
spacer() val cm = Modifier
DialogConfigItem( .fillMaxWidth()
modifier = Modifier, .clip(shape)
title = { .border(1.dp, myColors.onBackground / 0.15f, shape)
Row( .background(myColors.background / 25)
modifier = Modifier.onClick { .padding(8.dp)
setUseAuth(!useAuth) 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 = { 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 @Composable
private fun SettingsDialog( private fun SettingsDialog(
headerTitle: String, headerTitle: String,
@ -362,7 +425,6 @@ private fun SettingsDialog(
) )
.padding(16.dp) .padding(16.dp)
.width(450.dp), .width(450.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@ -385,7 +447,9 @@ private fun SettingsDialog(
) )
} }
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
content() Box(Modifier.weight(1f, false)) {
content()
}
actions?.let { actions?.let {
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
Row( Row(
@ -398,6 +462,11 @@ private fun SettingsDialog(
} }
} }
@Composable
private fun ProxyConfigSpacer() {
Spacer(Modifier.height(8.dp))
}
@Composable @Composable
private fun DialogConfigItem( private fun DialogConfigItem(
modifier: Modifier, modifier: Modifier,
@ -426,4 +495,36 @@ private fun DialogConfigItem(
} }
} }
} }
} }
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 <T> Accordion(
possibleValues: List<T>,
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)
},
)
}
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
)
}
}

View File

@ -1,8 +1,6 @@
package ir.amirab.downloader.connection package ir.amirab.downloader.connection
import ir.amirab.downloader.connection.proxy.ProxyStrategy import ir.amirab.downloader.connection.proxy.*
import ir.amirab.downloader.connection.proxy.ProxyStrategyProvider
import ir.amirab.downloader.connection.proxy.ProxyType
import ir.amirab.downloader.connection.response.ResponseInfo import ir.amirab.downloader.connection.response.ResponseInfo
import ir.amirab.downloader.downloaditem.IDownloadCredentials import ir.amirab.downloader.downloaditem.IDownloadCredentials
import ir.amirab.downloader.utils.await import ir.amirab.downloader.utils.await
@ -14,6 +12,8 @@ import java.net.ProxySelector
class OkHttpDownloaderClient( class OkHttpDownloaderClient(
private val okHttpClient: OkHttpClient, private val okHttpClient: OkHttpClient,
private val proxyStrategyProvider: ProxyStrategyProvider, private val proxyStrategyProvider: ProxyStrategyProvider,
private val systemProxySelectorProvider: SystemProxySelectorProvider,
private val autoConfigurableProxyProvider: AutoConfigurableProxyProvider,
) : DownloaderClient() { ) : DownloaderClient() {
private fun newCall( private fun newCall(
downloadCredentials: IDownloadCredentials, downloadCredentials: IDownloadCredentials,
@ -67,10 +67,22 @@ class OkHttpDownloaderClient(
ProxyStrategy.Direct -> return this ProxyStrategy.Direct -> return this
ProxyStrategy.UseSystem -> { ProxyStrategy.UseSystem -> {
newBuilder() newBuilder()
.proxySelector(ProxySelector.getDefault()) .proxySelector(
systemProxySelectorProvider.getSystemProxySelector()
?: ProxySelector.getDefault()
)
.build() .build()
} }
is ProxyStrategy.ByScript -> {
val proxySelector = autoConfigurableProxyProvider.getAutoConfigurableProxy(strategy.scriptPath)
if (proxySelector != null) {
newBuilder()
.proxySelector(proxySelector)
.build()
} else {
this
}
}
is ProxyStrategy.ManualProxy -> { is ProxyStrategy.ManualProxy -> {
val proxy = strategy.proxy val proxy = strategy.proxy
return newBuilder() return newBuilder()
@ -158,4 +170,4 @@ class OkHttpDownloaderClient(
responseInfo = createFileInfo(response) responseInfo = createFileInfo(response)
) )
} }
} }

View File

@ -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?
}

View File

@ -4,4 +4,5 @@ sealed interface ProxyStrategy {
data object Direct : ProxyStrategy data object Direct : ProxyStrategy
data object UseSystem : ProxyStrategy data object UseSystem : ProxyStrategy
data class ManualProxy(val proxy: Proxy) : ProxyStrategy data class ManualProxy(val proxy: Proxy) : ProxyStrategy
} data class ByScript(val scriptPath: String) : ProxyStrategy
}

View File

@ -0,0 +1,7 @@
package ir.amirab.downloader.connection.proxy
import java.net.ProxySelector
interface SystemProxySelectorProvider {
fun getSystemProxySelector(): ProxySelector?
}

View File

@ -29,6 +29,7 @@ kotlinFileWatcher = "1.3.0"
markdownRenderer = "0.27.0" markdownRenderer = "0.27.0"
autoServiceKsp = "1.2.0" autoServiceKsp = "1.2.0"
autoService = "1.1.1" autoService = "1.1.1"
proxyVole = "1.1.6"
[libraries] [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" } autoService-annoations = { module = "com.google.auto.service:auto-service-annotations", version.ref = "autoService" }
systemTray = "com.dorkbox:SystemTray:4.4" systemTray = "com.dorkbox:SystemTray:4.4"
proxyVole = { module = "org.bidib.com.github.markusbernhardt:proxy-vole", version.ref = "proxyVole" }
[plugins] [plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }

View File

@ -15,6 +15,15 @@ data class ProxyWithRules(
val rules: ProxyRules, 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 { enum class ProxyMode {
@SerialName("direct") @SerialName("direct")
Direct, Direct,
@ -23,7 +32,10 @@ enum class ProxyMode {
UseSystem, UseSystem,
@SerialName("manual") @SerialName("manual")
Manual; Manual,
@SerialName("pac")
Pac;
companion object { companion object {
fun usableValues(): List<ProxyMode> { fun usableValues(): List<ProxyMode> {
@ -31,6 +43,8 @@ enum class ProxyMode {
// so we filter it for now. // so we filter it for now.
return listOf( return listOf(
Direct, Direct,
UseSystem,
Pac,
Manual, Manual,
) )
} }
@ -41,7 +55,10 @@ enum class ProxyMode {
@Serializable @Serializable
data class ProxyData( data class ProxyData(
val proxyMode: ProxyMode, val proxyMode: ProxyMode,
//manual proxy config
val proxyWithRules: ProxyWithRules, val proxyWithRules: ProxyWithRules,
//configuration script config
val pac: PACProxy,
) { ) {
companion object { companion object {
fun default() = ProxyData( fun default() = ProxyData(
@ -49,7 +66,8 @@ data class ProxyData(
proxyWithRules = ProxyWithRules( proxyWithRules = ProxyWithRules(
proxy = Proxy.default(), proxy = Proxy.default(),
rules = ProxyRules(emptyList()) rules = ProxyRules(emptyList())
) ),
pac = PACProxy.default()
) )
} }
} }

View File

@ -1,5 +1,6 @@
package com.abdownloadmanager.shared.utils.proxy 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.Proxy
import ir.amirab.downloader.connection.proxy.ProxyStrategy import ir.amirab.downloader.connection.proxy.ProxyStrategy
import ir.amirab.downloader.connection.proxy.ProxyStrategyProvider import ir.amirab.downloader.connection.proxy.ProxyStrategyProvider
@ -18,6 +19,9 @@ class ProxyManager(
Authenticator.setDefault(mySocksProxyAuthenticator) Authenticator.setDefault(mySocksProxyAuthenticator)
} }
/**
* I don't like this it's better to improve this later
*/
private fun getProxyModeForThisURL(url: String): ProxyStrategy { private fun getProxyModeForThisURL(url: String): ProxyStrategy {
val usingProxy = proxyData.value val usingProxy = proxyData.value
return when (usingProxy.proxyMode) { return when (usingProxy.proxyMode) {
@ -31,6 +35,14 @@ class ProxyManager(
ProxyStrategy.Direct 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 return null
} }
} }

View File

@ -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<Float>(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)
}
}
}

View File

@ -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_no_proxy=No Proxy will be used
settings_use_proxy_describe_system_proxy=System 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_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=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_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 settings_download_speed_unit=Download Speed Unit
@ -317,6 +318,8 @@ change_proxy=Change Proxy
proxy_no=No Proxy proxy_no=No Proxy
proxy_system=System Proxy proxy_system=System Proxy
proxy_manual=Manual Proxy proxy_manual=Manual Proxy
proxy_pac=Proxy Auto Configuration
proxy_pac_url=Proxy Auto Configuration URL
address=Address address=Address
port=Port port=Port
address_and_port=Address & Port address_and_port=Address & Port