mirror of
https://gitlab.com/vyla-entertainment/home.git
synced 2026-11-09 19:57:20 -05:00
271 lines
7.8 KiB
JavaScript
271 lines
7.8 KiB
JavaScript
const https = require('https');
|
|
const fs = require('fs');
|
|
|
|
const GITLAB_GROUP = 'vyla-entertainment';
|
|
const GITLAB_REPO = 'home';
|
|
const RELEASES_API = `https://gitlab.com/api/v4/projects/${GITLAB_GROUP}%2F${GITLAB_REPO}/releases`;
|
|
|
|
const ALLOWED_REDIRECT_HOSTS = [
|
|
'gitlab.com',
|
|
'storage.googleapis.com'
|
|
];
|
|
|
|
const httpsAgent = new https.Agent({ keepAlive: true, maxSockets: 32 });
|
|
|
|
function isAllowedHost(url) {
|
|
try {
|
|
const hostname = new URL(url).hostname;
|
|
return ALLOWED_REDIRECT_HOSTS.includes(hostname) ||
|
|
hostname.endsWith('.gitlab.com') ||
|
|
hostname.endsWith('.storage.googleapis.com') ||
|
|
hostname.endsWith('.amazonaws.com') ||
|
|
hostname.endsWith('.cloudfront.net');
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
class UpdateClient {
|
|
constructor(appDataPath, token = null) {
|
|
this.appDataPath = appDataPath;
|
|
this.token = token || process.env.GITLAB_TOKEN || process.env.VYLA_GITLAB_TOKEN || null;
|
|
}
|
|
|
|
fetchJson(url) {
|
|
return new Promise((resolve, reject) => {
|
|
const urlObj = new URL(url);
|
|
const headers = {
|
|
'User-Agent': 'VylaHome-Client',
|
|
'Accept': 'application/json'
|
|
};
|
|
|
|
if (this.token) {
|
|
headers['PRIVATE-TOKEN'] = this.token;
|
|
}
|
|
|
|
const requestOptions = {
|
|
hostname: urlObj.hostname,
|
|
port: 443,
|
|
path: urlObj.pathname + urlObj.search,
|
|
method: 'GET',
|
|
headers,
|
|
agent: httpsAgent
|
|
};
|
|
|
|
const req = https.request(requestOptions, (res) => {
|
|
let data = '';
|
|
res.on('data', (chunk) => { data += chunk; });
|
|
res.on('end', () => {
|
|
if (res.statusCode !== 200) {
|
|
reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`));
|
|
return;
|
|
}
|
|
try {
|
|
resolve(JSON.parse(data));
|
|
} catch (e) {
|
|
reject(new Error('Invalid JSON response'));
|
|
}
|
|
});
|
|
});
|
|
|
|
req.on('error', reject);
|
|
req.setTimeout(30000, () => {
|
|
req.destroy();
|
|
reject(new Error('Request timeout'));
|
|
});
|
|
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
downloadFile(url, destPath, onProgress) {
|
|
return new Promise((resolve, reject) => {
|
|
const doRequest = (requestUrl, redirectCount) => {
|
|
if (redirectCount > 5) {
|
|
reject(new Error('Too many redirects'));
|
|
return;
|
|
}
|
|
|
|
const urlObj = new URL(requestUrl);
|
|
const headers = {
|
|
'User-Agent': 'VylaHome-Client',
|
|
'Accept': 'application/octet-stream'
|
|
};
|
|
|
|
const isGitLabHost = urlObj.hostname === 'gitlab.com' || urlObj.hostname.endsWith('.gitlab.com');
|
|
if (this.token && isGitLabHost) {
|
|
headers['PRIVATE-TOKEN'] = this.token;
|
|
}
|
|
|
|
const requestOptions = {
|
|
hostname: urlObj.hostname,
|
|
port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
|
|
path: urlObj.pathname + urlObj.search,
|
|
headers,
|
|
agent: httpsAgent
|
|
};
|
|
|
|
const req = https.get(requestOptions, (res) => {
|
|
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
res.resume();
|
|
const redirectUrl = new URL(res.headers.location, requestUrl).toString();
|
|
if (!isAllowedHost(redirectUrl)) {
|
|
reject(new Error(`Redirect to disallowed host: ${redirectUrl}`));
|
|
return;
|
|
}
|
|
doRequest(redirectUrl, redirectCount + 1);
|
|
return;
|
|
}
|
|
|
|
if (res.statusCode !== 200) {
|
|
reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`));
|
|
return;
|
|
}
|
|
|
|
const totalSize = parseInt(res.headers['content-length'], 10);
|
|
let downloadedSize = 0;
|
|
|
|
const file = fs.createWriteStream(destPath);
|
|
res.pipe(file);
|
|
|
|
res.on('data', (chunk) => {
|
|
downloadedSize += chunk.length;
|
|
if (onProgress && totalSize) {
|
|
onProgress(downloadedSize, totalSize);
|
|
}
|
|
});
|
|
|
|
file.on('finish', () => {
|
|
file.close(() => {
|
|
if (totalSize && downloadedSize < totalSize) {
|
|
fs.unlink(destPath, () => { });
|
|
reject(new Error(`Download truncated: Received ${downloadedSize} of ${totalSize} bytes. Connection dropped prematurely.`));
|
|
} else {
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
|
|
file.on('error', (err) => {
|
|
fs.unlink(destPath, () => { });
|
|
reject(err);
|
|
});
|
|
});
|
|
|
|
req.on('error', (err) => {
|
|
fs.unlink(destPath, () => { });
|
|
reject(err);
|
|
});
|
|
|
|
req.setTimeout(900000, () => {
|
|
req.destroy();
|
|
fs.unlink(destPath, () => { });
|
|
reject(new Error('Download timeout (15 minutes reached)'));
|
|
});
|
|
};
|
|
|
|
doRequest(url, 0);
|
|
});
|
|
}
|
|
|
|
async fetchAssetJson(url) {
|
|
return new Promise((resolve, reject) => {
|
|
const doRequest = (requestUrl, redirectCount) => {
|
|
if (redirectCount > 5) {
|
|
reject(new Error('Too many redirects'));
|
|
return;
|
|
}
|
|
|
|
const urlObj = new URL(requestUrl);
|
|
const headers = {
|
|
'User-Agent': 'VylaHome-Client',
|
|
'Accept': 'application/octet-stream'
|
|
};
|
|
|
|
const isGitLabHost = urlObj.hostname === 'gitlab.com' || urlObj.hostname.endsWith('.gitlab.com');
|
|
if (this.token && isGitLabHost) {
|
|
headers['PRIVATE-TOKEN'] = this.token;
|
|
}
|
|
|
|
const requestOptions = {
|
|
hostname: urlObj.hostname,
|
|
port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
|
|
path: urlObj.pathname + urlObj.search,
|
|
headers,
|
|
agent: httpsAgent
|
|
};
|
|
|
|
const req = https.get(requestOptions, (res) => {
|
|
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
res.resume();
|
|
const redirectUrl = new URL(res.headers.location, requestUrl).toString();
|
|
if (!isAllowedHost(redirectUrl)) {
|
|
reject(new Error(`Redirect to disallowed host: ${redirectUrl}`));
|
|
return;
|
|
}
|
|
doRequest(redirectUrl, redirectCount + 1);
|
|
return;
|
|
}
|
|
|
|
if (res.statusCode !== 200) {
|
|
reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`));
|
|
return;
|
|
}
|
|
|
|
let data = '';
|
|
res.on('data', (chunk) => { data += chunk; });
|
|
res.on('end', () => {
|
|
try {
|
|
resolve(JSON.parse(data));
|
|
} catch (e) {
|
|
reject(new Error('Invalid JSON in manifest asset'));
|
|
}
|
|
});
|
|
});
|
|
|
|
req.on('error', reject);
|
|
req.setTimeout(30000, () => {
|
|
req.destroy();
|
|
reject(new Error('Request timeout'));
|
|
});
|
|
};
|
|
|
|
doRequest(url, 0);
|
|
});
|
|
}
|
|
|
|
async checkForUpdates() {
|
|
const cacheBuster = `?cb=${Date.now()}`;
|
|
const releases = await this.fetchJson(RELEASES_API + cacheBuster);
|
|
if (!releases || releases.length === 0) {
|
|
return null;
|
|
}
|
|
const release = releases[0];
|
|
|
|
const payloadAsset = (release.assets && release.assets.links ? release.assets.links : []).find(a => a.name === 'update-payload.zip');
|
|
const manifestAsset = (release.assets && release.assets.links ? release.assets.links : []).find(a => a.name === 'manifest.json');
|
|
|
|
if (!payloadAsset) {
|
|
return null;
|
|
}
|
|
|
|
let sha256 = null;
|
|
if (manifestAsset) {
|
|
const manifest = await this.fetchAssetJson(manifestAsset.url);
|
|
sha256 = manifest.sha256 || null;
|
|
}
|
|
|
|
return {
|
|
version: release.tag_name,
|
|
url: payloadAsset.url,
|
|
sha256
|
|
};
|
|
}
|
|
|
|
async downloadUpdate(release, destPath, onProgress) {
|
|
await this.downloadFile(release.url, destPath, onProgress);
|
|
return destPath;
|
|
}
|
|
}
|
|
|
|
module.exports = UpdateClient; |