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,
|
2017-12-02 20:38:13 +01:00
|
|
|
UpdateCheckFiles: ['app.asar'],
|
2015-10-29 22:20:01 +01:00
|
|
|
nextCheckTimeout: null,
|
2015-11-13 20:56:22 +01:00
|
|
|
updateCheckDate: new Date(0),
|
2016-02-06 12:40:40 +01:00
|
|
|
enabled: Launcher && Launcher.updaterEnabled(),
|
2015-11-14 16:28:36 +01:00
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
getAutoUpdateType() {
|
2016-02-06 12:40:40 +01:00
|
|
|
if (!this.enabled) {
|
2015-11-16 20:04:33 +01:00
|
|
|
return false;
|
|
|
|
}
|
2019-09-17 19:50:42 +02:00
|
|
|
let autoUpdate = AppSettingsModel.autoUpdate;
|
2015-11-16 20:04:33 +01:00
|
|
|
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' ||
|
|
|
|
['downloading', 'extracting'].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();
|
2019-09-23 20:32:56 +02:00
|
|
|
if (!Launcher && navigator.serviceWorker && !RuntimeInfo.beta && !RuntimeInfo.devMode) {
|
|
|
|
navigator.serviceWorker
|
|
|
|
.register('service-worker.js')
|
2020-06-01 16:53:51 +02:00
|
|
|
.then((reg) => {
|
2019-09-23 20:32:56 +02:00
|
|
|
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) => {
|
2019-09-23 20:32:56 +02:00
|
|
|
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;
|
|
|
|
}
|
2015-11-16 20:04:33 +01:00
|
|
|
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) {
|
2016-02-06 12:40:40 +01:00
|
|
|
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-11-16 20:04:33 +01:00
|
|
|
}
|
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) {
|
2017-05-23 20:03:29 +02:00
|
|
|
const cmp = SemVer.compareVersions(Launcher.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;
|
2016-02-06 12:40:40 +01:00
|
|
|
if (!this.enabled) {
|
|
|
|
logger.info('Updater is disabled');
|
|
|
|
return;
|
|
|
|
}
|
2017-05-23 20:03:29 +02:00
|
|
|
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);
|
2015-11-13 20:56:22 +01:00
|
|
|
Transport.httpGet({
|
|
|
|
url: Links.UpdateDesktop.replace('{ver}', ver),
|
|
|
|
file: 'KeeWeb-' + ver + '.zip',
|
|
|
|
cache: !startedByUser,
|
2020-06-01 16:53:51 +02:00
|
|
|
success: (filePath) => {
|
2019-09-17 21:56:58 +02:00
|
|
|
UpdateModel.set({ updateStatus: 'extracting' });
|
2016-07-17 13:30:38 +02:00
|
|
|
logger.info('Extracting update file', this.UpdateCheckFiles, filePath);
|
2020-06-01 16:53:51 +02:00
|
|
|
this.extractAppUpdate(filePath, (err) => {
|
2015-11-13 20:56:22 +01:00
|
|
|
if (err) {
|
2015-12-12 09:53:50 +01:00
|
|
|
logger.error('Error extracting update', err);
|
2019-09-17 21:56:58 +02:00
|
|
|
UpdateModel.set({
|
2019-08-18 08:05:38 +02:00
|
|
|
updateStatus: 'error',
|
|
|
|
updateError: 'Error extracting update'
|
|
|
|
});
|
2015-11-13 20:56:22 +01:00
|
|
|
} else {
|
2019-09-17 21:56:58 +02:00
|
|
|
UpdateModel.set({ updateStatus: 'ready', updateError: null });
|
2015-11-14 12:09:36 +01:00
|
|
|
if (!startedByUser) {
|
2019-09-16 22:57:56 +02:00
|
|
|
Events.emit('update-app');
|
2015-11-14 12:09:36 +01:00
|
|
|
}
|
2015-11-16 20:04:33 +01:00
|
|
|
if (typeof successCallback === 'function') {
|
|
|
|
successCallback();
|
|
|
|
}
|
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
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
extractAppUpdate(updateFile, cb) {
|
2017-01-31 07:50:28 +01:00
|
|
|
const expectedFiles = this.UpdateCheckFiles;
|
|
|
|
const appPath = Launcher.getUserDataPath();
|
|
|
|
const StreamZip = Launcher.req('node-stream-zip');
|
2017-12-02 20:38:13 +01:00
|
|
|
StreamZip.setFs(Launcher.req('original-fs'));
|
2017-01-31 07:50:28 +01:00
|
|
|
const zip = new StreamZip({ file: updateFile, storeEntries: true });
|
2015-11-13 20:56:22 +01:00
|
|
|
zip.on('error', cb);
|
2016-07-17 13:30:38 +02:00
|
|
|
zip.on('ready', () => {
|
2020-06-01 16:53:51 +02:00
|
|
|
const containsAll = expectedFiles.every((expFile) => {
|
2017-01-31 07:50:28 +01:00
|
|
|
const entry = zip.entry(expFile);
|
2015-11-13 20:56:22 +01:00
|
|
|
return entry && entry.isFile;
|
|
|
|
});
|
|
|
|
if (!containsAll) {
|
|
|
|
return cb('Bad archive');
|
|
|
|
}
|
2019-09-28 14:17:55 +02:00
|
|
|
this.validateArchiveSignature(updateFile, zip)
|
|
|
|
.then(() => {
|
2020-06-01 16:53:51 +02:00
|
|
|
zip.extract(null, appPath, (err) => {
|
2019-09-28 14:17:55 +02:00
|
|
|
zip.close();
|
|
|
|
if (err) {
|
|
|
|
return cb(err);
|
|
|
|
}
|
|
|
|
Launcher.deleteFile(updateFile);
|
|
|
|
cb();
|
|
|
|
});
|
|
|
|
})
|
2020-06-01 16:53:51 +02:00
|
|
|
.catch((e) => {
|
2019-09-28 14:17:55 +02:00
|
|
|
return cb('Invalid archive: ' + e);
|
|
|
|
});
|
2015-11-13 20:56:22 +01:00
|
|
|
});
|
2015-11-14 16:28:36 +01:00
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
validateArchiveSignature(archivePath, zip) {
|
2016-03-05 12:16:12 +01:00
|
|
|
if (!zip.comment) {
|
2019-09-28 14:17:55 +02:00
|
|
|
return Promise.reject('No comment in ZIP');
|
2016-03-05 12:16:12 +01:00
|
|
|
}
|
|
|
|
if (zip.comment.length !== 512) {
|
2019-09-28 14:17:55 +02:00
|
|
|
return Promise.reject('Bad comment length in ZIP: ' + zip.comment.length);
|
2016-03-05 12:16:12 +01:00
|
|
|
}
|
|
|
|
try {
|
2017-01-31 07:50:28 +01:00
|
|
|
const zipFileData = Launcher.req('fs').readFileSync(archivePath);
|
2019-09-28 14:17:55 +02:00
|
|
|
const dataToVerify = zipFileData.slice(0, zip.centralDirectory.headerOffset + 22);
|
2019-01-06 11:44:10 +01:00
|
|
|
const signature = window.Buffer.from(zip.comment, 'hex');
|
2019-09-28 14:17:55 +02:00
|
|
|
return SignatureVerifier.verify(dataToVerify, signature)
|
|
|
|
.catch(() => {
|
|
|
|
throw new Error('Error verifying signature');
|
|
|
|
})
|
2020-06-01 16:53:51 +02:00
|
|
|
.then((isValid) => {
|
2019-09-28 14:17:55 +02:00
|
|
|
if (!isValid) {
|
|
|
|
throw new Error('Invalid signature');
|
|
|
|
}
|
|
|
|
});
|
2016-03-05 12:16:12 +01:00
|
|
|
} catch (err) {
|
2019-09-28 14:17:55 +02:00
|
|
|
return Promise.reject(err.toString());
|
2016-03-05 12:16:12 +01:00
|
|
|
}
|
2015-10-25 20:26:33 +01:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2019-09-15 14:16:32 +02:00
|
|
|
export { Updater };
|