diff --git a/server/service/application/update.go b/server/service/application/update.go index 90330cb..dfd4f64 100644 --- a/server/service/application/update.go +++ b/server/service/application/update.go @@ -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 diff --git a/server/service/application/update_offline.go b/server/service/application/update_offline.go index f2aa8b6..982bf7b 100644 --- a/server/service/application/update_offline.go +++ b/server/service/application/update_offline.go @@ -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, diff --git a/server/service/application/version.go b/server/service/application/version.go index b0ddb1b..05cca56 100644 --- a/server/service/application/version.go +++ b/server/service/application/version.go @@ -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") diff --git a/server/utils/http.go b/server/utils/http.go index f297685..dfa4fc9 100644 --- a/server/utils/http.go +++ b/server/utils/http.go @@ -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) diff --git a/web/src/api/application.ts b/web/src/api/application.ts index 8c7f4e3..2d91449 100644 --- a/web/src/api/application.ts +++ b/web/src/api/application.ts @@ -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 }); } diff --git a/web/src/i18n/locales/ca.ts b/web/src/i18n/locales/ca.ts index b3e4b90..d0f9d0d 100644 --- a/web/src/i18n/locales/ca.ts +++ b/web/src/i18n/locales/ca.ts @@ -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.' } diff --git a/web/src/i18n/locales/cz.ts b/web/src/i18n/locales/cz.ts index 1864278..6e2f094 100644 --- a/web/src/i18n/locales/cz.ts +++ b/web/src/i18n/locales/cz.ts @@ -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.' } diff --git a/web/src/i18n/locales/da.ts b/web/src/i18n/locales/da.ts index a013deb..ac42607 100644 --- a/web/src/i18n/locales/da.ts +++ b/web/src/i18n/locales/da.ts @@ -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.' } diff --git a/web/src/i18n/locales/de.ts b/web/src/i18n/locales/de.ts index dd940a8..f093764 100644 --- a/web/src/i18n/locales/de.ts +++ b/web/src/i18n/locales/de.ts @@ -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.' diff --git a/web/src/i18n/locales/en.ts b/web/src/i18n/locales/en.ts index 215f957..087ad34 100644 --- a/web/src/i18n/locales/en.ts +++ b/web/src/i18n/locales/en.ts @@ -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.' } diff --git a/web/src/i18n/locales/es.ts b/web/src/i18n/locales/es.ts index 0325ef9..8cc94f5 100644 --- a/web/src/i18n/locales/es.ts +++ b/web/src/i18n/locales/es.ts @@ -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.' diff --git a/web/src/i18n/locales/fr.ts b/web/src/i18n/locales/fr.ts index 7179124..c7b8e6f 100644 --- a/web/src/i18n/locales/fr.ts +++ b/web/src/i18n/locales/fr.ts @@ -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.' diff --git a/web/src/i18n/locales/hu.ts b/web/src/i18n/locales/hu.ts index bc0679b..a2ab1d1 100644 --- a/web/src/i18n/locales/hu.ts +++ b/web/src/i18n/locales/hu.ts @@ -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.' } diff --git a/web/src/i18n/locales/id.ts b/web/src/i18n/locales/id.ts index 46fa573..f2e63eb 100644 --- a/web/src/i18n/locales/id.ts +++ b/web/src/i18n/locales/id.ts @@ -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.' } diff --git a/web/src/i18n/locales/it.ts b/web/src/i18n/locales/it.ts index 0ca9388..8a65b9f 100644 --- a/web/src/i18n/locales/it.ts +++ b/web/src/i18n/locales/it.ts @@ -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.' } diff --git a/web/src/i18n/locales/ja.ts b/web/src/i18n/locales/ja.ts index 2f35f9b..f3a0594 100644 --- a/web/src/i18n/locales/ja.ts +++ b/web/src/i18n/locales/ja.ts @@ -544,6 +544,10 @@ const ja = { title: 'オフラインアップデート', desc: 'ローカルインストールパッケージでアップデートする', upload: 'アップロード', + checksumPlaceholder: 'SHA-256チェックサム(任意)', + invalidChecksum: 'SHA-256チェックサムは64文字の16進数である必要があります。', + checksumMismatch: + 'SHA-256の検証に失敗しました。パッケージが破損している可能性があります。', invalidName: 'ファイル名の形式が正しくありません。GitHub リリースページにアクセスしてインストールパッケージをダウンロードしてください。', updateFailed: 'アップデートに失敗しました。もう一度お試しください。' diff --git a/web/src/i18n/locales/ko.ts b/web/src/i18n/locales/ko.ts index 384a6fb..cf36851 100644 --- a/web/src/i18n/locales/ko.ts +++ b/web/src/i18n/locales/ko.ts @@ -535,6 +535,9 @@ const ko = { title: '오프라인 업데이트', desc: '로컬 설치 패키지를 통한 업데이트', upload: '업로드', + checksumPlaceholder: 'SHA-256 체크섬(선택 사항)', + invalidChecksum: 'SHA-256 체크섬은 64개의 16진수 문자를 포함해야 합니다.', + checksumMismatch: 'SHA-256 검증에 실패했습니다. 패키지가 손상되었을 수 있습니다.', invalidName: '유효하지 않은 파일 이름 형식입니다. GitHub 릴리즈에서 다운로드하세요.', updateFailed: '업데이트에 실패했습니다. 재시도하세요.' } diff --git a/web/src/i18n/locales/nb.ts b/web/src/i18n/locales/nb.ts index 788b139..9a22318 100644 --- a/web/src/i18n/locales/nb.ts +++ b/web/src/i18n/locales/nb.ts @@ -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.' } diff --git a/web/src/i18n/locales/nl.ts b/web/src/i18n/locales/nl.ts index fd074c7..5652acc 100644 --- a/web/src/i18n/locales/nl.ts +++ b/web/src/i18n/locales/nl.ts @@ -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.' } diff --git a/web/src/i18n/locales/pl.ts b/web/src/i18n/locales/pl.ts index 8701567..a13a10d 100644 --- a/web/src/i18n/locales/pl.ts +++ b/web/src/i18n/locales/pl.ts @@ -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.' } diff --git a/web/src/i18n/locales/pt_br.ts b/web/src/i18n/locales/pt_br.ts index 435ed2b..161c293 100644 --- a/web/src/i18n/locales/pt_br.ts +++ b/web/src/i18n/locales/pt_br.ts @@ -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.' } diff --git a/web/src/i18n/locales/ru.ts b/web/src/i18n/locales/ru.ts index 6c5ed5f..f1928fc 100644 --- a/web/src/i18n/locales/ru.ts +++ b/web/src/i18n/locales/ru.ts @@ -543,6 +543,10 @@ const ru = { title: 'Автономные обновления', desc: 'Обновление через локальный установочный пакет', upload: 'Загрузить', + checksumPlaceholder: 'Контрольная сумма SHA-256 (необязательно)', + invalidChecksum: + 'Контрольная сумма SHA-256 должна содержать 64 шестнадцатеричных символа.', + checksumMismatch: 'Проверка SHA-256 не пройдена. Возможно, пакет повреждён.', invalidName: 'Неверный формат имени файла. Загрузите выпуски с GitHub.', updateFailed: 'Обновление не удалось. Пожалуйста, попробуйте еще раз.' } diff --git a/web/src/i18n/locales/se.ts b/web/src/i18n/locales/se.ts index ec84bca..53a778d 100644 --- a/web/src/i18n/locales/se.ts +++ b/web/src/i18n/locales/se.ts @@ -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.' } diff --git a/web/src/i18n/locales/th.ts b/web/src/i18n/locales/th.ts index 847870d..dd7aae8 100644 --- a/web/src/i18n/locales/th.ts +++ b/web/src/i18n/locales/th.ts @@ -533,6 +533,9 @@ const th = { title: 'อัปเดตออฟไลน์', desc: 'อัปเดตผ่านแพ็คเกจการติดตั้งในเครื่อง', upload: 'อัปโหลด', + checksumPlaceholder: 'ผลรวมตรวจสอบ SHA-256 (ไม่บังคับ)', + invalidChecksum: 'ผลรวมตรวจสอบ SHA-256 ต้องมีอักขระเลขฐานสิบหก 64 ตัว', + checksumMismatch: 'การตรวจสอบ SHA-256 ล้มเหลว แพ็กเกจอาจเสียหาย', invalidName: 'รูปแบบชื่อไฟล์ไม่ถูกต้อง กรุณาดาวน์โหลดจากรุ่น GitHub', updateFailed: 'การอัปเดตล้มเหลว กรุณาลองใหม่' } diff --git a/web/src/i18n/locales/tr.ts b/web/src/i18n/locales/tr.ts index d5e457f..cee7d3e 100644 --- a/web/src/i18n/locales/tr.ts +++ b/web/src/i18n/locales/tr.ts @@ -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.' } diff --git a/web/src/i18n/locales/uk.ts b/web/src/i18n/locales/uk.ts index 4f23446..f09dd80 100644 --- a/web/src/i18n/locales/uk.ts +++ b/web/src/i18n/locales/uk.ts @@ -541,6 +541,9 @@ const uk = { title: 'Оновлення в автономному режимі', desc: 'Оновлення через локальний інсталяційний пакет', upload: 'Завантажити', + checksumPlaceholder: "Контрольна сума SHA-256 (необов'язково)", + invalidChecksum: 'Контрольна сума SHA-256 має містити 64 шістнадцяткові символи.', + checksumMismatch: 'Не вдалося перевірити SHA-256. Пакунок може бути пошкоджений.', invalidName: 'Недійсний формат імені файлу. Завантажте випуски з GitHub.', updateFailed: 'Оновлення не вдалося. Будь ласка, спробуйте ще раз.' } diff --git a/web/src/i18n/locales/vi.ts b/web/src/i18n/locales/vi.ts index ae2b79d..f0ec1e7 100644 --- a/web/src/i18n/locales/vi.ts +++ b/web/src/i18n/locales/vi.ts @@ -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.' diff --git a/web/src/i18n/locales/zh.ts b/web/src/i18n/locales/zh.ts index 95c64ae..b7a997d 100644 --- a/web/src/i18n/locales/zh.ts +++ b/web/src/i18n/locales/zh.ts @@ -525,6 +525,9 @@ const zh = { title: '离线更新', desc: '通过本地安装包进行更新', upload: '上传', + checksumPlaceholder: 'SHA-256 校验和(可选)', + invalidChecksum: 'SHA-256 校验和必须为 64 位十六进制字符。', + checksumMismatch: 'SHA-256 校验失败,安装包可能已损坏。', invalidName: '文件名格式错误,请前往 GitHub 发布页下载安装包。', updateFailed: '更新失败,请重试' } diff --git a/web/src/i18n/locales/zh_tw.ts b/web/src/i18n/locales/zh_tw.ts index 2276c3b..1d12530 100644 --- a/web/src/i18n/locales/zh_tw.ts +++ b/web/src/i18n/locales/zh_tw.ts @@ -525,6 +525,9 @@ const zh_tw = { title: '離線更新', desc: '透過本地安裝包進行更新', upload: '上傳', + checksumPlaceholder: 'SHA-256 校驗和(選填)', + invalidChecksum: 'SHA-256 校驗和必須包含 64 個十六進位字元。', + checksumMismatch: 'SHA-256 驗證失敗。套件可能已損毀。', invalidName: '檔名格式錯誤,請前往 GitHub 釋出頁下載安裝包。', updateFailed: '更新失敗,請重試' } diff --git a/web/src/pages/desktop/menu/settings/update/custom-server.tsx b/web/src/pages/desktop/menu/settings/update/custom-server.tsx index c7fd967..96dc304 100644 --- a/web/src/pages/desktop/menu/settings/update/custom-server.tsx +++ b/web/src/pages/desktop/menu/settings/update/custom-server.tsx @@ -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({ @@ -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 = (
diff --git a/web/src/pages/desktop/menu/settings/update/index.tsx b/web/src/pages/desktop/menu/settings/update/index.tsx index 4726fce..58f7a0e 100644 --- a/web/src/pages/desktop/menu/settings/update/index.tsx +++ b/web/src/pages/desktop/menu/settings/update/index.tsx @@ -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) => {
{t('settings.update.title')}
- - + + { title={`${currentVersion} -> ${latestVersion}`} subTitle={t('settings.update.available')} extra={[ - ]} diff --git a/web/src/pages/desktop/menu/settings/update/offline.tsx b/web/src/pages/desktop/menu/settings/update/offline.tsx index a1115a7..093c44f 100644 --- a/web/src/pages/desktop/menu/settings/update/offline.tsx +++ b/web/src/pages/desktop/menu/settings/update/offline.tsx @@ -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(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 ( <> -
-
-
- {t('settings.update.offline.title')} +
+
+
+
+ {t('settings.update.offline.title')} - - - + + + +
+ + {t('settings.update.offline.desc')}
- {t('settings.update.offline.desc')} + +
- setSha256Checksum(event.target.value)} /> -
);