mirror of
https://gitlab.com/vyla-entertainment/home.git
synced 2026-11-09 19:57:20 -05:00
209 lines
6.3 KiB
JavaScript
209 lines
6.3 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
const crypto = require('crypto');
|
|
const AdmZip = require('adm-zip');
|
|
const CryptoUtils = require('./crypto-utils');
|
|
|
|
class Patcher {
|
|
constructor(appDataPath) {
|
|
this.appDataPath = appDataPath;
|
|
}
|
|
|
|
getCurrentVersion() {
|
|
const versionFile = path.join(this.appDataPath, 'version.json');
|
|
if (fs.existsSync(versionFile)) {
|
|
try {
|
|
return JSON.parse(fs.readFileSync(versionFile, 'utf8')).version || '0.0.0';
|
|
} catch (e) {
|
|
return '0.0.0';
|
|
}
|
|
}
|
|
return '0.0.0';
|
|
}
|
|
|
|
getCurrentSha() {
|
|
const versionFile = path.join(this.appDataPath, 'version.json');
|
|
if (fs.existsSync(versionFile)) {
|
|
try {
|
|
return JSON.parse(fs.readFileSync(versionFile, 'utf8')).sha256 || null;
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
bridgeCredentials(credentialManager) {
|
|
const encryptedPath = path.join(this.appDataPath, 'encrypted-credentials.json');
|
|
if (!fs.existsSync(encryptedPath)) {
|
|
return false;
|
|
}
|
|
|
|
const buildKey = CryptoUtils.deriveKey('vyla-home-build-key', 'vyla-home-payload-salt-v1');
|
|
const encryptedData = fs.readFileSync(encryptedPath, 'utf8');
|
|
const credentials = CryptoUtils.decryptObject(encryptedData, buildKey);
|
|
|
|
for (const [service, keys] of Object.entries(credentials)) {
|
|
credentialManager.setServiceCredentials(service, keys);
|
|
}
|
|
|
|
fs.unlinkSync(encryptedPath);
|
|
return true;
|
|
}
|
|
|
|
saveCurrentVersion(version, sha256 = null) {
|
|
const versionFile = path.join(this.appDataPath, 'version.json');
|
|
fs.mkdirSync(this.appDataPath, { recursive: true });
|
|
fs.writeFileSync(versionFile, JSON.stringify({ version, sha256 }, null, 2), 'utf8');
|
|
}
|
|
|
|
purgeOldCode() {
|
|
const dirs = ['frontend', 'player', 'stream-api', 'auth-proxy', 'config'];
|
|
for (const dir of dirs) {
|
|
const fullPath = path.join(this.appDataPath, dir);
|
|
if (fs.existsSync(fullPath)) {
|
|
try {
|
|
fs.rmSync(fullPath, { recursive: true, force: true });
|
|
} catch (e) { }
|
|
}
|
|
}
|
|
}
|
|
|
|
extractZipPayload(zipPath) {
|
|
const zip = new AdmZip(zipPath);
|
|
const entries = zip.getEntries();
|
|
|
|
if (!entries || entries.length === 0) {
|
|
throw new Error('Archive is empty, invalid, or truncated.');
|
|
}
|
|
|
|
const targetDir = path.resolve(this.appDataPath);
|
|
for (const entry of entries) {
|
|
const entryPath = path.normalize(entry.entryName);
|
|
const resolvedPath = path.resolve(targetDir, entryPath);
|
|
if (!resolvedPath.startsWith(targetDir)) {
|
|
throw new Error(`Directory traversal attempt detected in archive: ${entry.entryName}`);
|
|
}
|
|
}
|
|
|
|
zip.extractAllTo(this.appDataPath, true);
|
|
}
|
|
|
|
async extractToStaging(zipPath, onFile) {
|
|
const stagingDir = path.join(this.appDataPath, '.staging');
|
|
fs.rmSync(stagingDir, { recursive: true, force: true });
|
|
fs.mkdirSync(stagingDir, { recursive: true });
|
|
const zip = new AdmZip(zipPath);
|
|
const entries = zip.getEntries().filter((e) => !e.isDirectory);
|
|
if (entries.length === 0) {
|
|
throw new Error('Downloaded archive is empty, invalid, or truncated.');
|
|
}
|
|
|
|
const resolvedStaging = path.resolve(stagingDir);
|
|
for (const entry of entries) {
|
|
const entryPath = path.normalize(entry.entryName);
|
|
const resolvedPath = path.resolve(resolvedStaging, entryPath);
|
|
if (!resolvedPath.startsWith(resolvedStaging)) {
|
|
throw new Error(`Directory traversal attempt detected: ${entry.entryName}`);
|
|
}
|
|
}
|
|
|
|
for (let i = 0; i < entries.length; i++) {
|
|
const entry = entries[i];
|
|
zip.extractEntryTo(entry, stagingDir, true, true);
|
|
if (onFile) onFile(entry.entryName, i + 1, entries.length);
|
|
if (i % 20 === 0) await new Promise((resolve) => setImmediate(resolve));
|
|
}
|
|
return stagingDir;
|
|
}
|
|
|
|
commitStaging(stagingDir) {
|
|
this.purgeOldCode();
|
|
for (const name of fs.readdirSync(stagingDir)) {
|
|
const dest = path.join(this.appDataPath, name);
|
|
fs.rmSync(dest, { recursive: true, force: true });
|
|
fs.renameSync(path.join(stagingDir, name), dest);
|
|
}
|
|
fs.rmSync(stagingDir, { recursive: true, force: true });
|
|
}
|
|
|
|
verifyChecksum(zipPath, expectedSha256) {
|
|
if (!expectedSha256) return false;
|
|
const hash = crypto.createHash('sha256');
|
|
const data = fs.readFileSync(zipPath);
|
|
hash.update(data);
|
|
const calculated = hash.digest('hex');
|
|
return calculated === String(expectedSha256).trim().toLowerCase();
|
|
}
|
|
|
|
verifyExtraction(root = this.appDataPath) {
|
|
const requiredFiles = [
|
|
'frontend/server.js',
|
|
'player/server.js'
|
|
];
|
|
|
|
for (const file of requiredFiles) {
|
|
const fullPath = path.join(root, file);
|
|
if (!fs.existsSync(fullPath)) {
|
|
return false;
|
|
}
|
|
const stat = fs.statSync(fullPath);
|
|
if (stat.size === 0) {
|
|
return false;
|
|
}
|
|
try {
|
|
const sampleSize = Math.min(stat.size, 4096);
|
|
const fd = fs.openSync(fullPath, 'r');
|
|
const buffer = Buffer.alloc(sampleSize);
|
|
fs.readSync(fd, buffer, 0, sampleSize, 0);
|
|
fs.closeSync(fd);
|
|
if (buffer.every(byte => byte === 0)) {
|
|
return false;
|
|
}
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
processDirectory(dir, replacementList = []) {
|
|
const files = fs.readdirSync(dir);
|
|
for (const file of files) {
|
|
const fullPath = path.join(dir, file);
|
|
const stat = fs.statSync(fullPath);
|
|
|
|
if (stat.isDirectory()) {
|
|
if (file === 'node_modules' || file === '.git') continue;
|
|
this.processDirectory(fullPath, replacementList);
|
|
} else if (stat.isFile()) {
|
|
let content = fs.readFileSync(fullPath, 'utf8');
|
|
let modified = false;
|
|
|
|
for (const rep of replacementList) {
|
|
if (content.includes(rep.find)) {
|
|
content = content.split(rep.find).join(rep.replace);
|
|
modified = true;
|
|
}
|
|
}
|
|
|
|
if (modified) {
|
|
fs.writeFileSync(fullPath, content, 'utf8');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
applyPatches(replacements) {
|
|
const targetDirs = ['frontend', 'player'];
|
|
for (const dirName of targetDirs) {
|
|
const dirPath = path.join(this.appDataPath, dirName);
|
|
if (!fs.existsSync(dirPath)) continue;
|
|
const replacementList = replacements[dirName] || [];
|
|
this.processDirectory(dirPath, replacementList);
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = Patcher;
|