// updater.js - Moderner Auto-Updater für Git Manager GUI const { app, shell } = require('electron'); const https = require('https'); const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const { spawn } = require('child_process'); const GITEA_API_URL = 'https://git.viper.ipv64.net/api/v1/repos/M_Viper/Git-Manager-Gui/releases'; const TRUSTED_UPDATE_HOST = 'git.viper.ipv64.net'; class Updater { constructor(mainWindow) { this.mainWindow = mainWindow; this.checkingForUpdates = false; } /** * Hauptfunktion zur Prüfung auf Updates */ async checkForUpdates(silent = false) { if (this.checkingForUpdates) return; this.checkingForUpdates = true; try { const latestRelease = await this.getLatestRelease(); if (!latestRelease) return; // Versionen säubern (nur Zahlen und Punkte) const serverVer = latestRelease.tag_name.replace(/[^\d.]/g, ''); const localVer = app.getVersion().replace(/[^\d.]/g, ''); console.log(`[Updater] Version-Check: Server(${serverVer}) vs Lokal(${localVer})`); if (this.compareVersions(serverVer, localVer) > 0) { const asset = this.findAsset(latestRelease.assets); const checksumAsset = this.findChecksumAsset(latestRelease.assets, asset); const expectedSha256 = this.extractChecksumFromReleaseBody(latestRelease.body, asset?.name); console.log("[Updater] Update verfügbar. Sende Daten an Renderer..."); this.mainWindow.webContents.send('update-available', { version: serverVer, body: latestRelease.body, url: latestRelease.html_url, asset: asset ? { ...asset, checksumUrl: checksumAsset ? checksumAsset.browser_download_url : null, expectedSha256 } : null }); } else { console.log("[Updater] Anwendung ist auf dem neuesten Stand."); if (!silent) { this.mainWindow.webContents.send('update-not-available', { version: localVer }); } } } catch (error) { console.error('[Updater] Fehler beim Update-Check:', error); } finally { this.checkingForUpdates = false; } } async getLatestRelease() { return new Promise((resolve, reject) => { const options = { headers: { 'User-Agent': 'GitManager-GUI-Updater' } }; https.get(GITEA_API_URL, options, (res) => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => { try { const releases = JSON.parse(data); resolve(Array.isArray(releases) ? releases[0] : null); } catch (e) { reject(e); } }); }).on('error', reject); }); } compareVersions(v1, v2) { const a = v1.split('.').map(Number); const b = v2.split('.').map(Number); for (let i = 0; i < 3; i++) { if ((a[i] || 0) > (b[i] || 0)) return 1; if ((a[i] || 0) < (b[i] || 0)) return -1; } return 0; } findAsset(assets) { if (!assets) return null; const ext = process.platform === 'win32' ? '.exe' : '.AppImage'; return assets.find(a => { const name = String(a?.name || '').toLowerCase(); const validName = /^[a-z0-9._-]+$/.test(name); return validName && name.endsWith(ext); }); } findChecksumAsset(assets, targetAsset) { if (!Array.isArray(assets) || !targetAsset?.name) return null; const targetLower = String(targetAsset.name).toLowerCase(); const exactCandidates = [ `${targetLower}.sha256`, `${targetLower}.sha256sum`, `${targetLower}.sha512`, `${targetLower}.sha512sum` ]; const exact = assets.find(a => exactCandidates.includes(String(a?.name || '').toLowerCase())); if (exact) return exact; return assets.find(a => { const name = String(a?.name || '').toLowerCase(); return name.includes('checksum') || name.includes('checksums') || name.endsWith('.sha256') || name.endsWith('.sha256sum'); }) || null; } extractChecksumFromReleaseBody(body, fileName) { const text = String(body || ''); const target = String(fileName || '').trim(); if (!text || !target) return null; const lines = text.split(/\r?\n/); const targetLower = target.toLowerCase(); for (const line of lines) { const normalized = String(line || '').trim(); if (!normalized) continue; const match = normalized.match(/\b([a-fA-F0-9]{64})\b/); if (!match) continue; if (normalized.toLowerCase().includes(targetLower)) { return match[1].toLowerCase(); } } return null; } downloadText(url) { return new Promise((resolve, reject) => { if (!this.isTrustedDownloadUrl(url)) { reject(new Error('Unsichere Checksum-URL blockiert.')); return; } https.get(url, { headers: { 'User-Agent': 'Electron-Updater' } }, (res) => { if (res.statusCode === 301 || res.statusCode === 302) { return resolve(this.downloadText(res.headers.location || '')); } if (res.statusCode !== 200) { reject(new Error(`Checksum-Download fehlgeschlagen: HTTP ${res.statusCode}`)); return; } let data = ''; res.on('data', chunk => data += chunk.toString('utf8')); res.on('end', () => resolve(data)); }).on('error', reject); }); } extractChecksumFromText(text, fileName) { const lines = String(text || '').split(/\r?\n/); const targetLower = String(fileName || '').toLowerCase(); for (const line of lines) { const normalized = String(line || '').trim(); if (!normalized) continue; const hashMatch = normalized.match(/\b([a-fA-F0-9]{64})\b/); if (!hashMatch) continue; if (!targetLower || normalized.toLowerCase().includes(targetLower)) { return hashMatch[1].toLowerCase(); } } return null; } computeFileSha256(filePath) { return new Promise((resolve, reject) => { const hash = crypto.createHash('sha256'); const stream = fs.createReadStream(filePath); stream.on('error', reject); stream.on('data', chunk => hash.update(chunk)); stream.on('end', () => resolve(hash.digest('hex').toLowerCase())); }); } async resolveExpectedSha256(asset) { const expectedFromAsset = String(asset?.expectedSha256 || '').trim().toLowerCase(); if (/^[a-f0-9]{64}$/.test(expectedFromAsset)) return expectedFromAsset; const checksumUrl = String(asset?.checksumUrl || '').trim(); if (!checksumUrl) return null; const checksumText = await this.downloadText(checksumUrl); return this.extractChecksumFromText(checksumText, asset?.name); } isTrustedDownloadUrl(rawUrl) { try { const parsed = new URL(String(rawUrl || '')); return parsed.protocol === 'https:' && parsed.hostname === TRUSTED_UPDATE_HOST; } catch (_) { return false; } } /** * Startet den Download und führt die Installation aus */ async startDownload(asset) { if (!asset || !asset.browser_download_url) { console.error("[Updater] Kein gültiges Asset gefunden!"); return; } if (!this.isTrustedDownloadUrl(asset.browser_download_url)) { console.error('[Updater] Unsichere Download-URL blockiert.'); return; } const tempPath = path.join(app.getPath('temp'), asset.name); console.log(`[Updater] Download gestartet: ${asset.name} -> ${tempPath}`); let expectedSha256 = null; try { expectedSha256 = await this.resolveExpectedSha256(asset); } catch (e) { console.error('[Updater] Konnte erwartete Checksumme nicht laden:', e?.message || e); return; } if (!expectedSha256) { console.error('[Updater] Kein SHA-256-Checksum-Wert gefunden. Update wurde aus Sicherheitsgruenden blockiert.'); return; } const file = fs.createWriteStream(tempPath); const download = (url) => { if (!this.isTrustedDownloadUrl(url)) { console.error('[Updater] Unsicherer Redirect/Download blockiert.'); fs.unlink(tempPath, () => {}); return; } https.get(url, { headers: { 'User-Agent': 'Electron-Updater' } }, (res) => { // Handle Redirects if (res.statusCode === 301 || res.statusCode === 302) { return download(res.headers.location || ''); } if (res.statusCode !== 200) { console.error(`[Updater] Download-Fehler: Status ${res.statusCode}`); return; } res.pipe(file); file.on('finish', async () => { file.close(); try { const actualSha256 = await this.computeFileSha256(tempPath); if (actualSha256 !== expectedSha256) { console.error('[Updater] Checksum-Validierung fehlgeschlagen. Installation wurde blockiert.'); fs.unlink(tempPath, () => {}); return; } } catch (verifyErr) { console.error('[Updater] Checksum-Validierung konnte nicht ausgeführt werden:', verifyErr?.message || verifyErr); fs.unlink(tempPath, () => {}); return; } console.log("[Updater] Download und Checksum-Validierung abgeschlossen. Initialisiere entkoppelten Installer..."); this.installAndQuit(tempPath); }); }).on('error', (err) => { fs.unlink(tempPath, () => {}); console.error("[Updater] Netzwerkfehler beim Download:", err); }); }; download(asset.browser_download_url); } /** * Führt den Installer entkoppelt aus und schließt die App mit Verzögerung */ installAndQuit(filePath) { console.log(`[Updater] Bereite Installation vor: ${filePath}`); if (process.platform === 'win32') { try { const child = spawn(filePath, [], { detached: true, stdio: 'ignore', shell: false }); child.unref(); console.log("[Updater] Installer-Prozess entkoppelt gestartet. App schließt in 2 Sek..."); // WICHTIG: Wir geben Windows Zeit, den Installer stabil zu laden, // bevor wir die App schließen und die Dateisperren aufheben. setTimeout(() => { app.quit(); }, 2000); } catch (err) { console.error("[Updater] Kritischer Fehler beim Installer-Start:", err); // Notfall-Versuch über shell shell.openPath(filePath).then(() => app.quit()); } } else { // Linux/AppImage shell.openPath(filePath).then(() => { setTimeout(() => app.quit(), 1000); }); } } } module.exports = Updater;