From 7156458c6f9e531721842f449211c9af4107ae82 Mon Sep 17 00:00:00 2001 From: oxpoe Date: Tue, 13 Oct 2026 08:10:05 +0700 Subject: [PATCH] feat(updater): verifikasi SHA-256 sebelum mengganti AppImage; CI menerbitkan .sha256 + .zsync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - do_update: unduh → verifikasi checksum rilis → baru ganti berkas (mismatch = dibatalkan) - workflow: hasilkan GAMEMAX-LCD-x86_64.AppImage.sha256 dan .zsync (zsyncmake), lampirkan ketiganya - README: bagian Updates menjelaskan checksum + zsync (delta update opsional) - versi 1.2.3 --- .github/workflows/release.yml | 20 ++++++++++++++++++-- README.md | 7 +++++++ src/gamemax_gui.py | 26 ++++++++++++++++++++++++-- 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b3a77d5..b615438 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: run: | sudo apt-get update sudo apt-get install -y --no-install-recommends \ - libgl1 libegl1 libxkbcommon0 libdbus-1-3 libfuse2 fuse + libgl1 libegl1 libxkbcommon0 libdbus-1-3 libfuse2 fuse zsync - name: Set up uv uses: astral-sh/setup-uv@v5 @@ -38,6 +38,15 @@ jobs: run: | ./dist/GAMEMAX-LCD-x86_64.AppImage --selftest + - name: Checksum and zsync metadata + run: | + cd dist + sha256sum GAMEMAX-LCD-x86_64.AppImage > GAMEMAX-LCD-x86_64.AppImage.sha256 + zsyncmake -u "https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/GAMEMAX-LCD-x86_64.AppImage" \ + -o GAMEMAX-LCD-x86_64.AppImage.zsync GAMEMAX-LCD-x86_64.AppImage + cat GAMEMAX-LCD-x86_64.AppImage.sha256 + ls -l + - name: Publish release uses: softprops/action-gh-release@v2 with: @@ -58,5 +67,12 @@ jobs: Device access still needs the udev rule once: `sudo cp packaging/70-gamemax.rules /etc/udev/rules.d/ && sudo udevadm control --reload` + **Integrity:** verify the download with the published SHA-256, or use + `appimageupdatetool`/AppImageUpdate with the `.zsync` asset for delta updates. + The built-in updater (v1.2.3+) also verifies that checksum before replacing itself. + Independent project, not affiliated with the vendor — see DISCLAIMER.md. - files: dist/GAMEMAX-LCD-x86_64.AppImage + files: | + dist/GAMEMAX-LCD-x86_64.AppImage + dist/GAMEMAX-LCD-x86_64.AppImage.sha256 + dist/GAMEMAX-LCD-x86_64.AppImage.zsync diff --git a/README.md b/README.md index 4b1db7b..3cdf7e9 100644 --- a/README.md +++ b/README.md @@ -218,8 +218,15 @@ The app keeps itself current from GitHub Releases: - When a newer release exists, a **Perbarui ke vX.Y.Z** button appears. One click downloads the release asset, replaces the running AppImage atomically, refreshes the sender from the new bundle and restarts the service. Restart the app to use the new version. +- The download is verified against the release's published **SHA-256** before the AppImage is + replaced; a mismatch aborts the update. Older releases without a checksum are updated with a + warning in the log. - Nothing is downloaded without that click, and no other files are touched. +Each release publishes three assets: the AppImage, its `.sha256`, and a `.zsync` file for +`appimageupdatetool` / AppImageUpdate (delta updates). The zsync file is optional — the +built-in updater does not need it. + If the check fails (offline, proxy), the message appears in the app log. `GAMEMAX_DEBUG=1` also mirrors the log to stdout for headless runs. diff --git a/src/gamemax_gui.py b/src/gamemax_gui.py index 4f8ea25..ca3871f 100755 --- a/src/gamemax_gui.py +++ b/src/gamemax_gui.py @@ -8,6 +8,7 @@ mengendalikannya dan memakai gamemax_report.py sebagai pengirim. gamemax_gui.py --tray-only # tanpa jendela gamemax_gui.py --selftest # uji muat tanpa tampilan (untuk build AppImage) """ +import hashlib import json import math import os @@ -30,7 +31,7 @@ from PySide6.QtWidgets import ( ) from PySide6.QtNetwork import QLocalServer, QLocalSocket -APP_VERSION = "1.2.2" +APP_VERSION = "1.2.3" REPO = "oxpoe/Gamemax-linux-apps" RELEASES_API = f"https://api.github.com/repos/{REPO}/releases/latest" UPDATE_ASSET = "GAMEMAX-LCD-x86_64.AppImage" @@ -96,6 +97,19 @@ def ensure_ca_bundle(): return +def verify_sha256(path, sha_url): + """True/False bila berkas checksum tersedia, None bila rilis tidak menyediakannya.""" + try: + with urllib.request.urlopen(sha_url, timeout=10) as resp: + expected = resp.read().decode().split()[0].strip().lower() + except Exception: # noqa: BLE001 + return None + if len(expected) != 64: + return None + actual = hashlib.sha256(open(path, "rb").read()).hexdigest() + return actual == expected + + def latest_release(): """(tag, url aset) dari rilis terbaru GitHub — dipanggil dari thread pekerja.""" ensure_ca_bundle() @@ -503,6 +517,10 @@ class Window(QWidget): ensure_ca_bundle() tmp = target + ".new" urllib.request.urlretrieve(url, tmp) + checked = verify_sha256(tmp, url + ".sha256") + if checked is False: + os.unlink(tmp) + raise RuntimeError("checksum SHA-256 tidak cocok — unduhan dibatalkan") os.chmod(tmp, 0o755) os.replace(tmp, target) # ganti berkas secara atomik # sender di layanan juga diambil dari bundel baru @@ -514,7 +532,11 @@ class Window(QWidget): os.makedirs(INSTALL_DIR, exist_ok=True) shutil.copyfile(sender, os.path.join(INSTALL_DIR, "gamemax_report.py")) systemctl("restart", SERVICE) - return f"diperbarui ke {tag} — tutup jendela ini lalu buka lagi untuk memakai versi baru" + note = "" if checked is None else " (checksum SHA-256 terverifikasi)" + if checked is None: + note = " (rilis ini tidak menyertakan checksum)" + return (f"diperbarui ke {tag}{note} — tutup jendela ini lalu buka lagi " + "untuk memakai versi baru") # -------------------------------------------------------------- tugas latar def start_task(self, label, fn):