2019-09-15 14:16:32 +02:00
|
|
|
import Backbone from 'backbone';
|
|
|
|
import { RuntimeInfo } from 'comp/app/runtime-info';
|
|
|
|
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';
|
|
|
|
import publicKey from 'public-key.pem';
|
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;
|
|
|
|
}
|
2017-01-31 07:50:28 +01:00
|
|
|
let autoUpdate = AppSettingsModel.instance.get('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 (
|
|
|
|
UpdateModel.instance.get('status') === 'checking' ||
|
|
|
|
['downloading', 'extracting'].indexOf(UpdateModel.instance.get('updateStatus')) >= 0
|
|
|
|
);
|
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();
|
2015-11-14 16:28:36 +01:00
|
|
|
if (!Launcher && window.applicationCache) {
|
2019-08-18 08:05:38 +02:00
|
|
|
window.applicationCache.addEventListener(
|
|
|
|
'updateready',
|
|
|
|
this.checkAppCacheUpdateReady.bind(this)
|
|
|
|
);
|
2015-11-14 16:28:36 +01:00
|
|
|
this.checkAppCacheUpdateReady();
|
|
|
|
}
|
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;
|
|
|
|
const lastCheckDate = UpdateModel.instance.get('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;
|
|
|
|
}
|
2015-10-29 22:20:01 +01:00
|
|
|
UpdateModel.instance.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({
|
2016-02-04 22:08:04 +01:00
|
|
|
url: Links.Manifest,
|
2015-11-13 20:56:22 +01:00
|
|
|
utf8: true,
|
2016-07-17 13:30:38 +02:00
|
|
|
success: data => {
|
2017-01-31 07:50:28 +01:00
|
|
|
const dt = new Date();
|
|
|
|
const match = data.match(/#\s*(\d+\-\d+\-\d+):v([\d+\.\w]+)/);
|
2015-12-12 09:53:50 +01:00
|
|
|
logger.info('Update check: ' + (match ? match[0] : 'unknown'));
|
2015-10-25 20:26:33 +01:00
|
|
|
if (!match) {
|
2017-01-31 07:50:28 +01:00
|
|
|
const errMsg = 'No version info found';
|
2019-08-18 08:05:38 +02:00
|
|
|
UpdateModel.instance.set({
|
|
|
|
status: 'error',
|
|
|
|
lastCheckDate: dt,
|
|
|
|
lastCheckError: errMsg
|
|
|
|
});
|
2015-10-29 22:20:01 +01:00
|
|
|
UpdateModel.instance.save();
|
2016-07-17 13:30:38 +02:00
|
|
|
this.scheduleNextCheck();
|
2015-10-25 20:26:33 +01:00
|
|
|
return;
|
|
|
|
}
|
2017-01-31 07:50:28 +01:00
|
|
|
const updateMinVersionMatch = data.match(/#\s*updmin:v([\d+\.\w]+)/);
|
|
|
|
const prevLastVersion = UpdateModel.instance.get('lastVersion');
|
2015-11-13 20:56:22 +01:00
|
|
|
UpdateModel.instance.set({
|
|
|
|
status: 'ok',
|
|
|
|
lastCheckDate: dt,
|
|
|
|
lastSuccessCheckDate: dt,
|
|
|
|
lastVersionReleaseDate: new Date(match[1]),
|
|
|
|
lastVersion: match[2],
|
2016-01-17 21:34:40 +01:00
|
|
|
lastCheckError: null,
|
|
|
|
lastCheckUpdMin: updateMinVersionMatch ? updateMinVersionMatch[1] : null
|
2015-11-13 20:56:22 +01:00
|
|
|
});
|
2015-10-29 22:20:01 +01:00
|
|
|
UpdateModel.instance.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 (
|
|
|
|
prevLastVersion === UpdateModel.instance.get('lastVersion') &&
|
|
|
|
UpdateModel.instance.get('updateStatus') === 'ready'
|
|
|
|
) {
|
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 (
|
|
|
|
SemVer.compareVersions(
|
|
|
|
UpdateModel.instance.get('lastVersion'),
|
|
|
|
RuntimeInfo.version
|
|
|
|
) > 0
|
|
|
|
) {
|
2015-11-16 20:04:33 +01:00
|
|
|
UpdateModel.instance.set('updateStatus', 'found');
|
|
|
|
}
|
2015-10-29 22:20:01 +01:00
|
|
|
},
|
2016-07-17 13:30:38 +02:00
|
|
|
error: e => {
|
2015-12-12 09:53:50 +01:00
|
|
|
logger.error('Update check error', e);
|
2015-11-13 20:56:22 +01:00
|
|
|
UpdateModel.instance.set({
|
|
|
|
status: 'error',
|
|
|
|
lastCheckDate: new Date(),
|
|
|
|
lastCheckError: 'Error checking last version'
|
|
|
|
});
|
2015-10-29 22:20:01 +01:00
|
|
|
UpdateModel.instance.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() {
|
2017-01-31 07:50:28 +01:00
|
|
|
const minLauncherVersion = UpdateModel.instance.get('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) {
|
|
|
|
UpdateModel.instance.set({ updateStatus: 'ready', updateManual: true });
|
|
|
|
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) {
|
2017-01-31 07:50:28 +01:00
|
|
|
const ver = UpdateModel.instance.get('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;
|
|
|
|
}
|
2015-11-13 20:56:22 +01:00
|
|
|
UpdateModel.instance.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,
|
2016-07-17 13:30:38 +02:00
|
|
|
success: filePath => {
|
2015-11-14 12:09:36 +01:00
|
|
|
UpdateModel.instance.set('updateStatus', 'extracting');
|
2016-07-17 13:30:38 +02:00
|
|
|
logger.info('Extracting update file', this.UpdateCheckFiles, filePath);
|
|
|
|
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-08-18 08:05:38 +02:00
|
|
|
UpdateModel.instance.set({
|
|
|
|
updateStatus: 'error',
|
|
|
|
updateError: 'Error extracting update'
|
|
|
|
});
|
2015-11-13 20:56:22 +01:00
|
|
|
} else {
|
|
|
|
UpdateModel.instance.set({ updateStatus: 'ready', updateError: null });
|
2015-11-14 12:09:36 +01:00
|
|
|
if (!startedByUser) {
|
|
|
|
Backbone.trigger('update-app');
|
|
|
|
}
|
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-08-18 08:05:38 +02:00
|
|
|
UpdateModel.instance.set({
|
|
|
|
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', () => {
|
2017-01-31 07:50:28 +01:00
|
|
|
const containsAll = expectedFiles.every(expFile => {
|
|
|
|
const entry = zip.entry(expFile);
|
2015-11-13 20:56:22 +01:00
|
|
|
return entry && entry.isFile;
|
|
|
|
});
|
|
|
|
if (!containsAll) {
|
|
|
|
return cb('Bad archive');
|
|
|
|
}
|
2017-01-31 07:50:28 +01:00
|
|
|
const validationError = this.validateArchiveSignature(updateFile, zip);
|
2016-03-05 12:16:12 +01:00
|
|
|
if (validationError) {
|
|
|
|
return cb('Invalid archive: ' + validationError);
|
|
|
|
}
|
2016-07-17 13:30:38 +02:00
|
|
|
zip.extract(null, appPath, err => {
|
2015-11-13 20:56:22 +01:00
|
|
|
zip.close();
|
|
|
|
if (err) {
|
|
|
|
return cb(err);
|
|
|
|
}
|
2019-01-06 19:20:56 +01:00
|
|
|
Launcher.deleteFile(updateFile);
|
2015-11-13 20:56:22 +01:00
|
|
|
cb();
|
|
|
|
});
|
|
|
|
});
|
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) {
|
|
|
|
return 'No comment in ZIP';
|
|
|
|
}
|
|
|
|
if (zip.comment.length !== 512) {
|
|
|
|
return 'Bad comment length in ZIP: ' + zip.comment.length;
|
|
|
|
}
|
|
|
|
try {
|
2017-01-31 07:50:28 +01:00
|
|
|
const zipFileData = Launcher.req('fs').readFileSync(archivePath);
|
|
|
|
const verify = Launcher.req('crypto').createVerify('RSA-SHA256');
|
2016-03-05 12:16:12 +01:00
|
|
|
verify.write(zipFileData.slice(0, zip.centralDirectory.headerOffset + 22));
|
|
|
|
verify.end();
|
2019-01-06 11:44:10 +01:00
|
|
|
const signature = window.Buffer.from(zip.comment, 'hex');
|
2016-03-05 12:16:12 +01:00
|
|
|
if (!verify.verify(publicKey, signature)) {
|
|
|
|
return 'Invalid signature';
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
return err.toString();
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
},
|
|
|
|
|
2019-08-18 10:17:09 +02:00
|
|
|
checkAppCacheUpdateReady() {
|
2015-11-14 16:28:36 +01:00
|
|
|
if (window.applicationCache.status === window.applicationCache.UPDATEREADY) {
|
2019-08-16 23:05:39 +02:00
|
|
|
try {
|
|
|
|
window.applicationCache.swapCache();
|
|
|
|
} catch (e) {}
|
2015-11-14 16:28:36 +01:00
|
|
|
UpdateModel.instance.set('updateStatus', 'ready');
|
|
|
|
}
|
2015-10-25 20:26:33 +01:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2019-09-15 14:16:32 +02:00
|
|
|
export { Updater };
|