feat: verify updater package and harden client reliability in 0.1.25
This commit is contained in:
@@ -112,11 +112,15 @@ const DOWNLOAD_SIGNED_URL_EXPIRES_SECONDS = Math.max(
|
||||
10,
|
||||
Math.min(3600, Number(process.env.DOWNLOAD_SIGNED_URL_EXPIRES_SECONDS || 30))
|
||||
);
|
||||
const DEFAULT_DESKTOP_VERSION = process.env.DESKTOP_LATEST_VERSION || '0.1.24';
|
||||
const DEFAULT_DESKTOP_VERSION = process.env.DESKTOP_LATEST_VERSION || '0.1.25';
|
||||
const DEFAULT_DESKTOP_INSTALLER_URL = process.env.DESKTOP_INSTALLER_URL || '';
|
||||
const DEFAULT_DESKTOP_INSTALLER_SHA256 = String(process.env.DESKTOP_INSTALLER_SHA256 || '').trim().toLowerCase();
|
||||
const DEFAULT_DESKTOP_INSTALLER_SIZE = Math.max(0, Number(process.env.DESKTOP_INSTALLER_SIZE || 0));
|
||||
const DEFAULT_DESKTOP_RELEASE_NOTES = process.env.DESKTOP_RELEASE_NOTES || '';
|
||||
const FRONTEND_ROOT_DIR = path.resolve(__dirname, '../frontend');
|
||||
const DESKTOP_INSTALLERS_DIR = path.resolve(__dirname, '../frontend/downloads');
|
||||
const DESKTOP_INSTALLER_FILE_PATTERN = /^(wanwan-cloud-desktop|玩玩云)_.*_x64-setup\.exe$/i;
|
||||
const DESKTOP_INSTALLER_SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
||||
const RESUMABLE_UPLOAD_SESSION_TTL_MS = Number(process.env.RESUMABLE_UPLOAD_SESSION_TTL_MS || (24 * 60 * 60 * 1000)); // 24小时
|
||||
const RESUMABLE_UPLOAD_CHUNK_SIZE_BYTES = Number(process.env.RESUMABLE_UPLOAD_CHUNK_SIZE_BYTES || (4 * 1024 * 1024)); // 4MB
|
||||
const RESUMABLE_UPLOAD_MAX_CHUNK_SIZE_BYTES = Number(process.env.RESUMABLE_UPLOAD_MAX_CHUNK_SIZE_BYTES || (32 * 1024 * 1024)); // 32MB
|
||||
@@ -180,6 +184,72 @@ function normalizeReleaseNotes(rawValue) {
|
||||
.trim();
|
||||
}
|
||||
|
||||
function normalizeSha256(rawValue) {
|
||||
const digest = String(rawValue || '').trim().toLowerCase();
|
||||
return DESKTOP_INSTALLER_SHA256_PATTERN.test(digest) ? digest : '';
|
||||
}
|
||||
|
||||
function normalizeNonNegativeInteger(rawValue, fallback = 0) {
|
||||
const value = Number(rawValue);
|
||||
if (!Number.isFinite(value) || value < 0) return Math.max(0, Number(fallback) || 0);
|
||||
return Math.floor(value);
|
||||
}
|
||||
|
||||
function isPathInside(parent, child) {
|
||||
const rel = path.relative(parent, child);
|
||||
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
||||
}
|
||||
|
||||
function resolveDesktopInstallerLocalPath(installerUrl) {
|
||||
const raw = String(installerUrl || '').trim();
|
||||
if (!raw) return null;
|
||||
|
||||
try {
|
||||
const parsed = new URL(raw, 'http://local.invalid');
|
||||
const pathname = decodeURIComponent(parsed.pathname || '');
|
||||
const normalizedPath = pathname.replace(/^\/+/, '');
|
||||
if (!normalizedPath) return null;
|
||||
|
||||
const preferredPath = path.resolve(FRONTEND_ROOT_DIR, normalizedPath);
|
||||
if (isPathInside(FRONTEND_ROOT_DIR, preferredPath) && fs.existsSync(preferredPath) && fs.statSync(preferredPath).isFile()) {
|
||||
return preferredPath;
|
||||
}
|
||||
|
||||
const fallbackPath = path.resolve(FRONTEND_ROOT_DIR, path.basename(normalizedPath));
|
||||
if (isPathInside(FRONTEND_ROOT_DIR, fallbackPath) && fs.existsSync(fallbackPath) && fs.statSync(fallbackPath).isFile()) {
|
||||
return fallbackPath;
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore malformed installer url
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function computeFileSha256HexSync(filePath) {
|
||||
const hash = crypto.createHash('sha256');
|
||||
const content = fs.readFileSync(filePath);
|
||||
hash.update(content);
|
||||
return hash.digest('hex');
|
||||
}
|
||||
|
||||
function getLocalDesktopInstallerMeta(installerUrl) {
|
||||
const localPath = resolveDesktopInstallerLocalPath(installerUrl);
|
||||
if (!localPath) return null;
|
||||
|
||||
try {
|
||||
const stats = fs.statSync(localPath);
|
||||
if (!stats.isFile() || stats.size <= 0) return null;
|
||||
return {
|
||||
path: localPath,
|
||||
size: stats.size,
|
||||
sha256: normalizeSha256(computeFileSha256HexSync(localPath))
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('[桌面端更新] 读取本地安装包元数据失败:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getDesktopUpdateConfig() {
|
||||
const latestVersion = normalizeVersion(
|
||||
SettingsDB.get('desktop_latest_version') || DEFAULT_DESKTOP_VERSION,
|
||||
@@ -197,9 +267,30 @@ function getDesktopUpdateConfig() {
|
||||
''
|
||||
);
|
||||
const mandatory = SettingsDB.get('desktop_force_update') === 'true';
|
||||
let installerSha256 = normalizeSha256(
|
||||
SettingsDB.get('desktop_installer_sha256') ||
|
||||
DEFAULT_DESKTOP_INSTALLER_SHA256
|
||||
);
|
||||
let packageSize = normalizeNonNegativeInteger(
|
||||
SettingsDB.get('desktop_installer_size'),
|
||||
DEFAULT_DESKTOP_INSTALLER_SIZE
|
||||
);
|
||||
if (installerUrl && (!installerSha256 || packageSize <= 0)) {
|
||||
const localMeta = getLocalDesktopInstallerMeta(installerUrl);
|
||||
if (localMeta) {
|
||||
if (!installerSha256 && localMeta.sha256) {
|
||||
installerSha256 = localMeta.sha256;
|
||||
}
|
||||
if (packageSize <= 0 && localMeta.size > 0) {
|
||||
packageSize = localMeta.size;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
latestVersion,
|
||||
installerUrl,
|
||||
installerSha256,
|
||||
packageSize,
|
||||
releaseNotes,
|
||||
mandatory
|
||||
};
|
||||
@@ -3673,6 +3764,8 @@ app.get('/api/client/desktop-update', (req, res) => {
|
||||
latestVersion: config.latestVersion,
|
||||
updateAvailable,
|
||||
downloadUrl: config.installerUrl,
|
||||
sha256: config.installerSha256,
|
||||
packageSize: config.packageSize,
|
||||
releaseNotes: config.releaseNotes,
|
||||
mandatory: config.mandatory && updateAvailable,
|
||||
platform,
|
||||
@@ -6239,13 +6332,13 @@ app.post('/api/upload/resumable/chunk', authMiddleware, upload.single('chunk'),
|
||||
});
|
||||
}
|
||||
|
||||
const chunkBuffer = fs.readFileSync(req.file.path);
|
||||
const chunkBuffer = await fs.promises.readFile(req.file.path);
|
||||
const offset = chunkIndex * chunkSize;
|
||||
const fd = fs.openSync(session.temp_file_path, 'r+');
|
||||
const fd = await fs.promises.open(session.temp_file_path, 'r+');
|
||||
try {
|
||||
fs.writeSync(fd, chunkBuffer, 0, chunkBuffer.length, offset);
|
||||
await fd.write(chunkBuffer, 0, chunkBuffer.length, offset);
|
||||
} finally {
|
||||
fs.closeSync(fd);
|
||||
await fd.close();
|
||||
}
|
||||
|
||||
const uploadedChunks = UploadSessionDB.parseUploadedChunks(session.uploaded_chunks);
|
||||
@@ -9060,6 +9153,8 @@ app.get('/api/admin/settings', authMiddleware, adminMiddleware, (req, res) => {
|
||||
desktop_update: {
|
||||
latest_version: desktopUpdate.latestVersion,
|
||||
installer_url: desktopUpdate.installerUrl,
|
||||
installer_sha256: desktopUpdate.installerSha256,
|
||||
installer_size: desktopUpdate.packageSize,
|
||||
release_notes: desktopUpdate.releaseNotes,
|
||||
force_update: desktopUpdate.mandatory
|
||||
},
|
||||
@@ -9166,8 +9261,41 @@ app.post('/api/admin/settings',
|
||||
const normalizedInstallerUrl = String(desktop_update.installer_url || '').trim();
|
||||
SettingsDB.set('desktop_installer_url', normalizedInstallerUrl);
|
||||
SettingsDB.set('desktop_installer_url_win_x64', normalizedInstallerUrl);
|
||||
if (!normalizedInstallerUrl) {
|
||||
SettingsDB.set('desktop_installer_sha256', '');
|
||||
SettingsDB.set('desktop_installer_size', '0');
|
||||
} else {
|
||||
const localMeta = getLocalDesktopInstallerMeta(normalizedInstallerUrl);
|
||||
if (localMeta?.sha256) {
|
||||
SettingsDB.set('desktop_installer_sha256', localMeta.sha256);
|
||||
}
|
||||
if (localMeta?.size) {
|
||||
SettingsDB.set('desktop_installer_size', String(localMeta.size));
|
||||
}
|
||||
}
|
||||
desktopInstallerCleanup = cleanupDesktopInstallerPackages(normalizedInstallerUrl);
|
||||
}
|
||||
if (desktop_update.installer_sha256 !== undefined) {
|
||||
const rawDigest = String(desktop_update.installer_sha256 || '').trim().toLowerCase();
|
||||
const normalizedDigest = normalizeSha256(rawDigest);
|
||||
if (rawDigest && !normalizedDigest) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '安装包 SHA256 格式无效'
|
||||
});
|
||||
}
|
||||
SettingsDB.set('desktop_installer_sha256', normalizedDigest);
|
||||
}
|
||||
if (desktop_update.installer_size !== undefined) {
|
||||
const rawSize = Number(desktop_update.installer_size);
|
||||
if (!Number.isFinite(rawSize) || rawSize < 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '安装包大小格式无效'
|
||||
});
|
||||
}
|
||||
SettingsDB.set('desktop_installer_size', String(normalizeNonNegativeInteger(rawSize, 0)));
|
||||
}
|
||||
if (desktop_update.release_notes !== undefined) {
|
||||
SettingsDB.set('desktop_release_notes', String(desktop_update.release_notes || '').trim());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user