diff --git a/app/src/main/java/org/prolibertate/games/update/UpdatePolicy.kt b/app/src/main/java/org/prolibertate/games/update/UpdatePolicy.kt index 6eb110e..a08e791 100644 --- a/app/src/main/java/org/prolibertate/games/update/UpdatePolicy.kt +++ b/app/src/main/java/org/prolibertate/games/update/UpdatePolicy.kt @@ -1,5 +1,7 @@ package org.prolibertate.games.update +import java.net.URI + /** * Which line of builds an APK belongs to. * @@ -65,6 +67,28 @@ fun channelOfTag(tag: String, prerelease: Boolean): UpdateChannel = fun versionCodeOfTag(tag: String): Int? = tag.substringAfterLast('.').removeSuffix(DEV_SUFFIX).toIntOrNull() +/** + * Rewrites a download URL onto [host], keeping its scheme, path and query. + * + * A release server can answer for more than one host name, and it hands out + * asset URLs on whichever one it treats as canonical — which need not be the + * one the app was told to talk to. Fetching the release list from one host and + * the APK from another is a single DNS change away from a channel that half + * works, so the app stays on the host it decided to trust and takes only the + * path from what the server reported. + * + * Anything that cannot be parsed is returned unchanged: a broken URL fails + * loudly at download time, while a swallowed one would fail as a silent + * "something went wrong". + */ +fun onReportedHost(url: String, host: String): String = try { + val reported = URI(url) + val scheme = reported.scheme ?: "https" + URI(scheme, null, host, -1, reported.path, reported.query, null).toString() +} catch (e: Exception) { + url +} + /** * The build a check on launch should interrupt somebody for, or null to say * nothing at all. diff --git a/app/src/main/java/org/prolibertate/games/update/Updater.kt b/app/src/main/java/org/prolibertate/games/update/Updater.kt index b6e2459..69977b8 100644 --- a/app/src/main/java/org/prolibertate/games/update/Updater.kt +++ b/app/src/main/java/org/prolibertate/games/update/Updater.kt @@ -105,8 +105,8 @@ class Updater(private val activity: Activity) { .map { assets.getJSONObject(it) } .filter { it.optString("name").endsWith(".apk") } - // A production release carries the same build twice: once named for its - // version, and once under a fixed name so a permanent download link + // A production release may carry the same build twice: once named for + // its version, and once under a fixed name so a permanent download link // exists. Pick the versioned one explicitly rather than whichever was // uploaded first, so what gets installed is traceable to its tag. val chosen = apks.firstOrNull { it.optString("name").contains(tag) } @@ -116,7 +116,7 @@ class Updater(private val activity: Activity) { return Release( tag = tag, versionCode = versionCode, - apkUrl = chosen.getString("browser_download_url"), + apkUrl = onReportedHost(chosen.getString("browser_download_url"), SERVER_HOST), apkName = chosen.optString("name"), channel = channel, ) @@ -204,6 +204,7 @@ class Updater(private val activity: Activity) { private companion object { /** The Gitea instance that publishes this app's releases. */ const val SERVER = "https://git.wallacenet.ca" + const val SERVER_HOST = "git.wallacenet.ca" const val OWNER = "devadmin" const val REPO = "PilgrimsOfKatan" diff --git a/app/src/test/java/org/prolibertate/games/update/UpdatePolicyTest.kt b/app/src/test/java/org/prolibertate/games/update/UpdatePolicyTest.kt index 301be67..213b7f4 100644 --- a/app/src/test/java/org/prolibertate/games/update/UpdatePolicyTest.kt +++ b/app/src/test/java/org/prolibertate/games/update/UpdatePolicyTest.kt @@ -53,6 +53,28 @@ class UpdatePolicyTest { assertNull(versionCodeOfTag("nightly")) } + // -- Where the download happens ---------------------------------------- + + @Test + fun `a release asset is fetched from this app's host, keeping its path`() { + // The instance reports its canonical host; the app was told to trust + // another. Same bytes, one host, whatever the server names itself. + assertEquals( + "https://git.wallacenet.ca/devadmin/PilgrimsOfKatan/releases/download/" + + "v1.4.1001-dev/PilgrimsOfKatan-v1.4.1001-dev.apk", + onReportedHost( + "https://git.saskvalleytech.com/devadmin/PilgrimsOfKatan/releases/download/" + + "v1.4.1001-dev/PilgrimsOfKatan-v1.4.1001-dev.apk", + "git.wallacenet.ca", + ), + ) + // A URL that already says the right host is left exactly as it is. + val already = "https://git.wallacenet.ca/devadmin/PilgrimsOfKatan/releases/download/v1.4.1/a.apk" + assertEquals(already, onReportedHost(already, "git.wallacenet.ca")) + // Anything unparseable survives untouched rather than being dropped. + assertEquals("not a url", onReportedHost("not a url", "git.wallacenet.ca")) + } + // -- The wall ----------------------------------------------------------- @Test diff --git a/scripts/publish-release.sh b/scripts/publish-release.sh index 4facd05..a41f74b 100755 --- a/scripts/publish-release.sh +++ b/scripts/publish-release.sh @@ -35,11 +35,16 @@ esac [ -n "${GITEA_TOKEN:-}" ] || { echo "publish-release: GITEA_TOKEN is not set" >&2; exit 2; } api="${GITEA_API:-${GITHUB_API_URL:-https://git.wallacenet.ca/api/v1}}/repos/${GITEA_REPO:-${GITHUB_REPOSITORY:-devadmin/PilgrimsOfKatan}}" -apk_name="$(basename "$apk")" tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT INT TERM +# Named for its tag, not for whatever the build happened to call it: the +# updater picks the versioned APK out of a release's assets by finding the one +# whose name contains the tag, so an asset called app-debug.apk would only be +# found by the fallback rule. +apk_name="PilgrimsOfKatan-${tag}.apk" + # The tag and the file name are both derived from gradle.properties and a run # number, so neither can carry a quote or a backslash into this JSON. if [ "$kind" = "prerelease" ]; then @@ -83,7 +88,7 @@ id="$(grep -o '"id":[0-9]*' "$tmp/create.json" | head -n1 | tr -dc '0-9' || true code="$(curl -sS -o "$tmp/asset.json" -w '%{http_code}' -X POST \ -H "Authorization: token ${GITEA_TOKEN}" \ - -F "attachment=@${apk}" \ + -F "attachment=@${apk};filename=${apk_name}" \ "$api/releases/${id}/assets")" if [ "$code" != "201" ] && [ "$code" != "200" ]; then