From 42cafed57bdff1d7dda14f6b6250f080cdcba015 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Wed, 11 Nov 2026 00:33:16 +0800 Subject: [PATCH] Fix Android snapshot downloads behind HTTPS proxies --- .../logseq/AndroidGraphSnapshotDownloader.kt | 16 +++++++++++++++- .../AndroidGraphSnapshotDownloaderTest.kt | 19 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/kotlin/com/logseq/logseq/AndroidGraphSnapshotDownloader.kt b/android/app/src/main/kotlin/com/logseq/logseq/AndroidGraphSnapshotDownloader.kt index 6e2bda0f..0aa53bd8 100644 --- a/android/app/src/main/kotlin/com/logseq/logseq/AndroidGraphSnapshotDownloader.kt +++ b/android/app/src/main/kotlin/com/logseq/logseq/AndroidGraphSnapshotDownloader.kt @@ -15,6 +15,20 @@ data class AndroidDownloadedGraphSnapshot( class AndroidGraphSnapshotException(message: String, cause: Throwable? = null) : Exception(message, cause) +internal fun resolveSnapshotStreamUrl(metadataUrl: URL, advertisedUrl: String): URL { + val url = URL(metadataUrl, advertisedUrl) + if (metadataUrl.protocol != "https" || url.protocol != "http") return url + + // A self-hosted Node server behind a TLS proxy can advertise its HTTP origin. + // The graph's standard stream is served beside its metadata on the proxy. + val stream = URL(metadataUrl, "stream") + val path = url.path + return if (path.endsWith("/snapshot/stream") && + (path == stream.path || (path.startsWith("/sync/") && stream.path.endsWith(path)))) { + URL(metadataUrl, "stream" + url.query?.let { "?$it" }.orEmpty()) + } else url +} + class AndroidGraphSnapshotDownloader { suspend fun download( baseUrl: String, @@ -30,7 +44,7 @@ class AndroidGraphSnapshotDownloader { val metadataBody = readText(metadataUrl, accessToken, "Snapshot metadata") val resolved = shared(callCore, "resolveSnapshotDownload", JSONObject() .put("metadataBody", metadataBody)) - val snapshotUrl = URL(metadataUrl, resolved.getString("downloadURL")) + val snapshotUrl = resolveSnapshotStreamUrl(metadataUrl, resolved.getString("downloadURL")) val directory = File(workingDirectory) if (!directory.isDirectory && !directory.mkdirs()) { diff --git a/android/app/src/test/kotlin/com/logseq/logseq/AndroidGraphSnapshotDownloaderTest.kt b/android/app/src/test/kotlin/com/logseq/logseq/AndroidGraphSnapshotDownloaderTest.kt index 6961f890..3beef419 100644 --- a/android/app/src/test/kotlin/com/logseq/logseq/AndroidGraphSnapshotDownloaderTest.kt +++ b/android/app/src/test/kotlin/com/logseq/logseq/AndroidGraphSnapshotDownloaderTest.kt @@ -7,9 +7,28 @@ import org.junit.Test import org.json.JSONObject import com.sun.net.httpserver.HttpServer import java.net.InetSocketAddress +import java.net.URL import java.nio.file.Files class AndroidGraphSnapshotDownloaderTest { + @Test + fun `downloads the standard stream through the configured HTTPS proxy`() { + val metadata = URL("https://sync.example.test:8443/logseq/sync/graph/snapshot/download") + val url = resolveSnapshotStreamUrl(metadata, "http://backend:8787/sync/graph/snapshot/stream?cursor=42") + assertEquals("https://sync.example.test:8443/logseq/sync/graph/snapshot/stream?cursor=42", url.toString()) + } + + @Test + fun `respects other download links and direct HTTP servers`() { + val metadata = URL("https://sync.example.test/sync/graph/snapshot/download") + val otherGraph = "http://backend:8787/sync/other/snapshot/stream" + val signedDownload = "https://storage.example.test/graph.snapshot?signature=abc" + val directStream = "http://localhost:8787/sync/graph/snapshot/stream" + assertEquals(otherGraph, resolveSnapshotStreamUrl(metadata, otherGraph).toString()) + assertEquals(signedDownload, resolveSnapshotStreamUrl(metadata, signedDownload).toString()) + assertEquals(directStream, resolveSnapshotStreamUrl(URL("http://localhost:8787/sync/graph/snapshot/download"), directStream).toString()) + } + @Test fun `downloads the shared snapshot plan and preserves binary bytes`() = runBlocking { val server = HttpServer.create(InetSocketAddress("127.0.0.1", 0), 0)