feat(ota): verify offline update checksums

This commit is contained in:
watermeko
2026-08-03 02:56:16 +00:00
committed by Guoguo
parent 7cda3b2920
commit 0f8ef20fb5
32 changed files with 286 additions and 51 deletions

View File

@@ -39,10 +39,16 @@ func (s *Service) Update(c *gin.Context) {
rsp.OkRsp(c)
log.Debugf("update application success")
// Sleep for a second before restarting the device
go restartServices()
}
func restartServices() {
// Let the HTTP response reach the client before stopping the server.
time.Sleep(1 * time.Second)
_ = exec.Command("sh", "-c", "/kvmapp/system/init.d/S95nanokvm restart").Run()
if err := exec.Command("/kvmapp/system/init.d/S95nanokvm", "restart").Run(); err != nil {
log.Errorf("failed to restart services after update: %v", err)
}
}
func update() error {
@@ -88,7 +94,7 @@ func download(url string, target string) (err error) {
}
var req *http.Request
req, err = http.NewRequest("GET", url, nil)
req, err = utils.NewAuthenticatedRequest("GET", url, nil)
if err != nil {
log.Errorf("new request err: %s", err)
continue

View File

@@ -1,15 +1,16 @@
package application
import (
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"fmt"
"io"
"mime/multipart"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"time"
"NanoKVM-Server/proto"
"github.com/gin-gonic/gin"
@@ -35,11 +36,15 @@ func (s *Service) OfflineUpdate(c *gin.Context) {
rsp.OkRsp(c)
log.Debugf("offline update application success")
time.Sleep(1 * time.Second)
_ = exec.Command("sh", "-c", "/kvmapp/system/init.d/S95nanokvm restart").Run()
go restartServices()
}
func offlineUpdate(c *gin.Context) error {
expectedSHA256, err := parseSHA256Checksum(c.GetHeader("X-SHA256-Checksum"))
if err != nil {
return err
}
_ = os.RemoveAll(CacheDir)
_ = os.MkdirAll(CacheDir, 0o755)
defer func() {
@@ -63,6 +68,11 @@ func offlineUpdate(c *gin.Context) error {
return err
}
if err := verifySHA256Checksum(target, expectedSHA256); err != nil {
log.Errorf("failed to verify install package: %v", err)
return err
}
if err := installPackage(target); err != nil {
log.Errorf("failed to install package: %v", err)
return err
@@ -71,6 +81,43 @@ func offlineUpdate(c *gin.Context) error {
return nil
}
func parseSHA256Checksum(value string) ([]byte, error) {
value = strings.TrimSpace(value)
if value == "" {
return nil, nil
}
checksum, err := hex.DecodeString(value)
if err != nil || len(checksum) != sha256.Size {
return nil, fmt.Errorf("invalid sha256 checksum")
}
return checksum, nil
}
func verifySHA256Checksum(filePath string, expected []byte) error {
if len(expected) == 0 {
return nil
}
file, err := os.Open(filePath)
if err != nil {
return fmt.Errorf("failed to open uploaded file: %w", err)
}
defer file.Close()
hasher := sha256.New()
if _, err := io.Copy(hasher, file); err != nil {
return fmt.Errorf("failed to calculate sha256 checksum: %w", err)
}
if subtle.ConstantTimeCompare(hasher.Sum(nil), expected) != 1 {
return fmt.Errorf("sha256 checksum mismatch")
}
return nil
}
func createSentinelFile() error {
file, err := os.OpenFile(
sentinelPath,

View File

@@ -14,6 +14,7 @@ import (
"time"
"NanoKVM-Server/proto"
"NanoKVM-Server/utils"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
@@ -32,7 +33,7 @@ const (
)
var (
latestClient = &http.Client{Timeout: 15 * time.Second}
latestClient = utils.NewUpdateHTTPClient(15 * time.Second)
packageNamePattern = regexp.MustCompile(`^nanokvm_[0-9]+\.[0-9]+\.[0-9]+\.tar\.gz$`)
versionPattern = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$`)
)
@@ -84,7 +85,11 @@ func getLatest() (*Latest, error) {
query.Set("now", fmt.Sprintf("%d", time.Now().Unix()))
parsedManifestURL.RawQuery = query.Encode()
resp, err := latestClient.Get(parsedManifestURL.String())
request, err := utils.NewAuthenticatedRequest("GET", parsedManifestURL.String(), nil)
if err != nil {
return nil, err
}
resp, err := latestClient.Do(request)
if err != nil {
log.Debugf("failed to request version from %s", parsedManifestURL.Redacted())
return nil, errors.New("update server is inaccessible")

View File

@@ -5,8 +5,10 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"
log "github.com/sirupsen/logrus"
@@ -14,7 +16,55 @@ import (
const maxDownloadSize = int64(1024 * 1024 * 1024)
var downloadClient = &http.Client{Timeout: 15 * time.Minute}
var downloadClient = NewUpdateHTTPClient(15 * time.Minute)
func NewAuthenticatedRequest(method string, rawURL string, body io.Reader) (*http.Request, error) {
req, err := http.NewRequest(method, rawURL, body)
if err != nil {
return nil, err
}
if req.URL.User != nil {
username := req.URL.User.Username()
password, _ := req.URL.User.Password()
req.SetBasicAuth(username, password)
// Keep credentials out of the request URL after copying them to the header.
req.URL.User = nil
}
return req, nil
}
func NewUpdateHTTPClient(timeout time.Duration) *http.Client {
return &http.Client{
Timeout: timeout,
CheckRedirect: preserveBasicAuthRedirect,
}
}
func preserveBasicAuthRedirect(req *http.Request, via []*http.Request) error {
if len(via) == 0 {
return nil
}
previous := via[len(via)-1]
authorization := previous.Header.Get("Authorization")
if authorization == "" {
return nil
}
if !sameUpdateHost(previous.URL, req.URL) ||
(previous.URL.Scheme == "https" && req.URL.Scheme != "https") {
return http.ErrUseLastResponse
}
req.Header.Set("Authorization", authorization)
return nil
}
func sameUpdateHost(left *url.URL, right *url.URL) bool {
return strings.EqualFold(left.Host, right.Host)
}
func Download(req *http.Request, target string) error {
log.Debugf("downloading %s to %s", req.URL.Redacted(), target)

View File

@@ -21,11 +21,12 @@ export function update() {
}
// offline update application
export function offlineUpdate(data: FormData) {
export function offlineUpdate(data: FormData, sha256Checksum = '') {
const baseUrl = getBaseUrl('http');
const url = `${baseUrl}/api/application/update/offline`;
return fetch(url, {
method: 'POST',
headers: sha256Checksum ? { 'X-SHA256-Checksum': sha256Checksum } : undefined,
body: data
});
}

View File

@@ -538,6 +538,11 @@ const ca = {
title: 'Actualitzacions fora de línia',
desc: "Actualització mitjançant el paquet d'instal·lació local",
upload: 'Puja',
checksumPlaceholder: 'Suma de verificació SHA-256 (opcional)',
invalidChecksum:
'La suma de verificació SHA-256 ha de contenir 64 caràcters hexadecimals.',
checksumMismatch:
'La verificació SHA-256 ha fallat. És possible que el paquet estigui malmès.',
invalidName: 'Format de nom de fitxer no vàlid. Baixeu-lo des de les versions de GitHub.',
updateFailed: 'Error en actualitzar. Torna-ho a intentar.'
}

View File

@@ -542,6 +542,9 @@ const cz = {
title: 'Offline aktualizace',
desc: 'Aktualizace prostřednictvím místního instalačního balíčku',
upload: 'Nahrát',
checksumPlaceholder: 'Kontrolní součet SHA-256 (volitelný)',
invalidChecksum: 'Kontrolní součet SHA-256 musí obsahovat 64 hexadecimálních znaků.',
checksumMismatch: 'Ověření SHA-256 se nezdařilo. Balíček může být poškozený.',
invalidName: 'Neplatný formát souboru. Stáhněte si prosím z vydání GitHubu.',
updateFailed: 'Aktualizace se nezdařila. Zkuste to prosím znovu.'
}

View File

@@ -540,6 +540,9 @@ const da = {
title: 'Offline opdateringer',
desc: 'Opdatering via lokal installationspakke',
upload: 'Upload',
checksumPlaceholder: 'SHA-256-kontrolsum (valgfri)',
invalidChecksum: 'SHA-256-kontrolsummen skal indeholde 64 hexadecimale tegn.',
checksumMismatch: 'SHA-256-verificeringen mislykkedes. Pakken kan være beskadiget.',
invalidName: 'Ugyldigt filnavnsformat. Download venligst fra GitHub-udgivelser.',
updateFailed: 'Opdatering fejlede. Prøv igen.'
}

View File

@@ -547,6 +547,10 @@ const de = {
title: 'Offline Aktualisierung',
desc: 'Über lokales Installationspaket aktualisieren',
upload: 'Hochladen',
checksumPlaceholder: 'SHA-256-Prüfsumme (optional)',
invalidChecksum: 'Die SHA-256-Prüfsumme muss 64 hexadezimale Zeichen enthalten.',
checksumMismatch:
'Die SHA-256-Überprüfung ist fehlgeschlagen. Das Paket ist möglicherweise beschädigt.',
invalidName:
'Ungültiges Dateinamenformat. Bitte laden Sie von den GitHub-Releases herunter.',
updateFailed: 'Aktualisierung fehlgeschlagen. Bitte versuchen Sie es erneut.'

View File

@@ -536,6 +536,9 @@ const en = {
title: 'Offline Updates',
desc: 'Update through local installation package',
upload: 'Upload',
checksumPlaceholder: 'SHA-256 checksum (optional)',
invalidChecksum: 'The SHA-256 checksum must contain 64 hexadecimal characters.',
checksumMismatch: 'SHA-256 verification failed. The package may be corrupted.',
invalidName: 'Invalid filename format. Please download from GitHub releases.',
updateFailed: 'Update failed. Please retry.'
}

View File

@@ -545,6 +545,11 @@ const es = {
title: 'Actualizaciones sin conexión',
desc: 'Actualización a través del paquete de instalación local',
upload: 'Subir',
checksumPlaceholder: 'Suma de comprobación SHA-256 (opcional)',
invalidChecksum:
'La suma de comprobación SHA-256 debe contener 64 caracteres hexadecimales.',
checksumMismatch:
'La verificación SHA-256 ha fallado. Es posible que el paquete esté dañado.',
invalidName:
'Formato de nombre de archivo no válido. Descargue desde las versiones de GitHub.',
updateFailed: 'La actualización falló. Por favor, inténtalo de nuevo.'

View File

@@ -547,6 +547,9 @@ const fr = {
title: 'Mises à jour hors ligne',
desc: "Mise à jour via le package d'installation local",
upload: 'Téléverser',
checksumPlaceholder: 'Somme de contrôle SHA-256 (facultative)',
invalidChecksum: 'La somme de contrôle SHA-256 doit contenir 64 caractères hexadécimaux.',
checksumMismatch: 'La vérification SHA-256 a échoué. Le paquet est peut-être endommagé.',
invalidName:
'Format de nom de fichier invalide. Veuillez télécharger à partir des versions de GitHub.',
updateFailed: 'Mise à jour échouée. Veuillez réessayer.'

View File

@@ -544,6 +544,9 @@ const hu = {
title: 'Offline frissítések',
desc: 'Frissítés helyi telepítőcsomaggal',
upload: 'Feltöltés',
checksumPlaceholder: 'SHA-256 ellenőrzőösszeg (opcionális)',
invalidChecksum: 'A SHA-256 ellenőrzőösszegnek 64 hexadecimális karakterből kell állnia.',
checksumMismatch: 'Az SHA-256 ellenőrzése sikertelen. Lehet, hogy a csomag sérült.',
invalidName: 'Érvénytelen fájlnévformátum. Kérjük, töltse le a GitHub kiadásaiból.',
updateFailed: 'Frissítés sikertelen. Kérem, próbálja újra.'
}

View File

@@ -541,6 +541,9 @@ const id = {
title: 'Pembaruan Offline',
desc: 'Perbarui melalui paket instalasi lokal',
upload: 'Mengunggah',
checksumPlaceholder: 'Checksum SHA-256 (opsional)',
invalidChecksum: 'Checksum SHA-256 harus berisi 64 karakter heksadesimal.',
checksumMismatch: 'Verifikasi SHA-256 gagal. Paket mungkin rusak.',
invalidName: 'Format nama file tidak valid. Silakan unduh dari rilis GitHub.',
updateFailed: 'Gagal memperbarui, tolong coba lagi.'
}

View File

@@ -546,6 +546,10 @@ const it = {
title: 'Aggiornamenti offline',
desc: 'Aggiornamento tramite pacchetto di installazione locale',
upload: 'Carica',
checksumPlaceholder: 'Checksum SHA-256 (facoltativo)',
invalidChecksum: 'Il checksum SHA-256 deve contenere 64 caratteri esadecimali.',
checksumMismatch:
'La verifica SHA-256 non è riuscita. Il pacchetto potrebbe essere danneggiato.',
invalidName: 'Formato nome file non valido. Si prega di scaricare dalle versioni GitHub.',
updateFailed: 'Aggiornamento fallito. Riprova.'
}

View File

@@ -544,6 +544,10 @@ const ja = {
title: 'オフラインアップデート',
desc: 'ローカルインストールパッケージでアップデートする',
upload: 'アップロード',
checksumPlaceholder: 'SHA-256チェックサム任意',
invalidChecksum: 'SHA-256チェックサムは64文字の16進数である必要があります。',
checksumMismatch:
'SHA-256の検証に失敗しました。パッケージが破損している可能性があります。',
invalidName:
'ファイル名の形式が正しくありません。GitHub リリースページにアクセスしてインストールパッケージをダウンロードしてください。',
updateFailed: 'アップデートに失敗しました。もう一度お試しください。'

View File

@@ -535,6 +535,9 @@ const ko = {
title: '오프라인 업데이트',
desc: '로컬 설치 패키지를 통한 업데이트',
upload: '업로드',
checksumPlaceholder: 'SHA-256 체크섬(선택 사항)',
invalidChecksum: 'SHA-256 체크섬은 64개의 16진수 문자를 포함해야 합니다.',
checksumMismatch: 'SHA-256 검증에 실패했습니다. 패키지가 손상되었을 수 있습니다.',
invalidName: '유효하지 않은 파일 이름 형식입니다. GitHub 릴리즈에서 다운로드하세요.',
updateFailed: '업데이트에 실패했습니다. 재시도하세요.'
}

View File

@@ -540,6 +540,9 @@ const nb = {
title: 'Offline oppdateringer',
desc: 'Oppdater gjennom lokal installasjonspakke',
upload: 'Last opp',
checksumPlaceholder: 'SHA-256-sjekksum (valgfritt)',
invalidChecksum: 'SHA-256-sjekksummen må inneholde 64 heksadesimale tegn.',
checksumMismatch: 'SHA-256-verifiseringen mislyktes. Pakken kan være skadet.',
invalidName: 'Ugyldig filnavnformat. Last ned fra GitHub-utgivelser.',
updateFailed: 'En feil oppstod under oppdatering. Vennligst forsøk igjen.'
}

View File

@@ -546,6 +546,9 @@ const nl = {
title: 'Offline-updates',
desc: 'Update via lokaal installatiepakket',
upload: 'Uploaden',
checksumPlaceholder: 'SHA-256-controlesom (optioneel)',
invalidChecksum: 'De SHA-256-controlesom moet 64 hexadecimale tekens bevatten.',
checksumMismatch: 'De SHA-256-verificatie is mislukt. Het pakket is mogelijk beschadigd.',
invalidName: 'Ongeldig bestandsnaamformaat. Download de versie van GitHub-releases.',
updateFailed: 'Update mislukt. Probeer het opnieuw.'
}

View File

@@ -544,6 +544,9 @@ const pl = {
title: 'Aktualizacje offline',
desc: 'Aktualizacja poprzez lokalny pakiet instalacyjny',
upload: 'Prześlij',
checksumPlaceholder: 'Suma kontrolna SHA-256 (opcjonalnie)',
invalidChecksum: 'Suma kontrolna SHA-256 musi zawierać 64 znaki szesnastkowe.',
checksumMismatch: 'Weryfikacja SHA-256 nie powiodła się. Pakiet może być uszkodzony.',
invalidName: 'Nieprawidłowy format nazwy pliku. Proszę pobrać z wydań GitHub.',
updateFailed: 'Aktualizacja nie powiodła się. Spróbuj ponownie.'
}

View File

@@ -542,6 +542,9 @@ const pt_br = {
title: 'Atualizações off-line',
desc: 'Atualização através do pacote de instalação local',
upload: 'Upload',
checksumPlaceholder: 'Soma de verificação SHA-256 (opcional)',
invalidChecksum: 'A soma de verificação SHA-256 deve conter 64 caracteres hexadecimais.',
checksumMismatch: 'A verificação SHA-256 falhou. O pacote pode estar corrompido.',
invalidName: 'Formato de nome de arquivo inválido. Faça download das versões do GitHub.',
updateFailed: 'Falha na atualização. Por favor, tente novamente.'
}

View File

@@ -543,6 +543,10 @@ const ru = {
title: 'Автономные обновления',
desc: 'Обновление через локальный установочный пакет',
upload: 'Загрузить',
checksumPlaceholder: 'Контрольная сумма SHA-256 (необязательно)',
invalidChecksum:
'Контрольная сумма SHA-256 должна содержать 64 шестнадцатеричных символа.',
checksumMismatch: 'Проверка SHA-256 не пройдена. Возможно, пакет повреждён.',
invalidName: 'Неверный формат имени файла. Загрузите выпуски с GitHub.',
updateFailed: 'Обновление не удалось. Пожалуйста, попробуйте еще раз.'
}

View File

@@ -538,6 +538,9 @@ const se = {
title: 'Offlineuppdateringar',
desc: 'Uppdatera genom lokalt installationspaket',
upload: 'Ladda upp',
checksumPlaceholder: 'SHA-256-kontrollsumma (valfri)',
invalidChecksum: 'SHA-256-kontrollsumman måste innehålla 64 hexadecimala tecken.',
checksumMismatch: 'SHA-256-verifieringen misslyckades. Paketet kan vara skadat.',
invalidName: 'Ogiltigt filnamnsformat. Ladda ner från GitHub-versioner.',
updateFailed: 'Uppdatering misslyckades. Försök igen.'
}

View File

@@ -533,6 +533,9 @@ const th = {
title: 'อัปเดตออฟไลน์',
desc: 'อัปเดตผ่านแพ็คเกจการติดตั้งในเครื่อง',
upload: 'อัปโหลด',
checksumPlaceholder: 'ผลรวมตรวจสอบ SHA-256 (ไม่บังคับ)',
invalidChecksum: 'ผลรวมตรวจสอบ SHA-256 ต้องมีอักขระเลขฐานสิบหก 64 ตัว',
checksumMismatch: 'การตรวจสอบ SHA-256 ล้มเหลว แพ็กเกจอาจเสียหาย',
invalidName: 'รูปแบบชื่อไฟล์ไม่ถูกต้อง กรุณาดาวน์โหลดจากรุ่น GitHub',
updateFailed: 'การอัปเดตล้มเหลว กรุณาลองใหม่'
}

View File

@@ -542,6 +542,9 @@ const tr = {
title: 'Çevrimdışı Güncellemeler',
desc: 'Yerel kurulum paketi aracılığıyla güncelleme',
upload: 'Yükle',
checksumPlaceholder: 'SHA-256 sağlama toplamı (isteğe bağlı)',
invalidChecksum: 'SHA-256 sağlama toplamı 64 onaltılık karakter içermelidir.',
checksumMismatch: 'SHA-256 doğrulaması başarısız oldu. Paket bozulmuş olabilir.',
invalidName: 'Geçersiz dosya adı biçimi. Lütfen GitHub sürümlerinden indirin.',
updateFailed: 'Güncelleme başarısız oldu. Lütfen tekrar deneyin.'
}

View File

@@ -541,6 +541,9 @@ const uk = {
title: 'Оновлення в автономному режимі',
desc: 'Оновлення через локальний інсталяційний пакет',
upload: 'Завантажити',
checksumPlaceholder: "Контрольна сума SHA-256 (необов'язково)",
invalidChecksum: 'Контрольна сума SHA-256 має містити 64 шістнадцяткові символи.',
checksumMismatch: 'Не вдалося перевірити SHA-256. Пакунок може бути пошкоджений.',
invalidName: 'Недійсний формат імені файлу. Завантажте випуски з GitHub.',
updateFailed: 'Оновлення не вдалося. Будь ласка, спробуйте ще раз.'
}

View File

@@ -539,6 +539,9 @@ const vi = {
title: 'Cập nhật ngoại tuyến',
desc: 'Cập nhật thông qua gói cài đặt cục bộ',
upload: 'Tải lên',
checksumPlaceholder: 'Tổng kiểm SHA-256 (không bắt buộc)',
invalidChecksum: 'Tổng kiểm SHA-256 phải chứa 64 ký tự thập lục phân.',
checksumMismatch: 'Xác minh SHA-256 không thành công. Gói có thể đã bị hỏng.',
invalidName:
'Định dạng tên tệp không hợp lệ. Vui lòng tải xuống từ bản phát hành GitHub.',
updateFailed: 'Cập nhật thất bại. Vui lòng thử lại.'

View File

@@ -525,6 +525,9 @@ const zh = {
title: '离线更新',
desc: '通过本地安装包进行更新',
upload: '上传',
checksumPlaceholder: 'SHA-256 校验和(可选)',
invalidChecksum: 'SHA-256 校验和必须为 64 位十六进制字符。',
checksumMismatch: 'SHA-256 校验失败,安装包可能已损坏。',
invalidName: '文件名格式错误,请前往 GitHub 发布页下载安装包。',
updateFailed: '更新失败,请重试'
}

View File

@@ -525,6 +525,9 @@ const zh_tw = {
title: '離線更新',
desc: '透過本地安裝包進行更新',
upload: '上傳',
checksumPlaceholder: 'SHA-256 校驗和(選填)',
invalidChecksum: 'SHA-256 校驗和必須包含 64 個十六進位字元。',
checksumMismatch: 'SHA-256 驗證失敗。套件可能已損毀。',
invalidName: '檔名格式錯誤,請前往 GitHub 釋出頁下載安裝包。',
updateFailed: '更新失敗,請重試'
}

View File

@@ -10,9 +10,14 @@ const OFFICIAL_UPDATE_SERVER = 'https://cdn.sipeed.com/nanokvm';
interface CustomServerProps {
checkForUpdates: () => void;
onEnabledChange: (enabled: boolean) => void;
onPendingChange: (pending: boolean) => void;
}
export const CustomServer = ({ checkForUpdates, onEnabledChange }: CustomServerProps) => {
export const CustomServer = ({
checkForUpdates,
onEnabledChange,
onPendingChange
}: CustomServerProps) => {
const { t } = useTranslation();
const [savedConfig, setSavedConfig] = useState<api.UpdateServerConfig>({
@@ -99,7 +104,6 @@ export const CustomServer = ({ checkForUpdates, onEnabledChange }: CustomServerP
function confirmRisk() {
setEnabled(true);
setError('');
onEnabledChange(true);
setIsConfirmOpen(false);
}
@@ -136,6 +140,11 @@ export const CustomServer = ({ checkForUpdates, onEnabledChange }: CustomServerP
}
const hasChanges = enabled !== savedConfig.enabled || url.trim() !== savedConfig.url;
useEffect(() => {
onPendingChange(hasChanges);
}, [hasChanges, onPendingChange]);
const modalTitle = (
<div className="flex items-center space-x-1 text-red-500">
<TriangleAlertIcon size={18} />

View File

@@ -22,6 +22,7 @@ export const Update = ({ setIsLocked }: UpdateProps) => {
const [latestVersion, setLatestVersion] = useState('');
const [errMsg, setErrMsg] = useState('');
const [isCustomServerEnabled, setIsCustomServerEnabled] = useState(false);
const [isCustomServerPending, setIsCustomServerPending] = useState(false);
const versionRequestRef = useRef(0);
useEffect(() => {
@@ -60,7 +61,7 @@ export const Update = ({ setIsLocked }: UpdateProps) => {
}
function update() {
if (status !== 'outdated') return;
if (status !== 'outdated' || isCustomServerPending) return;
setIsLocked(true);
setStatus('updating');
@@ -88,8 +89,15 @@ export const Update = ({ setIsLocked }: UpdateProps) => {
<div className="text-base">{t('settings.update.title')}</div>
<Divider className="opacity-50" />
<Preview checkForUpdates={checkForUpdates} disabled={isCustomServerEnabled} />
<CustomServer checkForUpdates={checkForUpdates} onEnabledChange={setIsCustomServerEnabled} />
<Preview
checkForUpdates={checkForUpdates}
disabled={isCustomServerEnabled || isCustomServerPending}
/>
<CustomServer
checkForUpdates={checkForUpdates}
onEnabledChange={setIsCustomServerEnabled}
onPendingChange={setIsCustomServerPending}
/>
<Offline
status={status}
setStatus={setStatus}
@@ -133,7 +141,12 @@ export const Update = ({ setIsLocked }: UpdateProps) => {
title={`${currentVersion} -> ${latestVersion}`}
subTitle={t('settings.update.available')}
extra={[
<Button key="confirm" type="primary" onClick={update}>
<Button
key="confirm"
type="primary"
disabled={isCustomServerPending}
onClick={update}
>
{t('settings.update.confirm')}
</Button>
]}

View File

@@ -1,5 +1,5 @@
import { useRef } from 'react';
import { Button } from 'antd';
import { useRef, useState } from 'react';
import { Button, Input } from 'antd';
import { ExternalLinkIcon } from 'lucide-react';
import { useTranslation } from 'react-i18next';
@@ -16,6 +16,7 @@ export const Offline = ({ status, setStatus, setIsLocked, setErrMsg }: UpdatePro
const { t } = useTranslation();
const inputRef = useRef<HTMLInputElement | null>(null);
const [sha256Checksum, setSha256Checksum] = useState('');
function handleClick() {
inputRef.current?.click();
@@ -26,6 +27,7 @@ export const Offline = ({ status, setStatus, setIsLocked, setErrMsg }: UpdatePro
if (!file) {
return;
}
e.target.value = '';
if (!validateFilename(file.name)) {
setStatus('failed');
@@ -39,6 +41,13 @@ export const Offline = ({ status, setStatus, setIsLocked, setErrMsg }: UpdatePro
function upload(file: File | null) {
if (!file) return;
const checksum = sha256Checksum.trim();
if (checksum && !/^[a-fA-F0-9]{64}$/.test(checksum)) {
setStatus('failed');
setErrMsg(t('settings.update.offline.invalidChecksum'));
return;
}
if (!validateFilename(file.name)) {
setStatus('failed');
setErrMsg(t('settings.update.offline.invalidName'));
@@ -57,28 +66,30 @@ export const Offline = ({ status, setStatus, setIsLocked, setErrMsg }: UpdatePro
formData.append('file', file);
api
.offlineUpdate(formData)
.offlineUpdate(formData, checksum)
.then((rsp: Response) => {
// Prüfen ob HTTP OK
if (!rsp.ok) throw new Error(`HTTP error ${rsp.status}`);
return rsp.json(); // JSON-Payload parsen
return rsp.json();
})
.then((rspj: any) => {
// Jetzt rspj ist das tatsächliche JSON
if (rspj.code !== 0 || !rspj.data) {
setStatus('failed');
setErrMsg(rspj.msg || t('settings.update.offline.updateFailed'));
console.log(rspj);
return;
if (rspj.code !== 0) {
const message = rspj.msg?.includes('sha256 checksum mismatch')
? t('settings.update.offline.checksumMismatch')
: rspj.msg || t('settings.update.offline.updateFailed');
throw new Error(message);
}
})
.finally(() => {
setTimeout(() => {
setIsLocked(false);
setErrMsg('');
window.location.reload();
}, 12000);
})
.catch((error: unknown) => {
setIsLocked(false);
setStatus('failed');
setErrMsg(
error instanceof Error ? error.message : t('settings.update.offline.updateFailed')
);
});
}
@@ -89,31 +100,44 @@ export const Offline = ({ status, setStatus, setIsLocked, setErrMsg }: UpdatePro
return (
<>
<div className="mt-8 flex items-center justify-between">
<div className="flex flex-col space-y-1">
<div className="flex items-center space-x-2">
<span>{t('settings.update.offline.title')}</span>
<div className="mt-8 flex flex-col gap-3">
<div className="flex items-center justify-between gap-4">
<div className="flex flex-col space-y-1">
<div className="flex items-center space-x-2">
<span>{t('settings.update.offline.title')}</span>
<a
className="flex items-center text-neutral-500 hover:text-blue-500"
href="https://github.com/sipeed/NanoKVM/releases"
target="_blank"
>
<ExternalLinkIcon size={15} />
</a>
<a
className="flex items-center text-neutral-500 hover:text-blue-500"
href="https://github.com/sipeed/NanoKVM/releases"
target="_blank"
>
<ExternalLinkIcon size={15} />
</a>
</div>
<span className="text-xs text-neutral-500">{t('settings.update.offline.desc')}</span>
</div>
<span className="text-xs text-neutral-500">{t('settings.update.offline.desc')}</span>
<input
id="file-upload"
ref={inputRef}
type="file"
accept=".tar.gz"
onChange={handleFileChange}
className="hidden"
/>
<Button disabled={status === 'loading' || status === 'updating'} onClick={handleClick}>
{t('settings.update.offline.upload')}
</Button>
</div>
<input
id="file-upload"
ref={inputRef}
type="file"
onChange={handleFileChange}
className="hidden"
<Input
value={sha256Checksum}
maxLength={64}
disabled={status === 'updating'}
placeholder={t('settings.update.offline.checksumPlaceholder')}
onChange={(event) => setSha256Checksum(event.target.value)}
/>
<Button onClick={handleClick}>{t('settings.update.offline.upload')}</Button>
</div>
</>
);