ask if update checking is allowed

This commit is contained in:
Moritz Zwerger 2023-12-21 18:54:14 +01:00
parent 8e2bb9fc98
commit 15152a13da
No known key found for this signature in database
GPG Key ID: 5CAD791931B09AC4
10 changed files with 110 additions and 27 deletions

View File

@ -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

View File

@ -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(":)")) }
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* 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":":)"}""")
}
}
}

View File

@ -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." }
}

View File

@ -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

View File

@ -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!" }
}

View File

@ -19,5 +19,4 @@ data class DownloadLink(
val url: URL,
val size: Int,
val sha512: String,
val signature: String,
)

View File

@ -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)
}
}

View File

@ -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) {

View File

@ -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)