204
src/updater.js
204
src/updater.js
@@ -7,19 +7,27 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
child_process = require('child_process'),
|
||||
backups = require('./backups.js'),
|
||||
caas = require('./caas.js'),
|
||||
config = require('./config.js'),
|
||||
crypto = require('crypto'),
|
||||
debug = require('debug')('box:updater'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
locker = require('./locker.js'),
|
||||
mkdirp = require('mkdirp'),
|
||||
os = require('os'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
progress = require('./progress.js'),
|
||||
safe = require('safetydance'),
|
||||
shell = require('./shell.js'),
|
||||
updateChecker = require('./updatechecker.js'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
const RELEASES_PUBLIC_KEY = path.join(__dirname, 'releases.gpg');
|
||||
const UPDATE_CMD = path.join(__dirname, 'scripts/update.sh');
|
||||
|
||||
function UpdaterError(reason, errorOrMessage) {
|
||||
@@ -47,6 +55,156 @@ UpdaterError.BAD_STATE = 'Bad state';
|
||||
UpdaterError.ALREADY_UPTODATE = 'No Update Available';
|
||||
UpdaterError.NOT_FOUND = 'Not found';
|
||||
UpdaterError.SELF_UPGRADE_NOT_SUPPORTED = 'Self upgrade not supported';
|
||||
UpdaterError.NOT_SIGNED = 'Not signed';
|
||||
|
||||
function downloadUrl(url, file, callback) {
|
||||
assert.strictEqual(typeof file, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// do not assert since it comes from the appstore
|
||||
if (typeof url !== 'string') return callback(new UpdaterError(UpdaterError.EXTERNAL_ERROR, `url cannot be download to ${file} as it is not a string`));
|
||||
|
||||
let retryCount = 0;
|
||||
|
||||
safe.fs.unlinkSync(file);
|
||||
|
||||
async.retry({ times: 10, interval: 5000 }, function (retryCallback) {
|
||||
debug(`Downloading ${url} to ${file}. Try ${++retryCount}`);
|
||||
|
||||
const args = `-s --fail ${url} -o ${file}`;
|
||||
|
||||
debug(`downloadUrl: curl ${args}`);
|
||||
|
||||
shell.exec('downloadUrl', '/usr/bin/curl', args.split(' '), { }, function (error) {
|
||||
if (error) return retryCallback(new UpdaterError(UpdaterError.EXTERNAL_ERROR, `Failed to download ${url}: ${error.message}`));
|
||||
|
||||
debug(`downloadUrl: downloadUrl ${url} to ${file}`);
|
||||
|
||||
retryCallback();
|
||||
});
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function gpgVerify(file, sig, callback) {
|
||||
const cmd = `/usr/bin/gpg --status-fd 1 --no-default-keyring --keyring ${RELEASES_PUBLIC_KEY} --verify ${sig} ${file}`;
|
||||
|
||||
debug(`gpgVerify: ${cmd}`);
|
||||
|
||||
child_process.exec(cmd, { encoding: 'utf8' }, function (error, stdout, stderr) {
|
||||
if (error) return callback(new UpdaterError(UpdaterError.NOT_SIGNED, `The signature in ${path.basename(sig)} could not verified`));
|
||||
|
||||
if (stdout.indexOf('[GNUPG:] VALIDSIG 0EADB19CDDA23CD0FE71E3470A372F8703C493CC')) return callback();
|
||||
|
||||
debug(`gpgVerify: verification of ${sig} failed: ${stdout}\n${stderr}`);
|
||||
|
||||
return callback(new UpdaterError(UpdaterError.NOT_SIGNED, `The signature in ${path.basename(sig)} could not verified`));
|
||||
});
|
||||
}
|
||||
|
||||
function extractTarball(tarball, dir, callback) {
|
||||
const args = `-zxf ${tarball} -C ${dir}`;
|
||||
|
||||
debug(`extractTarball: tar ${args}`);
|
||||
|
||||
shell.exec('extractTarball', '/bin/tar', args.split(' '), { }, function (error) {
|
||||
if (error) return callback(new UpdaterError(UpdaterError.EXTERNAL_ERROR, `Failed to extract release package: ${error.message}`));
|
||||
|
||||
safe.fs.unlinkSync(tarball);
|
||||
|
||||
debug(`extractTarball: extracted ${tarball} to ${dir}`);
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function verifyUpdateInfo(versionsFile, updateInfo, callback) {
|
||||
assert.strictEqual(typeof versionsFile, 'string');
|
||||
assert.strictEqual(typeof updateInfo, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var releases = safe.JSON.parse(safe.fs.readFileSync(versionsFile, 'utf8')) || { };
|
||||
if (!releases[config.version()] || !releases[config.version()].next) return callback(new UpdaterError(UpdaterError.EXTERNAL_ERROR, 'No version info'));
|
||||
var nextVersion = releases[config.version()].next;
|
||||
if (typeof releases[nextVersion] !== 'object' || !releases[nextVersion]) return callback(new UpdaterError(UpdaterError.EXTERNAL_ERROR, 'No next version info'));
|
||||
if (releases[nextVersion].sourceTarballUrl !== updateInfo.sourceTarballUrl) return callback(new UpdaterError(UpdaterError.EXTERNAL_ERROR, 'Version info mismatch'));
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
function downloadAndVerifyRelease(updateInfo, callback) {
|
||||
assert.strictEqual(typeof updateInfo, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
let newBoxSource = path.join(os.tmpdir(), 'box-' + crypto.randomBytes(4).readUInt32LE(0));
|
||||
|
||||
async.series([
|
||||
downloadUrl.bind(null, updateInfo.boxVersionsUrl, `${paths.UPDATE_DIR}/versions.json`),
|
||||
downloadUrl.bind(null, updateInfo.boxVersionsSigUrl, `${paths.UPDATE_DIR}/versions.json.sig`),
|
||||
gpgVerify.bind(null, `${paths.UPDATE_DIR}/versions.json`, `${paths.UPDATE_DIR}/versions.json.sig`),
|
||||
verifyUpdateInfo.bind(null, `${paths.UPDATE_DIR}/versions.json`, updateInfo),
|
||||
downloadUrl.bind(null, updateInfo.sourceTarballUrl, `${paths.UPDATE_DIR}/box.tar.gz`),
|
||||
downloadUrl.bind(null, updateInfo.sourceTarballSigUrl, `${paths.UPDATE_DIR}/box.tar.gz.sig`),
|
||||
gpgVerify.bind(null, `${paths.UPDATE_DIR}/box.tar.gz`, `${paths.UPDATE_DIR}/box.tar.gz.sig`),
|
||||
mkdirp.bind(null, newBoxSource),
|
||||
extractTarball.bind(null, `${paths.UPDATE_DIR}/box.tar.gz`, newBoxSource)
|
||||
], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, { file: newBoxSource });
|
||||
});
|
||||
}
|
||||
|
||||
function doUpdate(boxUpdateInfo, callback) {
|
||||
assert(boxUpdateInfo && typeof boxUpdateInfo === 'object');
|
||||
|
||||
function updateError(e) {
|
||||
progress.set(progress.UPDATE, -1, e.message);
|
||||
callback(e);
|
||||
}
|
||||
|
||||
progress.set(progress.UPDATE, 5, 'Downloading and verifying release');
|
||||
|
||||
downloadAndVerifyRelease(boxUpdateInfo, function (error, packageInfo) {
|
||||
if (error) return updateError(error);
|
||||
|
||||
progress.set(progress.UPDATE, 10, 'Backing up');
|
||||
|
||||
backups.backupBoxAndApps({ userId: null, username: 'updater' }, function (error) {
|
||||
if (error) return updateError(error);
|
||||
|
||||
// NOTE: this data is opaque and will be passed through the installer.sh
|
||||
var data= {
|
||||
provider: config.provider(),
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
webServerOrigin: config.webServerOrigin(),
|
||||
adminDomain: config.adminDomain(),
|
||||
adminFqdn: config.adminFqdn(),
|
||||
adminLocation: config.adminLocation(),
|
||||
isDemo: config.isDemo(),
|
||||
|
||||
appstore: {
|
||||
apiServerOrigin: config.apiServerOrigin()
|
||||
},
|
||||
caas: {
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
webServerOrigin: config.webServerOrigin()
|
||||
},
|
||||
|
||||
version: boxUpdateInfo.version
|
||||
};
|
||||
|
||||
debug('updating box %s %j', boxUpdateInfo.sourceTarballUrl, _.omit(data, 'tlsCert', 'tlsKey', 'token', 'appstore', 'caas'));
|
||||
|
||||
progress.set(progress.UPDATE, 70, 'Installing update');
|
||||
|
||||
shell.sudo('update', [ UPDATE_CMD, packageInfo.file, JSON.stringify(data) ], function (error) {
|
||||
if (error) return updateError(error);
|
||||
|
||||
// Do not add any code here. The installer script will stop the box code any instant
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function update(boxUpdateInfo, auditSource, callback) {
|
||||
assert.strictEqual(typeof boxUpdateInfo, 'object');
|
||||
@@ -97,49 +255,3 @@ function updateToLatest(auditSource, callback) {
|
||||
|
||||
update(boxUpdateInfo, auditSource, callback);
|
||||
}
|
||||
|
||||
function doUpdate(boxUpdateInfo, callback) {
|
||||
assert(boxUpdateInfo && typeof boxUpdateInfo === 'object');
|
||||
|
||||
function updateError(e) {
|
||||
progress.set(progress.UPDATE, -1, e.message);
|
||||
callback(e);
|
||||
}
|
||||
|
||||
progress.set(progress.UPDATE, 5, 'Backing up for update');
|
||||
|
||||
backups.backupBoxAndApps({ userId: null, username: 'updater' }, function (error) {
|
||||
if (error) return updateError(error);
|
||||
|
||||
// NOTE: this data is opaque and will be passed through the installer.sh
|
||||
var data= {
|
||||
provider: config.provider(),
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
webServerOrigin: config.webServerOrigin(),
|
||||
adminDomain: config.adminDomain(),
|
||||
adminFqdn: config.adminFqdn(),
|
||||
adminLocation: config.adminLocation(),
|
||||
isDemo: config.isDemo(),
|
||||
|
||||
appstore: {
|
||||
apiServerOrigin: config.apiServerOrigin()
|
||||
},
|
||||
caas: {
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
webServerOrigin: config.webServerOrigin()
|
||||
},
|
||||
|
||||
version: boxUpdateInfo.version
|
||||
};
|
||||
|
||||
debug('updating box %s %j', boxUpdateInfo.sourceTarballUrl, _.omit(data, 'tlsCert', 'tlsKey', 'token', 'appstore', 'caas'));
|
||||
|
||||
progress.set(progress.UPDATE, 5, 'Downloading and installing new version');
|
||||
|
||||
shell.sudo('update', [ UPDATE_CMD, boxUpdateInfo.sourceTarballUrl, JSON.stringify(data) ], function (error) {
|
||||
if (error) return updateError(error);
|
||||
|
||||
// Do not add any code here. The installer script will stop the box code any instant
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user