diff --git a/src/asarUpdate.js b/src/asarUpdate.js index 5bffc4420..620e846b9 100644 --- a/src/asarUpdate.js +++ b/src/asarUpdate.js @@ -1,4 +1,4 @@ -const { get } = require('https'); +const { get } = require('./utils/get'); const fs = require('original-fs'); // Use original-fs, not Electron's modified fs const { join } = require('path'); @@ -6,29 +6,13 @@ const asarPath = join(__filename, '..'); const asarUrl = `https://github.com/GooseMod/OpenAsar/releases/download/${oaVersion.split('-')[0]}/app.asar`; -// todo: have these https utils centralised? -const redirs = url => new Promise(res => get(url, r => { // Minimal wrapper around https.get to follow redirects - const loc = r.headers.location; - if (loc) return redirs(loc).then(res); - - res(r); -})); - module.exports = async () => { // (Try) update asar if (!oaVersion.includes('-')) return; log('AsarUpdate', 'Updating...'); - const res = (await redirs(asarUrl)); - - let data = []; - res.on('data', d => { - data.push(d); - }); + const buf = (await get(asarUrl))[1]; - res.on('end', () => { - const buf = Buffer.concat(data); - if (!buf.toString('hex').startsWith('04000000')) return log('AsarUpdate', 'Download error'); // Not like ASAR header + if (!buf || !buf.toString('hex').startsWith('04000000')) return log('AsarUpdate', 'Download error'); // Request failed or ASAR header not present - fs.writeFile(asarPath, buf, e => log('AsarUpdate', 'Downloaded', e ?? '')); - }); + fs.writeFile(asarPath, buf, e => log('AsarUpdate', 'Downloaded', e ?? '')); }; \ No newline at end of file diff --git a/src/updater/moduleUpdater.js b/src/updater/moduleUpdater.js index 0545d2756..9702ae33e 100644 --- a/src/updater/moduleUpdater.js +++ b/src/updater/moduleUpdater.js @@ -3,7 +3,7 @@ const fs = require('fs'); const Module = require('module'); const { execFile } = require('child_process'); const { app, autoUpdater } = require('electron'); -const { get } = require('https'); +const { get, request } = require('../utils/get'); const paths = require('../paths'); @@ -32,20 +32,6 @@ const resetTracking = () => { installing = Object.assign({}, base); }; -const req = url => new Promise(res => get(url, r => { // Minimal wrapper around https.get to include body - let dat = ''; - r.on('data', b => dat += b.toString()); - - r.on('end', () => res([ r, dat ])); -})); - -const redirs = url => new Promise(res => get(url, r => { // Minimal wrapper around https.get to follow redirects - const loc = r.headers.location; - if (loc) return redirs(loc).then(res); - - res(r); -})); - exports.init = (endpoint, { releaseChannel, version }) => { skipHost = settings.get('SKIP_HOST_UPDATE'); skipModule = settings.get('SKIP_MODULE_UPDATE'); @@ -77,10 +63,10 @@ exports.init = (endpoint, { releaseChannel, version }) => { } checkForUpdates() { - req(this.url).then(([ r, b ]) => { - if (r.statusCode === 204) return this.emit('update-not-available'); + get(this.url).then(([r, b]) => { + if (!b || r === 204) return this.emit('update-not-available'); - this.emit('update-manually', b); + this.emit('update-manually', b.toString()); }); } @@ -111,7 +97,12 @@ exports.init = (endpoint, { releaseChannel, version }) => { }; const checkModules = async () => { - remote = JSON.parse((await req(baseUrl + '/versions.json' + qs))[1]); + const buf = (await get(baseUrl + '/versions.json' + qs))[1]; + if (!buf) { + log('Modules', 'versions.json retrieval failure.'); + return; + } + remote = JSON.parse(buf.toString()); for (const name in installed) { const inst = installed[name].installedVersion; @@ -135,23 +126,31 @@ const downloadModule = async (name, ver) => { // log('Modules', 'Downloading', `${name}@${ver}`); - let success, total, cur = 0; - const res = await redirs(baseUrl + '/' + name + '/' + ver + qs); - success = res.statusCode === 200; - total = parseInt(res.headers['content-length'] ?? 1, 10); - - res.pipe(file); - - res.on('data', c => { - cur += c.length; - - events.emit('downloading-module', { name, cur, total }); - }); + let success, total, cur = 0; + request( + baseUrl + '/' + name + '/' + ver + qs, + res => { + success = (res.statusCode === 200); + // res.headers is a + // https://www.electronjs.org/docs/latest/api/incoming-message#responseheaders + total = parseInt(res.headers['content-length'][0] ?? 1, 10); + }, + chunk => { + cur += chunk.length; + events.emit('downloading-module', { name, cur, total }); + + file.write(chunk); + }, + () => { + file.close(); + } + ); - await new Promise((res) => file.on('close', res)); + // block till file.close() + await new Promise(res => file.on('close', res)); if (success) commitManifest(); - else downloading.fail++; + else downloading.fail++; events.emit('downloaded-module', { name diff --git a/src/utils/get.js b/src/utils/get.js new file mode 100644 index 000000000..126f4e8fe --- /dev/null +++ b/src/utils/get.js @@ -0,0 +1,35 @@ +const { net } = require('electron'); + +// returns a promise that resolves to [statusCode, Buffer, headers] +// [code, null, null] if request failed +module.exports.get = async url => { + const response = await net.fetch(new Request(url, { + method: 'GET', + redirect: 'follow' + })); + + if (response.ok) { + return [response.status, Buffer.from(await response.arrayBuffer()), response.headers]; + } else { + return [response.status, null, null]; + } +}; + +// issues a GET request following redirects, calling provided callbacks to return data and statues: +// - `response_cb` is called with a `IncomingMessage` (https://www.electronjs.org/docs/latest/api/incoming-message) on receiving an HTTP response +// - `data_cb` is called with a chunk for every data chunk arrived +// - `end_cb` is called when there are no more data +module.exports.request = (url, response_cb, data_cb, end_cb) => { + const request = net.request({ + url: url, + redirect: 'follow', + }); + + request.on('response', response => { + response_cb(response); + response.on('data', data_cb); + response.on('end', end_cb) + }); + + request.end(); +}; \ No newline at end of file