mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-12 19:41:10 +00:00
fix: third-party GitHub mirrors only serve downloads with a trusted digest (#1057)
fix: a third-party GitHub mirror can only serve downloads that carry a trusted digest Downloads without an expected SHA-256 were still sent through gh-proxy, ghfast and other mirrors, and the first answer was accepted, so a mirror could swap an AppImage or a Windows component. Downloader.downloadFile now tries only the original URL when there is no digest. UserApps passes GitHub's own asset digest, and WinComponents passes the snapshot's archive digest, so those large files keep their mirror fallback. Co-authored-by: ovurrsl <ovurrsl@users.noreply.github.com> Co-authored-by: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com>
This commit is contained in:
1 parent
d9328c39df
commit
87e4b42ed8
4 files changed
+23
-6
No files matched your search
@@ -54,7 +54,12 @@ public final class Downloader {
|
||||
java.util.function.Consumer<Float> progress) {
|
||||
if (expectedSha256 != null && !expectedSha256.matches("(?i)[0-9a-f]{64}"))
|
||||
throw new IllegalArgumentException("Expected SHA-256 must be 64 hex characters");
|
||||
return downloadCandidates(url, DownloadSource.INSTANCE.candidates(url), destination, resume, expectedSha256, progress);
|
||||
return downloadCandidates(url, sources(url, expectedSha256), destination, resume, expectedSha256, progress);
|
||||
}
|
||||
|
||||
/** A third-party mirror may only serve bytes that can be checked against a digest from a direct source. */
|
||||
static List<String> sources(String url, String expectedSha256) {
|
||||
return expectedSha256 == null ? java.util.Collections.singletonList(url) : DownloadSource.INSTANCE.candidates(url);
|
||||
}
|
||||
|
||||
// Package-private for the local multi-source HTTP test.
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.caverock.androidsvg.SVG
|
||||
import com.droiddeck.launcher.R
|
||||
import com.droiddeck.launcher.core.Downloader
|
||||
import com.droiddeck.launcher.core.FileUtils
|
||||
import com.droiddeck.launcher.core.Hashes
|
||||
import com.droiddeck.launcher.store.FlathubApi
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
@@ -44,7 +45,7 @@ object UserApps {
|
||||
val fex: String = LinuxFex.AUTO, val arch: String? = null,
|
||||
)
|
||||
|
||||
class Release(val tag: String, val published: String, val asset: String, val url: String)
|
||||
class Release(val tag: String, val published: String, val asset: String, val url: String, val sha256: String? = null)
|
||||
|
||||
sealed class UpdateCheck {
|
||||
class Current(val tag: String) : UpdateCheck()
|
||||
@@ -318,9 +319,12 @@ object UserApps {
|
||||
val r = releases.getJSONObject(i)
|
||||
if (r.optBoolean("draft")) return@mapNotNull null
|
||||
val assets = r.optJSONArray("assets") ?: return@mapNotNull null
|
||||
val byName = (0 until assets.length()).associate { j -> assets.getJSONObject(j).let { it.getString("name") to it.getString("browser_download_url") } }
|
||||
val byName = (0 until assets.length()).associate { j -> assets.getJSONObject(j).let { it.getString("name") to it } }
|
||||
pickAsset(byName.keys.toList())?.let { asset ->
|
||||
r.optBoolean("prerelease") to Release(r.optString("tag_name", asset), r.optString("published_at"), asset, byName.getValue(asset))
|
||||
r.optBoolean("prerelease") to byName.getValue(asset).let { a ->
|
||||
Release(r.optString("tag_name", asset), r.optString("published_at"), asset, a.getString("browser_download_url"),
|
||||
Hashes.githubSha256(a.optString("digest")))
|
||||
}
|
||||
}
|
||||
}
|
||||
val pick = (candidates.firstOrNull { !it.first } ?: candidates.firstOrNull())?.second
|
||||
@@ -331,7 +335,7 @@ object UserApps {
|
||||
val download = File(context.cacheDir, "github-${release.asset}")
|
||||
try {
|
||||
val label = context.getString(R.string.user_apps_downloading, release.asset)
|
||||
val ok = Downloader.downloadFile(release.url, download, false) { f -> onProgress(label, if (f < 0) -1 else Math.round(f * 100f)) }
|
||||
val ok = Downloader.downloadFile(release.url, download, false, release.sha256) { f -> onProgress(label, if (f < 0) -1 else Math.round(f * 100f)) }
|
||||
return if (ok) use(download) else context.getString(R.string.user_apps_download_failed)
|
||||
} finally {
|
||||
download.delete()
|
||||
|
||||
@@ -369,7 +369,9 @@ object WinComponents {
|
||||
val archiveName = archive.optString("name")
|
||||
val url = c.snapshot.substringBeforeLast('/') + "/" + archiveName
|
||||
val file = File(work, "${c.name}.files.tar.xz")
|
||||
if (!Downloader.downloadFile(url, file, false) { f -> onProgress(Progress(c.name, archiveName, Phase.DOWNLOAD, if (f < 0) -1 else Math.round(f * 100f))) }) {
|
||||
// The snapshot came straight from github.com, so its digest may vouch for a mirror's copy of the archive.
|
||||
val sha = archive.optString("sha256").takeIf { it.matches(Regex("(?i)[0-9a-f]{64}")) }
|
||||
if (!Downloader.downloadFile(url, file, false, sha) { f -> onProgress(Progress(c.name, archiveName, Phase.DOWNLOAD, if (f < 0) -1 else Math.round(f * 100f))) }) {
|
||||
return null to "download failed: $archiveName"
|
||||
}
|
||||
archive.optString("sha256").takeIf { it.length == 64 }?.let { sha ->
|
||||
|
||||
@@ -167,4 +167,10 @@ class DownloaderResumeTest {
|
||||
assertEquals(listOf(catalog), DownloadSource.candidates(catalog))
|
||||
}
|
||||
|
||||
|
||||
@Test fun withoutADigestOnlyTheOriginalUrlIsTried() {
|
||||
val url = "https://github.com/owner/repo/releases/download/v1/app.AppImage"
|
||||
assertEquals(listOf(url), Downloader.sources(url, null))
|
||||
assertEquals(DownloadSource.candidates(url), Downloader.sources(url, "a".repeat(64)))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user