From 15152a13da16ad5f3b89a7f75074314e277c3d01 Mon Sep 17 00:00:00 2001 From: Moritz Zwerger Date: Thu, 21 Dec 2023 18:54:14 +0100 Subject: [PATCH] ask if update checking is allowed --- doc/Updater.md | 10 ++--- .../minosoft/updater/MinosoftUpdateTest.kt | 27 +++++++++---- .../minosoft/updater/MinosoftUpdaterTest.kt | 38 +++++++++++++++++++ src/main/java/de/bixilon/minosoft/Minosoft.kt | 14 +++++++ .../profiles/other/updater/UpdaterC.kt | 2 +- .../java/de/bixilon/minosoft/gui/eros/Eros.kt | 21 ++++++++++ .../bixilon/minosoft/updater/DownloadLink.kt | 1 - .../minosoft/updater/MinosoftUpdate.kt | 8 ---- .../minosoft/updater/MinosoftUpdater.kt | 9 ++++- .../assets/minosoft/language/en_us.lang | 7 ++++ 10 files changed, 110 insertions(+), 27 deletions(-) create mode 100644 src/integration-test/kotlin/de/bixilon/minosoft/updater/MinosoftUpdaterTest.kt diff --git a/doc/Updater.md b/doc/Updater.md index 9e6a1c0a8..05678d2c1 100644 --- a/doc/Updater.md +++ b/doc/Updater.md @@ -15,7 +15,7 @@ Minosoft fetches a configured url with the following parameters: ### Response (v1) The server internally checks if the user can receive updates for the version or platform. If no update is available, it responds with `204 No content`. -Otherwise it responds with `200 OK` and returns a json object: +Otherwise it responds with `200 OK` and returns a json object (with the signature, check below): ```json { @@ -37,6 +37,7 @@ Otherwise it responds with `200 OK` and returns a json object: All urls in the response **must** start with `https://` or be a localhost link. + ## Update process When an update is available, it prompts the user the install it. It tries to store the file in the current directory (if possible) or asks the user where to store it. @@ -46,12 +47,7 @@ The client will refuse to update, if the release date of the next version is low ## Signature -The signature is created by base64 appending the following strings (in order) together and then using RSA (`SHA512withRSA`) to sign it: - -1. Version id -2. release date (number to string) -3. Release page (optional) -4. sha512 hash of the binary +The first line of the file is the base64 encoded signature of the whole json object. ## Future diff --git a/src/integration-test/kotlin/de/bixilon/minosoft/updater/MinosoftUpdateTest.kt b/src/integration-test/kotlin/de/bixilon/minosoft/updater/MinosoftUpdateTest.kt index 224a2186d..5370ee206 100644 --- a/src/integration-test/kotlin/de/bixilon/minosoft/updater/MinosoftUpdateTest.kt +++ b/src/integration-test/kotlin/de/bixilon/minosoft/updater/MinosoftUpdateTest.kt @@ -13,21 +13,32 @@ package de.bixilon.minosoft.updater +import de.bixilon.kutil.hash.HashUtil.sha512 +import de.bixilon.kutil.url.URLUtil.toURL +import de.bixilon.minosoft.data.text.ChatComponent +import de.bixilon.minosoft.properties.MinosoftP +import de.bixilon.minosoft.properties.MinosoftProperties +import de.bixilon.minosoft.properties.general.GeneralP +import org.testng.Assert.assertThrows import org.testng.annotations.Test -@Test +@Test(groups = ["updater"]) class MinosoftUpdateTest { - fun `invalid signature`() { - TODO() + init { + MinosoftProperties = MinosoftP(GeneralP("old", -10L, false), null) + } + + fun `no download link`() { + MinosoftUpdate("dummy", "Dummy version", MinosoftProperties.general.date + 1, true, null, null, ChatComponent.of(":)")) + } + + fun `correct data`() { + MinosoftUpdate("dummy", "Dummy version", MinosoftProperties.general.date + 1, true, null, DownloadLink("https://bixilon.de/secret-update.jar".toURL(), 123, ByteArray(1).sha512()), ChatComponent.of(":)")) } fun `older signature`() { - TODO() - } - - fun `correct signature`() { - + assertThrows { MinosoftUpdate("dummy", "Dummy version", -10L, true, null, DownloadLink("https://bixilon.de/secret-update.jar".toURL(), 123, ByteArray(1).sha512()), ChatComponent.of(":)")) } } } diff --git a/src/integration-test/kotlin/de/bixilon/minosoft/updater/MinosoftUpdaterTest.kt b/src/integration-test/kotlin/de/bixilon/minosoft/updater/MinosoftUpdaterTest.kt new file mode 100644 index 000000000..bdbab723d --- /dev/null +++ b/src/integration-test/kotlin/de/bixilon/minosoft/updater/MinosoftUpdaterTest.kt @@ -0,0 +1,38 @@ +/* + * Minosoft + * Copyright (C) 2020-2023 Moritz Zwerger + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If not, see . + * + * This software is not affiliated with Mojang AB, the original developer of Minecraft. + */ + +package de.bixilon.minosoft.updater + +import org.testng.Assert.assertThrows +import org.testng.annotations.Test + + +@Test(groups = ["updater"]) +class MinosoftUpdaterTest { + + private fun verify(signature: String, data: String) { + MinosoftUpdater.parse(signature + "\n" + data) + } + + fun `verify broken signature`() { + assertThrows { verify("NOT_VALID", """{"id":"dummy","name":"Dummy version","date":-1,"stable":true,"page":null,"download":{"url":"https://bixilon.de/secret-update.jar","size":123,"sha512":"b8244d028981d693af7b456af8efa4cad63d282e19ff14942c246e50d9351d22704a802a71c3580b6370de4ceb293c324a8423342557d4e5c38438f0e36910ee"},"release_notes":":)"}""") } + } + + fun `verify correct signature`() { + assertThrows { + verify( + "Mv7979ky1AlMCcOLmX+Zdmo2Y7YOGiMthTBeNP2jKUPRkMtX1GCBYMrsKV+si9rR9Kg+1Ns82Hw1iYAI+ZOkTzVmoIeaWkqL4PN4sCCVllm2ZmZhTap7wdNAVEjW197Cf+V2YJW0TsG+j2s6OK86gdtVmyJ96X/ENhXTJRp8pW50lcCyy95ipQ1Qe8v4mAFykpU9XC/yU2Mhil/KznxvxKgd7N4+/VNpubOHetWfdiz9jqAB6uaYVi0H9E+EoZodkG3Iy5uagr1OrWNiwjk3LQUEk+J+5cYRPqBHrqLM9VNQFa5BqysSJoW7cIo/QUQA47EBxO8Rmg/juFA1l9bXtSUA+1j12n9ImhE/L3cYseYiIN8GFRpbhSaROgfZW9u3lVHM4g45q67zvvdf+Eo7lqfipYio89rQ984U58o5AvLhV+WqhDVRBTTtO+oI/FjdiHIruoiY/adEz7gJEAlrMlgoAAQkVnKma9uufObIemL+QGpDjLvdluIgts/cT34r4I5Xaij1vGAjzZ+Fe+Tn5tuW48pjtjWCzAwVTEu/zf/VKSJPoCVGx5YCvFE3CKXkVWuJ86gj+rO/SXWkjv672EetaVwv2Uc/RkCfru84m6bQWAHzb3P46Hfkw3kIyaIudxgizy1xlxLEEU3LwUU/vFxTd2Q6lAhGGMn6Imy9Z6I=", + """{"id":"dummy","name":"Dummy version","date":-1,"stable":true,"page":null,"download":{"url":"https://bixilon.de/secret-update.jar","size":123,"sha512":"b8244d028981d693af7b456af8efa4cad63d282e19ff14942c246e50d9351d22704a802a71c3580b6370de4ceb293c324a8423342557d4e5c38438f0e36910ee"},"release_notes":":)"}""") + } + } +} diff --git a/src/main/java/de/bixilon/minosoft/Minosoft.kt b/src/main/java/de/bixilon/minosoft/Minosoft.kt index 6c8cb9009..4a2a83b83 100644 --- a/src/main/java/de/bixilon/minosoft/Minosoft.kt +++ b/src/main/java/de/bixilon/minosoft/Minosoft.kt @@ -168,9 +168,23 @@ object Minosoft { ShutdownManager.shutdown(reason = AbstractShutdownReason.CRASH) } + private fun enableUpdates() { + val profile = OtherProfileManager.selected.updater + if (RunConfiguration.DISABLE_EROS) { + if (!profile.ask) return + Log.log(LogMessageType.OTHER, LogLevels.INFO) { "Automated update checking was §aenabled§r. To disable it, check the config file." } + profile.ask = false + profile.check = true + return + } + // gui enabled, eros will show the prompt there + } + fun checkForUpdates() { if (!OtherProfileManager.selected.updater.check) return DefaultThreadPool += ForcePooledRunnable(priority = ThreadPool.LOW) { + enableUpdates() + if (!OtherProfileManager.selected.updater.check) return@ForcePooledRunnable val update = MinosoftUpdater.check() ?: return@ForcePooledRunnable Log.log(LogMessageType.OTHER, LogLevels.INFO) { "A new update is available: ${update.name} (${update.id}). Type \"update\" or click in the gui to update." } } diff --git a/src/main/java/de/bixilon/minosoft/config/profile/profiles/other/updater/UpdaterC.kt b/src/main/java/de/bixilon/minosoft/config/profile/profiles/other/updater/UpdaterC.kt index 4306f1840..fa60b9273 100644 --- a/src/main/java/de/bixilon/minosoft/config/profile/profiles/other/updater/UpdaterC.kt +++ b/src/main/java/de/bixilon/minosoft/config/profile/profiles/other/updater/UpdaterC.kt @@ -29,7 +29,7 @@ class UpdaterC(profile: OtherProfile) { /** * Check for updates */ - var check by BooleanDelegate(profile, true) + var check by BooleanDelegate(profile, false) /** * Update channel diff --git a/src/main/java/de/bixilon/minosoft/gui/eros/Eros.kt b/src/main/java/de/bixilon/minosoft/gui/eros/Eros.kt index 65beb3ecd..e50b19400 100644 --- a/src/main/java/de/bixilon/minosoft/gui/eros/Eros.kt +++ b/src/main/java/de/bixilon/minosoft/gui/eros/Eros.kt @@ -14,11 +14,14 @@ package de.bixilon.minosoft.gui.eros import de.bixilon.kutil.collections.CollectionUtil.toSynchronizedSet +import de.bixilon.kutil.concurrent.pool.DefaultThreadPool import de.bixilon.kutil.exception.ExceptionUtil.catchAll import de.bixilon.kutil.latch.SimpleLatch import de.bixilon.kutil.observer.DataObserver.Companion.observe import de.bixilon.minosoft.config.profile.profiles.eros.ErosProfileManager import de.bixilon.minosoft.config.profile.profiles.other.OtherProfileManager +import de.bixilon.minosoft.data.registries.identified.Namespaces.i18n +import de.bixilon.minosoft.gui.eros.dialog.SimpleErosConfirmationDialog import de.bixilon.minosoft.gui.eros.dialog.UpdateAvailableDialog import de.bixilon.minosoft.gui.eros.main.MainErosController import de.bixilon.minosoft.gui.eros.modding.invoker.JavaFXEventListener.Companion.javaFX @@ -32,6 +35,7 @@ import de.bixilon.minosoft.util.KUtil.toResourceLocation import de.bixilon.minosoft.util.logging.Log import de.bixilon.minosoft.util.logging.LogLevels import de.bixilon.minosoft.util.logging.LogMessageType +import javafx.stage.Modality import javafx.stage.Window object Eros { @@ -93,12 +97,29 @@ object Eros { } } + private fun askForUpdates() { + val profile = OtherProfileManager.selected.updater + if (!profile.ask) return + val dialog = SimpleErosConfirmationDialog( + title = i18n("updater.ask.title"), + header = i18n("updater.ask.header"), + description = i18n("updater.ask.description"), + cancelButtonText = i18n("updater.ask.no"), + confirmButtonText = i18n("updater.ask.yes"), + onCancel = { profile.ask = false; profile.check = false }, + onConfirm = { profile.ask = false; profile.check = true; DefaultThreadPool += { MinosoftUpdater.check() } }, + modality = Modality.APPLICATION_MODAL, + ) + dialog.show() + } + fun start() { if (latch.count >= 1) return latch.await() mainErosController.stage.show() initialized = true visible = true + askForUpdates() Log.log(LogMessageType.JAVAFX, LogLevels.VERBOSE) { "Eros up!" } } diff --git a/src/main/java/de/bixilon/minosoft/updater/DownloadLink.kt b/src/main/java/de/bixilon/minosoft/updater/DownloadLink.kt index 963633d77..6d0da47d5 100644 --- a/src/main/java/de/bixilon/minosoft/updater/DownloadLink.kt +++ b/src/main/java/de/bixilon/minosoft/updater/DownloadLink.kt @@ -19,5 +19,4 @@ data class DownloadLink( val url: URL, val size: Int, val sha512: String, - val signature: String, ) diff --git a/src/main/java/de/bixilon/minosoft/updater/MinosoftUpdate.kt b/src/main/java/de/bixilon/minosoft/updater/MinosoftUpdate.kt index 862e26515..191e3abab 100644 --- a/src/main/java/de/bixilon/minosoft/updater/MinosoftUpdate.kt +++ b/src/main/java/de/bixilon/minosoft/updater/MinosoftUpdate.kt @@ -33,13 +33,5 @@ data class MinosoftUpdate( private fun verify() { if (MinosoftProperties.general.date >= date) throw Exception("Update is older than the current version!") - if (this.download == null) return - val builder = StringBuilder() - builder.append(id) - builder.append(date) - page?.let { builder.append(it) } - builder.append(download.sha512) - - UpdateKey.require(builder.toString(), download.signature) } } diff --git a/src/main/java/de/bixilon/minosoft/updater/MinosoftUpdater.kt b/src/main/java/de/bixilon/minosoft/updater/MinosoftUpdater.kt index 14c8cdd58..ed668116a 100644 --- a/src/main/java/de/bixilon/minosoft/updater/MinosoftUpdater.kt +++ b/src/main/java/de/bixilon/minosoft/updater/MinosoftUpdater.kt @@ -49,7 +49,6 @@ object MinosoftUpdater { fun check(url: String, channel: String): MinosoftUpdate? { - val commit = MinosoftProperties.git?.commit ?: "" val version = MinosoftProperties.general.name val stable = MinosoftProperties.general.stable @@ -82,11 +81,17 @@ object MinosoftUpdater { return when (response.statusCode) { 204 -> null - 200 -> Jackson.MAPPER.readValue(response.body, MinosoftUpdate::class.java) + 200 -> parse(response.body) else -> throw HTTPException(response.statusCode, response.body) } } + fun parse(data: String): MinosoftUpdate { + val (signature, json) = data.split('\n', limit = 2) + UpdateKey.require(signature, json) + return Jackson.MAPPER.readValue(json, MinosoftUpdate::class.java) + } + fun download(update: MinosoftUpdate, progress: UpdateProgress) { val download = update.download if (download == null) { diff --git a/src/main/resources/assets/minosoft/language/en_us.lang b/src/main/resources/assets/minosoft/language/en_us.lang index 18647e707..1671dcc0e 100644 --- a/src/main/resources/assets/minosoft/language/en_us.lang +++ b/src/main/resources/assets/minosoft/language/en_us.lang @@ -235,3 +235,10 @@ minosoft:updater.available.dismiss=Dismiss minosoft:updater.available.later=Update later minosoft:updater.available.open=Open in browser minosoft:updater.available.update=Update now + + +minosoft:updater.ask.title=Update checking +minosoft:updater.ask.header=Allow automated checking for updates? +minosoft:updater.ask.description=Minosoft can automatically check for updates on startup.\nIt is highly recommended to just run the latest version, there is no benefit from running outdated builds. Newer builds will include performance/feature improvements, potentially security updates and bug fixes.\nThe updater will submit data the following data to a server provided by Moritz Zwerger (Bixilon; original author of the program; visit the privacy https://imprint.bixilon.de): current running version, operating system, processor architecture. All requests are completely anonymized, but might be logged.\nMinosoft will prompt you to automatically update, once an update is available. You can always search for updates manually (Main window -> About -> Check for updates). This option can be changed at any time in the configuration files. +minosoft:updater.ask.no=Disallow update checking +minosoft:updater.ask.yes=Allow and check now (recommended)