Upload via Git Manager GUI - updater.js
This commit is contained in:
181
updater.js
181
updater.js
@@ -3,9 +3,11 @@ 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) {
|
||||
@@ -31,15 +33,28 @@ class Updater {
|
||||
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: this.findAsset(latestRelease.assets)
|
||||
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);
|
||||
@@ -77,7 +92,117 @@ class Updater {
|
||||
findAsset(assets) {
|
||||
if (!assets) return null;
|
||||
const ext = process.platform === 'win32' ? '.exe' : '.AppImage';
|
||||
return assets.find(a => a.name.toLowerCase().endsWith(ext));
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,16 +214,40 @@ class Updater {
|
||||
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);
|
||||
return download(res.headers.location || '');
|
||||
}
|
||||
|
||||
if (res.statusCode !== 200) {
|
||||
@@ -108,9 +257,22 @@ class Updater {
|
||||
|
||||
res.pipe(file);
|
||||
|
||||
file.on('finish', () => {
|
||||
file.on('finish', async () => {
|
||||
file.close();
|
||||
console.log("[Updater] Download abgeschlossen. Initialisiere entkoppelten Installer...");
|
||||
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) => {
|
||||
@@ -129,15 +291,14 @@ class Updater {
|
||||
console.log(`[Updater] Bereite Installation vor: ${filePath}`);
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
// Wir nutzen spawn mit detached: true, damit der Installer weiterläuft,
|
||||
// wenn der Hauptprozess (Electron) beendet wird.
|
||||
try {
|
||||
const child = spawn('cmd.exe', ['/c', 'start', '""', filePath], {
|
||||
const child = spawn(filePath, [], {
|
||||
detached: true,
|
||||
stdio: 'ignore'
|
||||
stdio: 'ignore',
|
||||
shell: false
|
||||
});
|
||||
|
||||
child.unref(); // Trennt die Referenz zum Installer
|
||||
child.unref();
|
||||
|
||||
console.log("[Updater] Installer-Prozess entkoppelt gestartet. App schließt in 2 Sek...");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user