mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 19:11:08 +00:00
Add an optional HTTP proxy for downloads (#1474)
* Add an optional HTTP proxy for downloads Downloads only reached GitHub directly or through URL-rewrite mirrors, which does not help where a local proxy is the workable route. Setup now takes a host:port HTTP proxy that every Downloader connection uses, independently of the download source mode. * Reference the Kotlin object instance from Java Java cannot call a Kotlin object member on the class name without @JvmStatic; the surrounding code already uses INSTANCE. * Assert the proxy host literal without a reverse lookup InetSocketAddress.getHostName reverse-resolves on some JVMs (127.0.0.1 became localhost on CI); hostString is the literal. * Pass the app proxy into the guest environment The Steam client installer and the guest package tools fetch with plain curl against their own CDNs, outside Downloader. Export the usual proxy variables for them when the app proxy is set; proot shares the host network, so a localhost proxy is reachable as-is. --------- Co-authored-by: VIIII4 <VIIII4@users.noreply.github.com>
This commit is contained in:
1 parent
2be08bc67d
commit
c6bc8aef04
9 files changed
+100
-1
No files matched your search
@@ -170,6 +170,7 @@ class FrontEndActivity : ComponentActivity() {
|
||||
private var downloadSource by mutableStateOf(DownloadSource.MODE_AUTO)
|
||||
private var downloadMirror by mutableStateOf("")
|
||||
private var downloadPrefix by mutableStateOf("")
|
||||
private var downloadProxy by mutableStateOf("")
|
||||
private var animationsEnabled by mutableStateOf(true)
|
||||
private var launcherIcon by mutableStateOf(LauncherIcon.DARK)
|
||||
private var gameStoresEnabled by mutableStateOf(true)
|
||||
@@ -768,6 +769,7 @@ class FrontEndActivity : ComponentActivity() {
|
||||
downloadSource = DownloadSource.mode(this)
|
||||
downloadMirror = DownloadSource.mirror(this)
|
||||
downloadPrefix = DownloadSource.custom(this)
|
||||
downloadProxy = DownloadSource.proxyHostPort(this)
|
||||
DownloadSource.probe()
|
||||
animationsEnabled = SessionPrefs.animationsEnabled(this)
|
||||
LauncherIcon.repair(this, intent?.component)
|
||||
@@ -833,6 +835,7 @@ class FrontEndActivity : ComponentActivity() {
|
||||
downloadSource = downloadSource,
|
||||
downloadMirror = downloadMirror,
|
||||
downloadPrefix = downloadPrefix,
|
||||
downloadProxy = downloadProxy,
|
||||
animationsEnabled = animationsEnabled,
|
||||
launcherIcon = launcherIcon,
|
||||
gameStoresEnabled = gameStoresEnabled,
|
||||
@@ -1033,6 +1036,10 @@ class FrontEndActivity : ComponentActivity() {
|
||||
DownloadSource.setCustom(this, prefix)
|
||||
downloadPrefix = DownloadSource.custom(this)
|
||||
},
|
||||
onDownloadProxy = { value ->
|
||||
DownloadSource.setProxy(this, value)
|
||||
downloadProxy = DownloadSource.proxyHostPort(this)
|
||||
},
|
||||
onHomeApp = { manageHomeApp() },
|
||||
onHomeScreen = { on ->
|
||||
HomeApp.setHomeScreenEnabled(this, on)
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.droiddeck.launcher.core
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
import java.net.URL
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.CountDownLatch
|
||||
@@ -20,6 +22,7 @@ object DownloadSource {
|
||||
private const val KEY_MODE = "mode"
|
||||
private const val KEY_MIRROR = "mirror"
|
||||
private const val KEY_CUSTOM = "custom"
|
||||
private const val KEY_PROXY = "proxy"
|
||||
private const val KEY_FASTEST = "fastest"
|
||||
|
||||
private const val PROBE = "https://raw.githubusercontent.com/Droid-Deck/DroidDeck/main/README.md"
|
||||
@@ -175,6 +178,22 @@ object DownloadSource {
|
||||
prefs(context)?.edit()?.putString(KEY_CUSTOM, normalizePrefix(prefix))?.apply()
|
||||
}
|
||||
|
||||
fun proxyHostPort(context: Context): String = prefs(context)?.getString(KEY_PROXY, "") ?: ""
|
||||
|
||||
fun setProxy(context: Context, value: String) {
|
||||
prefs(context)?.edit()?.putString(KEY_PROXY, normalizeProxy(value))?.apply()
|
||||
}
|
||||
|
||||
/** An HTTP proxy every Downloader connection uses, or null when none is configured. */
|
||||
fun httpProxy(): Proxy? = proxyOf(prefs()?.getString(KEY_PROXY, "") ?: "")
|
||||
|
||||
internal fun proxyOf(value: String): Proxy? {
|
||||
val host = value.substringBefore(':')
|
||||
val port = value.substringAfter(':', "").toIntOrNull() ?: return null
|
||||
if (host.isEmpty()) return null
|
||||
return Proxy(Proxy.Type.HTTP, InetSocketAddress(host, port))
|
||||
}
|
||||
|
||||
fun reset(context: Context) {
|
||||
lastGood = null
|
||||
prefs(context)?.edit()?.clear()?.apply()
|
||||
@@ -205,4 +224,11 @@ object DownloadSource {
|
||||
if (trimmed.isEmpty() || trimmed.startsWith("http://")) return ""
|
||||
return if (trimmed.startsWith("https://")) trimmed else "https://$trimmed"
|
||||
}
|
||||
|
||||
internal fun normalizeProxy(value: String): String {
|
||||
val trimmed = value.trim().removePrefix("http://").trimEnd('/')
|
||||
val host = trimmed.substringBefore(':')
|
||||
val port = trimmed.substringAfter(':', "").toIntOrNull() ?: 0
|
||||
return if (host.isNotEmpty() && port in 1..65535) "$host:$port" else ""
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.Proxy;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
@@ -277,7 +278,10 @@ public final class Downloader {
|
||||
}
|
||||
|
||||
private static HttpURLConnection open(String url, int readTimeoutMs) throws java.io.IOException {
|
||||
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
|
||||
// An explicit Proxy routes the connection through the app setting instead of the system-wide proxy.
|
||||
Proxy proxy = DownloadSource.INSTANCE.httpProxy();
|
||||
HttpURLConnection connection = (HttpURLConnection) (proxy != null
|
||||
? new URL(url).openConnection(proxy) : new URL(url).openConnection());
|
||||
connection.setConnectTimeout(CONNECT_TIMEOUT_MS);
|
||||
connection.setReadTimeout(readTimeoutMs);
|
||||
connection.setInstanceFollowRedirects(true);
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.droiddeck.launcher.runtime
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Environment
|
||||
import com.droiddeck.launcher.core.DownloadSource
|
||||
import com.droiddeck.launcher.core.HostProcess
|
||||
import com.droiddeck.launcher.session.OrphanReaper
|
||||
import com.droiddeck.launcher.session.SessionFiles
|
||||
@@ -45,6 +46,15 @@ object GuestCommand {
|
||||
"DROIDDECK_BWRAP_SOCKET=${BwrapSpawner.socket(context)}",
|
||||
"XDG_DATA_DIRS=/root/.local/share/flatpak/exports/share:/usr/local/share:/usr/share",
|
||||
)
|
||||
// The Steam client installer and the package tools inside the guest fetch over plain curl,
|
||||
// so the app proxy has to reach them through the environment proot starts them with.
|
||||
DownloadSource.proxyHostPort(context).takeIf { it.isNotEmpty() }?.let { hostPort ->
|
||||
cmd += listOf(
|
||||
"http_proxy=http://$hostPort", "https_proxy=http://$hostPort",
|
||||
"HTTP_PROXY=http://$hostPort", "HTTPS_PROXY=http://$hostPort",
|
||||
"no_proxy=localhost,127.0.0.1,::1", "NO_PROXY=localhost,127.0.0.1,::1",
|
||||
)
|
||||
}
|
||||
fastPathKey?.let { cmd += ProotFastPath.guestEnv(root, binds, it) }
|
||||
cmd += argv
|
||||
val builder = ProcessBuilder(cmd).directory(root).redirectErrorStream(true)
|
||||
|
||||
@@ -137,6 +137,7 @@ class FrontEndState(
|
||||
val downloadSource: String = DownloadSource.MODE_AUTO,
|
||||
val downloadMirror: String = "",
|
||||
val downloadPrefix: String = "",
|
||||
val downloadProxy: String = "",
|
||||
val animationsEnabled: Boolean = true,
|
||||
val launcherIcon: String = com.droiddeck.launcher.LauncherIcon.DARK,
|
||||
val gameStoresEnabled: Boolean = true,
|
||||
@@ -214,6 +215,7 @@ class FrontEndActions(
|
||||
val onDownloadSource: (String) -> Unit = {},
|
||||
val onDownloadMirror: (String) -> Unit = {},
|
||||
val onDownloadPrefix: (String) -> Unit = {},
|
||||
val onDownloadProxy: (String) -> Unit = {},
|
||||
val onGameStoresEnabled: (Boolean) -> Unit = {},
|
||||
val onGameStoresSpeedTier: (String) -> Unit = {},
|
||||
val onNavigated: () -> Unit = {},
|
||||
|
||||
@@ -174,6 +174,7 @@ internal fun SetupPanel(
|
||||
var showLimitDetails by rememberSaveable { mutableStateOf(false) }
|
||||
var editSgdbKey by remember { mutableStateOf(false) }
|
||||
var editDownloadPrefix by remember { mutableStateOf(false) }
|
||||
var editDownloadProxy by remember { mutableStateOf(false) }
|
||||
if (editSgdbKey) SgdbKeyDialog(s.sgdbUserKey, onSave = { a.onSgdbKey(it); editSgdbKey = false }, onDismiss = { editSgdbKey = false })
|
||||
|
||||
if (editDownloadPrefix) TextEntryDialog(
|
||||
@@ -181,6 +182,11 @@ internal fun SetupPanel(
|
||||
onDismiss = { editDownloadPrefix = false },
|
||||
onSave = { a.onDownloadPrefix(it); editDownloadPrefix = false },
|
||||
)
|
||||
if (editDownloadProxy) TextEntryDialog(
|
||||
stringResource(R.string.setup_download_proxy), s.downloadProxy,
|
||||
onDismiss = { editDownloadProxy = false },
|
||||
onSave = { a.onDownloadProxy(it); editDownloadProxy = false },
|
||||
)
|
||||
val tabs = listOf(stringResource(R.string.setup_tab_overview), stringResource(R.string.setup_tab_controller), stringResource(R.string.setup_tab_launcher), stringResource(R.string.setup_tab_storage), stringResource(R.string.setup_tab_diagnostics), stringResource(R.string.setup_tab_glasses))
|
||||
var tab by rememberSaveable { mutableStateOf(if (SetupTabs.openLauncher) LAUNCHER_TAB else 0) }
|
||||
SetupTabs.openLauncher = false
|
||||
@@ -439,6 +445,12 @@ internal fun SetupPanel(
|
||||
onClick = { editDownloadPrefix = true },
|
||||
)
|
||||
}
|
||||
ActionRow(
|
||||
stringResource(R.string.setup_download_proxy),
|
||||
s.downloadProxy.takeIf { it.isNotEmpty() },
|
||||
stringResource(R.string.mode_sgdb_edit),
|
||||
onClick = { editDownloadProxy = true },
|
||||
)
|
||||
}
|
||||
}
|
||||
if (wide) Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.Top) {
|
||||
|
||||
@@ -838,6 +838,7 @@
|
||||
<string name="setup_download_source_custom">自定义前缀</string>
|
||||
<string name="setup_download_mirror">镜像站点</string>
|
||||
<string name="setup_download_prefix">前缀</string>
|
||||
<string name="setup_download_proxy">HTTP 代理</string>
|
||||
<string name="setup_tool_files">文件</string>
|
||||
<string name="setup_tool_files_hint">浏览和管理文件</string>
|
||||
<string name="setup_tool_protons">Proton 版本</string>
|
||||
|
||||
@@ -2420,6 +2420,7 @@
|
||||
<string name="setup_download_source_custom">Custom prefix</string>
|
||||
<string name="setup_download_mirror">Site</string>
|
||||
<string name="setup_download_prefix">Prefix</string>
|
||||
<string name="setup_download_proxy">HTTP proxy</string>
|
||||
<string name="flathub_eyebrow">LINUX APPS</string>
|
||||
<string name="flathub_name">Flathub</string>
|
||||
<string name="flathub_line">Desktop apps for the Linux desktop, opened from Desktop.</string>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.droiddeck.launcher.core
|
||||
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
class DownloadSourceProxyTest {
|
||||
@Test fun hostAndPortBecomeAnHttpProxy() {
|
||||
val proxy = DownloadSource.proxyOf("127.0.0.1:9910")
|
||||
assertEquals(Proxy.Type.HTTP, proxy?.type())
|
||||
val address = proxy?.address() as InetSocketAddress
|
||||
assertEquals("127.0.0.1", address.hostString)
|
||||
assertEquals(9910, address.port)
|
||||
}
|
||||
|
||||
@Test fun missingOrInvalidPortIsIgnored() {
|
||||
assertNull(DownloadSource.proxyOf(""))
|
||||
assertNull(DownloadSource.proxyOf("127.0.0.1"))
|
||||
assertNull(DownloadSource.proxyOf("127.0.0.1:"))
|
||||
assertNull(DownloadSource.proxyOf("127.0.0.1:notaport"))
|
||||
}
|
||||
|
||||
@Test fun normalizationAcceptsSchemeAndTrailingSlash() {
|
||||
assertEquals("127.0.0.1:9910", DownloadSource.normalizeProxy(" http://127.0.0.1:9910/ "))
|
||||
assertEquals("192.168.1.2:8080", DownloadSource.normalizeProxy("192.168.1.2:8080"))
|
||||
}
|
||||
|
||||
@Test fun invalidInputDisablesTheProxy() {
|
||||
assertEquals("", DownloadSource.normalizeProxy(""))
|
||||
assertEquals("", DownloadSource.normalizeProxy("127.0.0.1"))
|
||||
assertEquals("", DownloadSource.normalizeProxy("127.0.0.1:70000"))
|
||||
assertEquals("", DownloadSource.normalizeProxy("http://127.0.0.1:notaport"))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user