keeweb/app/scripts/comp/app/updater.js

307 lines
12 KiB
JavaScript
Raw Normal View History

2021-05-08 11:38:23 +02:00
import * as kdbxweb from 'kdbxweb';
2019-09-16 22:57:56 +02:00
import { Events } from 'framework/events';
2019-10-26 22:56:36 +02:00
import { RuntimeInfo } from 'const/runtime-info';
2019-09-15 14:16:32 +02:00
import { Transport } from 'comp/browser/transport';
import { Launcher } from 'comp/launcher';
import { Links } from 'const/links';
import { AppSettingsModel } from 'models/app-settings-model';
import { UpdateModel } from 'models/update-model';
import { SemVer } from 'util/data/semver';
import { Logger } from 'util/logger';
2019-09-28 14:17:55 +02:00
import { SignatureVerifier } from 'util/data/signature-verifier';
2015-12-12 09:53:50 +01:00
2017-01-31 07:50:28 +01:00
const logger = new Logger('updater');
2015-10-25 20:26:33 +01:00
2017-01-31 07:50:28 +01:00
const Updater = {
2016-07-17 13:30:38 +02:00
UpdateInterval: 1000 * 60 * 60 * 24,
2015-11-14 12:09:36 +01:00
MinUpdateTimeout: 500,
2015-11-13 20:56:22 +01:00
MinUpdateSize: 10000,
2015-10-29 22:20:01 +01:00
nextCheckTimeout: null,
2015-11-13 20:56:22 +01:00
updateCheckDate: new Date(0),
2021-01-09 18:48:48 +01:00
enabled: Launcher?.updaterEnabled(),
2015-11-14 16:28:36 +01:00
2019-08-18 10:17:09 +02:00
getAutoUpdateType() {
if (!this.enabled) {
return false;
}
2019-09-17 19:50:42 +02:00
let autoUpdate = AppSettingsModel.autoUpdate;
if (autoUpdate && autoUpdate === true) {
autoUpdate = 'install';
}
return autoUpdate;
2015-10-29 22:20:01 +01:00
},
2015-11-14 16:28:36 +01:00
2019-08-18 10:17:09 +02:00
updateInProgress() {
2019-08-16 23:05:39 +02:00
return (
2019-09-17 21:56:58 +02:00
UpdateModel.status === 'checking' ||
2021-01-08 22:54:45 +01:00
['downloading', 'extracting', 'updating'].indexOf(UpdateModel.updateStatus) >= 0
2019-08-16 23:05:39 +02:00
);
2015-11-14 12:09:36 +01:00
},
2015-11-14 16:28:36 +01:00
2019-08-18 10:17:09 +02:00
init() {
2016-04-23 21:53:48 +02:00
this.scheduleNextCheck();
if (!Launcher && navigator.serviceWorker && !RuntimeInfo.beta && !RuntimeInfo.devMode) {
navigator.serviceWorker
.register('service-worker.js')
2020-06-01 16:53:51 +02:00
.then((reg) => {
logger.info('Service worker registered');
reg.addEventListener('updatefound', () => {
if (reg.active) {
logger.info('Service worker found an update');
UpdateModel.set({ updateStatus: 'ready' });
}
});
})
2020-06-01 16:53:51 +02:00
.catch((e) => {
logger.error('Failed to register a service worker', e);
});
2015-11-14 16:28:36 +01:00
}
2015-10-29 22:20:01 +01:00
},
2015-11-14 16:28:36 +01:00
2019-08-18 10:17:09 +02:00
scheduleNextCheck() {
2015-10-29 22:20:01 +01:00
if (this.nextCheckTimeout) {
clearTimeout(this.nextCheckTimeout);
this.nextCheckTimeout = null;
}
if (!this.getAutoUpdateType()) {
2015-10-29 22:20:01 +01:00
return;
}
2017-01-31 07:50:28 +01:00
let timeDiff = this.MinUpdateTimeout;
2019-09-17 21:56:58 +02:00
const lastCheckDate = UpdateModel.lastCheckDate;
2015-10-29 22:20:01 +01:00
if (lastCheckDate) {
2019-08-16 23:05:39 +02:00
timeDiff = Math.min(
Math.max(this.UpdateInterval + (lastCheckDate - new Date()), this.MinUpdateTimeout),
this.UpdateInterval
);
2015-10-29 22:20:01 +01:00
}
this.nextCheckTimeout = setTimeout(this.check.bind(this), timeDiff);
2015-12-12 09:53:50 +01:00
logger.info('Next update check will happen in ' + Math.round(timeDiff / 1000) + 's');
2015-10-29 22:20:01 +01:00
},
2015-11-14 16:28:36 +01:00
2019-08-18 10:17:09 +02:00
check(startedByUser) {
if (!this.enabled || this.updateInProgress()) {
2015-11-14 16:28:36 +01:00
return;
}
2019-09-17 21:56:58 +02:00
UpdateModel.set({ status: 'checking' });
2015-11-13 20:56:22 +01:00
if (!startedByUser) {
// additional protection from broken program logic, to ensure that auto-checks are not performed more than once an hour
2017-01-31 07:50:28 +01:00
const diffMs = new Date() - this.updateCheckDate;
2015-11-13 20:56:22 +01:00
if (isNaN(diffMs) || diffMs < 1000 * 60 * 60) {
2019-08-18 08:05:38 +02:00
logger.error(
'Prevented update check; last check was performed at ' + this.updateCheckDate
);
2016-07-17 13:30:38 +02:00
this.scheduleNextCheck();
2015-11-13 20:56:22 +01:00
return;
}
this.updateCheckDate = new Date();
}
2015-12-12 09:53:50 +01:00
logger.info('Checking for update...');
2015-11-13 20:56:22 +01:00
Transport.httpGet({
2021-01-07 23:00:16 +01:00
url: Links.UpdateJson,
json: true,
success: (updateJson) => {
2017-01-31 07:50:28 +01:00
const dt = new Date();
2021-01-07 23:00:16 +01:00
logger.info('Update check: ' + (updateJson.version || 'unknown'));
if (!updateJson.version) {
2017-01-31 07:50:28 +01:00
const errMsg = 'No version info found';
2019-09-17 21:56:58 +02:00
UpdateModel.set({
2019-08-18 08:05:38 +02:00
status: 'error',
lastCheckDate: dt,
lastCheckError: errMsg
});
2019-09-17 21:56:58 +02:00
UpdateModel.save();
2016-07-17 13:30:38 +02:00
this.scheduleNextCheck();
2015-10-25 20:26:33 +01:00
return;
}
2019-09-17 21:56:58 +02:00
const prevLastVersion = UpdateModel.lastVersion;
UpdateModel.set({
2015-11-13 20:56:22 +01:00
status: 'ok',
lastCheckDate: dt,
lastSuccessCheckDate: dt,
2021-01-07 23:00:16 +01:00
lastVersionReleaseDate: new Date(updateJson.date),
lastVersion: updateJson.version,
2016-01-17 21:34:40 +01:00
lastCheckError: null,
2021-01-07 23:00:16 +01:00
lastCheckUpdMin: updateJson.minVersion || null
2015-11-13 20:56:22 +01:00
});
2019-09-17 21:56:58 +02:00
UpdateModel.save();
2016-07-17 13:30:38 +02:00
this.scheduleNextCheck();
if (!this.canAutoUpdate()) {
2016-02-03 21:44:18 +01:00
return;
}
2019-08-16 23:05:39 +02:00
if (
2019-09-17 21:56:58 +02:00
prevLastVersion === UpdateModel.lastVersion &&
UpdateModel.updateStatus === 'ready'
2019-08-16 23:05:39 +02:00
) {
2015-12-12 09:53:50 +01:00
logger.info('Waiting for the user to apply downloaded update');
2015-11-14 12:09:36 +01:00
return;
}
2016-07-17 13:30:38 +02:00
if (!startedByUser && this.getAutoUpdateType() === 'install') {
this.update(startedByUser);
2019-08-18 08:05:38 +02:00
} else if (
2019-09-17 21:56:58 +02:00
SemVer.compareVersions(UpdateModel.lastVersion, RuntimeInfo.version) > 0
2019-08-18 08:05:38 +02:00
) {
2019-09-17 21:56:58 +02:00
UpdateModel.set({ updateStatus: 'found' });
}
2015-10-29 22:20:01 +01:00
},
2020-06-01 16:53:51 +02:00
error: (e) => {
2015-12-12 09:53:50 +01:00
logger.error('Update check error', e);
2019-09-17 21:56:58 +02:00
UpdateModel.set({
2015-11-13 20:56:22 +01:00
status: 'error',
lastCheckDate: new Date(),
lastCheckError: 'Error checking last version'
});
2019-09-17 21:56:58 +02:00
UpdateModel.save();
2016-07-17 13:30:38 +02:00
this.scheduleNextCheck();
2015-10-29 22:20:01 +01:00
}
2015-10-25 20:26:33 +01:00
});
2015-10-29 22:20:01 +01:00
},
2015-11-14 16:28:36 +01:00
2019-08-18 10:17:09 +02:00
canAutoUpdate() {
2019-09-17 21:56:58 +02:00
const minLauncherVersion = UpdateModel.lastCheckUpdMin;
2016-02-03 21:44:18 +01:00
if (minLauncherVersion) {
const cmp = SemVer.compareVersions(RuntimeInfo.version, minLauncherVersion);
2016-02-07 12:49:31 +01:00
if (cmp < 0) {
2019-09-17 21:56:58 +02:00
UpdateModel.set({ updateStatus: 'ready', updateManual: true });
2016-02-07 12:49:31 +01:00
return false;
2016-02-03 21:44:18 +01:00
}
}
return true;
2015-11-14 16:28:36 +01:00
},
2019-08-18 10:17:09 +02:00
update(startedByUser, successCallback) {
2019-09-17 21:56:58 +02:00
const ver = UpdateModel.lastVersion;
if (!this.enabled) {
logger.info('Updater is disabled');
return;
}
if (SemVer.compareVersions(RuntimeInfo.version, ver) >= 0) {
2015-12-12 09:53:50 +01:00
logger.info('You are using the latest version');
2015-10-29 22:20:01 +01:00
return;
}
2019-09-17 21:56:58 +02:00
UpdateModel.set({ updateStatus: 'downloading', updateError: null });
2015-12-12 09:53:50 +01:00
logger.info('Downloading update', ver);
2021-01-08 22:54:45 +01:00
const updateAssetName = this.getUpdateAssetName(ver);
if (!updateAssetName) {
logger.error('Empty updater asset name for', Launcher.platform(), Launcher.arch());
return;
}
const updateUrlBasePath = Links.UpdateBasePath.replace('{ver}', ver);
const updateAssetUrl = updateUrlBasePath + updateAssetName;
2015-11-13 20:56:22 +01:00
Transport.httpGet({
2021-01-08 22:54:45 +01:00
url: updateAssetUrl,
file: updateAssetName,
cleanupOldFiles: true,
2021-01-09 13:21:37 +01:00
cache: true,
2021-01-08 22:54:45 +01:00
success: (assetFilePath) => {
logger.info('Downloading update signatures');
Transport.httpGet({
url: updateUrlBasePath + 'Verify.sign.sha256',
text: true,
file: updateAssetName + '.sign',
cleanupOldFiles: true,
2021-01-09 13:21:37 +01:00
cache: true,
2021-01-08 22:54:45 +01:00
success: (assetFileSignaturePath) => {
this.verifySignature(assetFilePath, updateAssetName, (err, valid) => {
2021-01-09 13:21:37 +01:00
if (err || !valid) {
2021-01-08 22:54:45 +01:00
UpdateModel.set({
updateStatus: 'error',
2021-01-09 13:21:37 +01:00
updateError: err
? 'Error verifying update signature'
: 'Invalid update signature'
2021-01-08 22:54:45 +01:00
});
Launcher.deleteFile(assetFilePath);
Launcher.deleteFile(assetFileSignaturePath);
return;
}
logger.info('Update is ready', assetFilePath);
UpdateModel.set({ updateStatus: 'ready', updateError: null });
if (!startedByUser) {
Events.emit('update-app');
}
if (typeof successCallback === 'function') {
successCallback();
}
});
},
error(e) {
logger.error('Error downloading update signatures', e);
2019-09-17 21:56:58 +02:00
UpdateModel.set({
2019-08-18 08:05:38 +02:00
updateStatus: 'error',
2021-01-08 22:54:45 +01:00
updateError: 'Error downloading update signatures'
2019-08-18 08:05:38 +02:00
});
2015-11-13 20:56:22 +01:00
}
});
},
2019-08-18 10:17:09 +02:00
error(e) {
2015-12-12 09:53:50 +01:00
logger.error('Error downloading update', e);
2019-09-17 21:56:58 +02:00
UpdateModel.set({
2019-08-18 08:05:38 +02:00
updateStatus: 'error',
updateError: 'Error downloading update'
});
2015-10-29 22:20:01 +01:00
}
2015-11-13 20:56:22 +01:00
});
},
2015-10-29 22:20:01 +01:00
2021-01-08 22:54:45 +01:00
verifySignature(assetFilePath, assetName, callback) {
logger.info('Verifying update signature', assetName);
const fs = Launcher.req('fs');
const signaturesTxt = fs.readFileSync(assetFilePath + '.sign', 'utf8');
const assetSignatureLine = signaturesTxt
.split('\n')
.find((line) => line.endsWith(assetName));
if (!assetSignatureLine) {
logger.error('Signature not found for asset', assetName);
callback('Asset signature not found');
return;
}
const signature = kdbxweb.ByteUtils.hexToBytes(assetSignatureLine.split(' ')[0]);
const fileBytes = fs.readFileSync(assetFilePath);
SignatureVerifier.verify(fileBytes, signature)
.catch((e) => {
logger.error('Error verifying signature', e);
callback('Error verifying signature');
})
.then((valid) => {
logger.info(`Update asset signature is ${valid ? 'valid' : 'invalid'}`);
callback(undefined, valid);
2015-11-13 20:56:22 +01:00
});
2015-11-14 16:28:36 +01:00
},
2021-01-08 22:54:45 +01:00
getUpdateAssetName(ver) {
const platform = Launcher.platform();
const arch = Launcher.arch();
switch (platform) {
case 'win32':
switch (arch) {
case 'x64':
return `KeeWeb-${ver}.win.x64.exe`;
case 'ia32':
return `KeeWeb-${ver}.win.ia32.exe`;
case 'arm64':
return `KeeWeb-${ver}.win.arm64.exe`;
}
break;
case 'darwin':
switch (arch) {
case 'x64':
return `KeeWeb-${ver}.mac.x64.dmg`;
case 'arm64':
return `KeeWeb-${ver}.mac.arm64.dmg`;
}
break;
2016-03-05 12:16:12 +01:00
}
2021-01-08 22:54:45 +01:00
return undefined;
},
installAndRestart() {
if (!Launcher) {
return;
2016-03-05 12:16:12 +01:00
}
2021-01-08 22:54:45 +01:00
const updateAssetName = this.getUpdateAssetName(UpdateModel.lastVersion);
const updateFilePath = Transport.cacheFilePath(updateAssetName);
Launcher.requestRestartAndUpdate(updateFilePath);
2015-10-25 20:26:33 +01:00
}
};
2019-09-15 14:16:32 +02:00
export { Updater };