2018-07-31 11:35:23 -07:00
'use strict' ;
exports = module . exports = {
updateToLatest : updateToLatest ,
2018-11-30 16:00:47 -08:00
update : update ,
2018-07-31 11:35:23 -07:00
UpdaterError : UpdaterError
} ;
2019-02-25 10:03:46 -08:00
var apps = require ( './apps.js' ) ,
assert = require ( 'assert' ) ,
2018-08-01 15:38:40 -07:00
async = require ( 'async' ) ,
child _process = require ( 'child_process' ) ,
2018-07-31 11:35:23 -07:00
backups = require ( './backups.js' ) ,
config = require ( './config.js' ) ,
2019-07-25 14:40:52 -07:00
constants = require ( './constants.js' ) ,
2018-08-01 15:38:40 -07:00
crypto = require ( 'crypto' ) ,
2018-07-31 11:35:23 -07:00
debug = require ( 'debug' ) ( 'box:updater' ) ,
2018-12-09 03:20:00 -08:00
eventlog = require ( './eventlog.js' ) ,
locker = require ( './locker.js' ) ,
2018-08-01 15:38:40 -07:00
mkdirp = require ( 'mkdirp' ) ,
os = require ( 'os' ) ,
2018-07-31 11:35:23 -07:00
path = require ( 'path' ) ,
2018-08-01 15:38:40 -07:00
paths = require ( './paths.js' ) ,
safe = require ( 'safetydance' ) ,
2019-02-25 10:03:46 -08:00
semver = require ( 'semver' ) ,
2018-07-31 11:35:23 -07:00
shell = require ( './shell.js' ) ,
2018-11-19 20:01:02 -08:00
tasks = require ( './tasks.js' ) ,
2018-07-31 11:35:23 -07:00
updateChecker = require ( './updatechecker.js' ) ,
2018-11-19 20:01:02 -08:00
util = require ( 'util' ) ;
2018-07-31 11:35:23 -07:00
2018-08-01 15:38:40 -07:00
const RELEASES _PUBLIC _KEY = path . join ( _ _dirname , 'releases.gpg' ) ;
2018-07-31 11:35:23 -07:00
const UPDATE _CMD = path . join ( _ _dirname , 'scripts/update.sh' ) ;
function UpdaterError ( reason , errorOrMessage ) {
assert . strictEqual ( typeof reason , 'string' ) ;
assert ( errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined' ) ;
Error . call ( this ) ;
Error . captureStackTrace ( this , this . constructor ) ;
this . name = this . constructor . name ;
this . reason = reason ;
if ( typeof errorOrMessage === 'undefined' ) {
this . message = reason ;
} else if ( typeof errorOrMessage === 'string' ) {
this . message = errorOrMessage ;
} else {
this . message = 'Internal error' ;
this . nestedError = errorOrMessage ;
}
}
util . inherits ( UpdaterError , Error ) ;
UpdaterError . INTERNAL _ERROR = 'Internal Error' ;
UpdaterError . EXTERNAL _ERROR = 'External Error' ;
UpdaterError . BAD _STATE = 'Bad state' ;
UpdaterError . ALREADY _UPTODATE = 'No Update Available' ;
UpdaterError . NOT _FOUND = 'Not found' ;
2018-08-01 15:38:40 -07:00
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 } ` ) ;
2018-11-25 14:57:17 -08:00
shell . spawn ( 'downloadUrl' , '/usr/bin/curl' , args . split ( ' ' ) , { } , function ( error ) {
2018-08-01 15:38:40 -07:00
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 } ` ) ;
2018-11-25 14:57:17 -08:00
shell . spawn ( 'extractTarball' , '/bin/tar' , args . split ( ' ' ) , { } , function ( error ) {
2018-08-01 15:38:40 -07:00
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' ) ) || { } ;
2019-07-25 14:40:52 -07:00
if ( ! releases [ constants . VERSION ] || ! releases [ constants . VERSION ] . next ) return callback ( new UpdaterError ( UpdaterError . EXTERNAL _ERROR , 'No version info' ) ) ;
var nextVersion = releases [ constants . VERSION ] . next ;
2018-08-01 15:38:40 -07:00
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 } ) ;
} ) ;
}
2019-05-12 13:28:53 -07:00
function update ( boxUpdateInfo , options , progressCallback , callback ) {
2018-08-01 15:38:40 -07:00
assert ( boxUpdateInfo && typeof boxUpdateInfo === 'object' ) ;
2019-05-12 13:28:53 -07:00
assert ( options && typeof options === 'object' ) ;
2018-11-30 16:00:47 -08:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2018-08-01 15:38:40 -07:00
2018-11-30 16:00:47 -08:00
progressCallback ( { percent : 5 , message : 'Downloading and verifying release' } ) ;
2018-08-01 15:38:40 -07:00
downloadAndVerifyRelease ( boxUpdateInfo , function ( error , packageInfo ) {
2018-11-30 16:00:47 -08:00
if ( error ) return callback ( error ) ;
2018-08-01 15:38:40 -07:00
2019-05-12 13:28:53 -07:00
function maybeBackup ( next ) {
if ( options . skipBackup ) return next ( ) ;
2018-08-01 15:38:40 -07:00
2019-05-12 13:28:53 -07:00
progressCallback ( { percent : 10 , message : 'Backing up' } ) ;
backups . backupBoxAndApps ( ( progress ) => progressCallback ( { percent : 10 + progress . percent * 70 / 100 , message : progress . message } ) , next ) ;
}
maybeBackup ( function ( error ) {
2018-11-30 16:00:47 -08:00
if ( error ) return callback ( error ) ;
2018-08-01 15:38:40 -07:00
2018-10-26 09:49:52 -07:00
debug ( 'updating box %s' , boxUpdateInfo . sourceTarballUrl ) ;
2018-08-01 15:38:40 -07:00
2018-11-30 16:00:47 -08:00
progressCallback ( { percent : 70 , message : 'Installing update' } ) ;
2018-08-01 15:38:40 -07:00
2018-11-30 16:00:47 -08:00
// run installer.sh from new box code as a separate service
2018-11-25 14:57:17 -08:00
shell . sudo ( 'update' , [ UPDATE _CMD , packageInfo . file ] , { } , function ( error ) {
2018-11-30 16:00:47 -08:00
if ( error ) return callback ( error ) ;
2018-08-01 15:38:40 -07:00
// Do not add any code here. The installer script will stop the box code any instant
} ) ;
} ) ;
} ) ;
}
2018-07-31 11:35:23 -07:00
2019-02-25 10:03:46 -08:00
function canUpdate ( boxUpdateInfo , callback ) {
assert . strictEqual ( typeof boxUpdateInfo , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
apps . getAll ( function ( error , result ) {
if ( error ) return callback ( new UpdaterError ( UpdaterError . INTERNAL _ERROR , error ) ) ;
for ( let app of result ) {
const maxBoxVersion = app . manifest . maxBoxVersion ;
if ( semver . valid ( maxBoxVersion ) && semver . gt ( boxUpdateInfo . version , maxBoxVersion ) ) {
return callback ( new UpdaterError ( UpdaterError . BAD _STATE , ` Cannot update to v ${ boxUpdateInfo . version } because ${ app . fqdn } has a maxBoxVersion of ${ maxBoxVersion } ` ) ) ;
}
}
callback ( ) ;
} ) ;
}
2019-05-12 13:28:53 -07:00
function updateToLatest ( options , auditSource , callback ) {
assert . strictEqual ( typeof options , 'object' ) ;
2018-07-31 11:35:23 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
var boxUpdateInfo = updateChecker . getUpdateInfo ( ) . box ;
if ( ! boxUpdateInfo ) return callback ( new UpdaterError ( UpdaterError . ALREADY _UPTODATE , 'No update available' ) ) ;
if ( ! boxUpdateInfo . sourceTarballUrl ) return callback ( new UpdaterError ( UpdaterError . BAD _STATE , 'No automatic update available' ) ) ;
2019-02-25 10:03:46 -08:00
canUpdate ( boxUpdateInfo , function ( error ) {
if ( error ) return callback ( error ) ;
2018-12-04 14:04:43 -08:00
2019-02-25 10:03:46 -08:00
error = locker . lock ( locker . OP _BOX _UPDATE ) ;
if ( error ) return callback ( new UpdaterError ( UpdaterError . BAD _STATE , ` Cannot update now: ${ error . message } ` ) ) ;
2018-12-09 03:20:00 -08:00
2019-05-12 13:28:53 -07:00
let task = tasks . startTask ( tasks . TASK _UPDATE , [ boxUpdateInfo , options ] ) ;
2019-02-25 10:03:46 -08:00
task . on ( 'error' , ( error ) => callback ( new UpdaterError ( UpdaterError . INTERNAL _ERROR , error ) ) ) ;
task . on ( 'start' , ( taskId ) => {
eventlog . add ( eventlog . ACTION _UPDATE , auditSource , { taskId , boxUpdateInfo } ) ;
callback ( null , taskId ) ;
} ) ;
task . on ( 'finish' , ( error ) => {
locker . unlock ( locker . OP _BOX _UPDATE ) ;
debug ( 'Update failed with error' , error ) ;
} ) ;
2018-12-09 03:20:00 -08:00
} ) ;
2018-07-31 11:35:23 -07:00
}