2018-07-31 11:35:23 -07:00
'use strict' ;
exports = module . exports = {
2023-08-03 14:26:41 +05:30
setAutoupdatePattern ,
getAutoupdatePattern ,
2021-01-31 20:46:55 -08:00
updateToLatest ,
2023-08-12 19:28:07 +05:30
update ,
notifyUpdate
2018-07-31 11:35:23 -07:00
} ;
2021-08-31 11:16:58 -07:00
const apps = require ( './apps.js' ) ,
2019-02-25 10:03:46 -08:00
assert = require ( 'assert' ) ,
2023-08-12 19:28:07 +05:30
AuditSource = require ( './auditsource.js' ) ,
2019-10-23 09:39:26 -07:00
BoxError = require ( './boxerror.js' ) ,
2023-08-04 11:24:28 +05:30
backups = require ( './backups.js' ) ,
2021-08-31 11:16:58 -07:00
backuptask = require ( './backuptask.js' ) ,
2019-07-25 14:40:52 -07:00
constants = require ( './constants.js' ) ,
2023-08-03 14:26:41 +05:30
cron = require ( './cron.js' ) ,
2024-04-19 18:19:41 +02:00
{ CronTime } = require ( 'cron' ) ,
2018-08-01 15:38:40 -07:00
crypto = require ( 'crypto' ) ,
2018-07-31 11:35:23 -07:00
debug = require ( 'debug' ) ( 'box:updater' ) ,
2022-10-18 19:32:07 +02:00
df = require ( './df.js' ) ,
2018-12-09 03:20:00 -08:00
eventlog = require ( './eventlog.js' ) ,
2020-06-11 08:27:48 -07:00
fs = require ( 'fs' ) ,
2018-12-09 03:20:00 -08:00
locker = require ( './locker.js' ) ,
2018-08-01 15:38:40 -07:00
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' ) ,
2021-08-31 13:12:14 -07:00
promiseRetry = require ( './promise-retry.js' ) ,
2018-08-01 15:38:40 -07:00
safe = require ( 'safetydance' ) ,
2019-02-25 10:03:46 -08:00
semver = require ( 'semver' ) ,
2021-02-01 14:07:23 -08:00
settings = require ( './settings.js' ) ,
2018-07-31 11:35:23 -07:00
shell = require ( './shell.js' ) ,
2018-11-19 20:01:02 -08:00
tasks = require ( './tasks.js' ) ,
2021-09-26 18:37:04 -07:00
updateChecker = require ( './updatechecker.js' ) ;
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' ) ;
2023-08-03 14:26:41 +05:30
async function setAutoupdatePattern ( pattern ) {
assert . strictEqual ( typeof pattern , 'string' ) ;
if ( pattern !== constants . AUTOUPDATE _PATTERN _NEVER ) { // check if pattern is valid
2024-04-19 18:19:41 +02:00
const job = safe . safeCall ( function ( ) { return new CronTime ( pattern ) ; } ) ;
2023-08-03 14:26:41 +05:30
if ( ! job ) throw new BoxError ( BoxError . BAD _FIELD , 'Invalid pattern' ) ;
}
await settings . set ( settings . AUTOUPDATE _PATTERN _KEY , pattern ) ;
2023-08-04 11:43:39 +05:30
await cron . handleAutoupdatePatternChanged ( pattern ) ;
2023-08-03 14:26:41 +05:30
}
async function getAutoupdatePattern ( ) {
const pattern = await settings . get ( settings . AUTOUPDATE _PATTERN _KEY ) ;
return pattern || cron . DEFAULT _AUTOUPDATE _PATTERN ;
}
2021-08-31 13:12:14 -07:00
async function downloadUrl ( url , file ) {
2018-08-01 15:38:40 -07:00
assert . strictEqual ( typeof file , 'string' ) ;
// do not assert since it comes from the appstore
2021-08-31 13:12:14 -07:00
if ( typeof url !== 'string' ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` url cannot be download to ${ file } as it is not a string ` ) ;
2018-08-01 15:38:40 -07:00
safe . fs . unlinkSync ( file ) ;
2021-12-07 11:18:26 -08:00
await promiseRetry ( { times : 10 , interval : 5000 , debug } , async function ( ) {
2024-02-21 13:09:59 +01:00
debug ( ` downloadUrl: downloading ${ url } to ${ file } ` ) ;
2024-02-21 19:40:27 +01:00
const [ error ] = await safe ( shell . exec ( 'downloadUrl' , ` curl -s --fail ${ url } -o ${ file } ` , { } ) ) ;
2021-08-31 13:12:14 -07:00
if ( error ) throw new BoxError ( BoxError . NETWORK _ERROR , ` Failed to download ${ url } : ${ error . message } ` ) ;
2024-02-21 13:09:59 +01:00
debug ( 'downloadUrl: done' ) ;
2021-08-31 13:12:14 -07:00
} ) ;
2018-08-01 15:38:40 -07:00
}
2021-08-31 13:12:14 -07:00
async function gpgVerify ( file , sig ) {
assert . strictEqual ( typeof file , 'string' ) ;
assert . strictEqual ( typeof sig , 'string' ) ;
2018-08-01 15:38:40 -07:00
const cmd = ` /usr/bin/gpg --status-fd 1 --no-default-keyring --keyring ${ RELEASES _PUBLIC _KEY } --verify ${ sig } ${ file } ` ;
debug ( ` gpgVerify: ${ cmd } ` ) ;
2024-02-21 19:40:27 +01:00
const [ error , stdout ] = await safe ( shell . exec ( 'gpgVerify' , cmd , { } ) ) ;
2021-08-31 13:12:14 -07:00
if ( error ) {
debug ( ` gpgVerify: command failed. error: ${ error } \n stdout: ${ error . stdout } \n stderr: ${ error . stderr } ` ) ;
throw new BoxError ( BoxError . NOT _SIGNED , ` The signature in ${ path . basename ( sig ) } could not be verified (command failed) ` ) ;
}
2018-08-01 15:38:40 -07:00
2021-08-31 13:12:14 -07:00
if ( stdout . indexOf ( '[GNUPG:] VALIDSIG 0EADB19CDDA23CD0FE71E3470A372F8703C493CC' ) !== - 1 ) return ; // success
2018-08-01 15:38:40 -07:00
2021-08-31 13:12:14 -07:00
debug ( ` gpgVerify: verification of ${ sig } failed: ${ stdout } \n ` ) ;
2018-08-01 15:38:40 -07:00
2021-08-31 13:12:14 -07:00
throw new BoxError ( BoxError . NOT _SIGNED , ` The signature in ${ path . basename ( sig ) } could not be verified (bad sig) ` ) ;
2018-08-01 15:38:40 -07:00
}
2021-08-31 13:12:14 -07:00
async function extractTarball ( tarball , dir ) {
assert . strictEqual ( typeof tarball , 'string' ) ;
assert . strictEqual ( typeof dir , 'string' ) ;
2024-02-21 13:09:59 +01:00
debug ( ` extractTarball: extracting ${ tarball } to ${ dir } ` ) ;
2018-08-01 15:38:40 -07:00
2024-02-21 19:40:27 +01:00
const [ error ] = await safe ( shell . exec ( 'extractTarball' , ` tar -zxf ${ tarball } -C ${ dir } ` , { } ) ) ;
2021-08-31 13:12:14 -07:00
if ( error ) throw new BoxError ( BoxError . FS _ERROR , ` Failed to extract release package: ${ error . message } ` ) ;
safe . fs . unlinkSync ( tarball ) ;
2018-08-01 15:38:40 -07:00
2024-02-21 13:09:59 +01:00
debug ( 'extractTarball: extracted' ) ;
2018-08-01 15:38:40 -07:00
}
2021-08-31 13:12:14 -07:00
async function verifyUpdateInfo ( versionsFile , updateInfo ) {
2018-08-01 15:38:40 -07:00
assert . strictEqual ( typeof versionsFile , 'string' ) ;
assert . strictEqual ( typeof updateInfo , 'object' ) ;
2021-03-03 10:21:52 -08:00
const releases = safe . JSON . parse ( safe . fs . readFileSync ( versionsFile , 'utf8' ) ) || { } ;
2021-08-31 13:12:14 -07:00
if ( ! releases [ constants . VERSION ] ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` No version info for ${ constants . VERSION } ` ) ;
if ( ! releases [ constants . VERSION ] . next ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` No next version info for ${ constants . VERSION } ` ) ;
2021-03-03 10:21:52 -08:00
const nextVersion = releases [ constants . VERSION ] . next ;
2021-08-31 13:12:14 -07:00
if ( typeof releases [ nextVersion ] !== 'object' || ! releases [ nextVersion ] ) throw new BoxError ( BoxError . EXTERNAL _ERROR , 'No next version info' ) ;
if ( releases [ nextVersion ] . sourceTarballUrl !== updateInfo . sourceTarballUrl ) throw new BoxError ( BoxError . EXTERNAL _ERROR , 'Version info mismatch' ) ;
2018-08-01 15:38:40 -07:00
}
2021-08-31 13:12:14 -07:00
async function downloadAndVerifyRelease ( updateInfo ) {
2018-08-01 15:38:40 -07:00
assert . strictEqual ( typeof updateInfo , 'object' ) ;
2021-08-31 13:12:14 -07:00
2024-05-13 16:50:14 +02:00
await safe ( shell . exec ( 'cleanupOldArtifacts' , ` rm -rf ${ path . join ( os . tmpdir ( ) , 'box-*' ) } ` , { shell : '/bin/bash' } ) , { debug } ) ; // remove any old artifacts
2021-08-31 13:12:14 -07:00
await downloadUrl ( updateInfo . boxVersionsUrl , ` ${ paths . UPDATE _DIR } /versions.json ` ) ;
await downloadUrl ( updateInfo . boxVersionsSigUrl , ` ${ paths . UPDATE _DIR } /versions.json.sig ` ) ;
await gpgVerify ( ` ${ paths . UPDATE _DIR } /versions.json ` , ` ${ paths . UPDATE _DIR } /versions.json.sig ` ) ;
await verifyUpdateInfo ( ` ${ paths . UPDATE _DIR } /versions.json ` , updateInfo ) ;
await downloadUrl ( updateInfo . sourceTarballUrl , ` ${ paths . UPDATE _DIR } /box.tar.gz ` ) ;
await downloadUrl ( updateInfo . sourceTarballSigUrl , ` ${ paths . UPDATE _DIR } /box.tar.gz.sig ` ) ;
await gpgVerify ( ` ${ paths . UPDATE _DIR } /box.tar.gz ` , ` ${ paths . UPDATE _DIR } /box.tar.gz.sig ` ) ;
2024-05-13 16:50:14 +02:00
const newBoxSource = path . join ( os . tmpdir ( ) , 'box-' + crypto . randomBytes ( 4 ) . readUInt32LE ( 0 ) ) ;
2021-08-31 13:12:14 -07:00
const [ mkdirError ] = await safe ( fs . promises . mkdir ( newBoxSource , { recursive : true } ) ) ;
if ( mkdirError ) throw new BoxError ( BoxError . FS _ERROR , ` Failed to create directory ${ newBoxSource } : ${ mkdirError . message } ` ) ;
await extractTarball ( ` ${ paths . UPDATE _DIR } /box.tar.gz ` , newBoxSource ) ;
return { file : newBoxSource } ;
2018-08-01 15:38:40 -07:00
}
2021-08-31 13:12:14 -07:00
async function checkFreeDiskSpace ( neededSpace ) {
2019-08-12 21:09:22 -07:00
assert . strictEqual ( typeof neededSpace , 'number' ) ;
// can probably be a bit more aggressive here since a new update can bring in new docker images
2021-08-31 13:12:14 -07:00
const [ error , diskUsage ] = await safe ( df . file ( '/' ) ) ;
if ( error ) throw new BoxError ( BoxError . FS _ERROR , error ) ;
2019-08-12 21:47:22 -07:00
2023-01-30 12:54:25 +01:00
if ( diskUsage . available < neededSpace ) throw new BoxError ( BoxError . FS _ERROR , ` Not enough disk space. Updates require at least 2GB of free space. Available: ${ df . prettyBytes ( diskUsage . available ) } ` ) ;
2019-08-12 21:09:22 -07:00
}
2021-08-31 13:12:14 -07:00
async function update ( boxUpdateInfo , options , progressCallback ) {
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' ) ;
2018-08-01 15:38:40 -07:00
2019-08-12 21:09:22 -07:00
progressCallback ( { percent : 1 , message : 'Checking disk space' } ) ;
2018-08-01 15:38:40 -07:00
2022-02-18 09:56:35 -08:00
await checkFreeDiskSpace ( 2 * 1024 * 1024 * 1024 ) ;
2018-08-01 15:38:40 -07:00
2021-08-31 13:12:14 -07:00
progressCallback ( { percent : 5 , message : 'Downloading and verifying release' } ) ;
2019-08-12 21:09:22 -07:00
2021-08-31 13:12:14 -07:00
const packageInfo = await downloadAndVerifyRelease ( boxUpdateInfo ) ;
2018-08-01 15:38:40 -07:00
2021-08-31 13:12:14 -07:00
if ( ! options . skipBackup ) {
progressCallback ( { percent : 10 , message : 'Backing up' } ) ;
2018-08-01 15:38:40 -07:00
2021-09-26 18:37:04 -07:00
await backuptask . fullBackup ( { preserveSecs : 3 * 7 * 24 * 60 * 60 } , ( progress ) => progressCallback ( { percent : 10 + progress . percent * 70 / 100 , message : progress . message } ) ) ;
2021-11-16 18:20:12 -08:00
2022-02-18 09:56:35 -08:00
await checkFreeDiskSpace ( 2 * 1024 * 1024 * 1024 ) ; // check again in case backup is in same disk
2021-08-31 13:12:14 -07:00
}
2018-08-01 15:38:40 -07:00
2021-11-16 18:20:12 -08:00
debug ( ` Updating box with ${ boxUpdateInfo . sourceTarballUrl } ` ) ;
2019-08-12 21:09:22 -07:00
2021-08-31 13:12:14 -07:00
progressCallback ( { percent : 70 , message : 'Installing update' } ) ;
2019-08-12 21:09:22 -07:00
2023-05-15 19:09:40 +02:00
await shell . promises . sudo ( 'update' , [ UPDATE _CMD , packageInfo . file , process . stdout . logFile ] , { } ) ; // run installer.sh from new box code as a separate service
2019-08-12 21:09:22 -07:00
2021-08-31 13:12:14 -07:00
// Do not add any code here. The installer script will stop the box code any instant
2018-08-01 15:38:40 -07:00
}
2018-07-31 11:35:23 -07:00
2023-09-09 20:46:24 +05:30
async function checkUpdateRequirements ( boxUpdateInfo ) {
2019-02-25 10:03:46 -08:00
assert . strictEqual ( typeof boxUpdateInfo , 'object' ) ;
2021-08-20 09:19:44 -07:00
const result = await apps . list ( ) ;
2019-02-25 10:03:46 -08:00
2024-05-13 17:02:20 +02:00
for ( const app of result ) {
2021-08-20 09:19:44 -07:00
const maxBoxVersion = app . manifest . maxBoxVersion ;
if ( semver . valid ( maxBoxVersion ) && semver . gt ( boxUpdateInfo . version , maxBoxVersion ) ) {
throw new BoxError ( BoxError . BAD _STATE , ` Cannot update to v ${ boxUpdateInfo . version } because ${ app . fqdn } has a maxBoxVersion of ${ maxBoxVersion } ` ) ;
2019-02-25 10:03:46 -08:00
}
2021-08-20 09:19:44 -07:00
}
2019-02-25 10:03:46 -08:00
}
2021-08-20 09:19:44 -07:00
async function updateToLatest ( options , auditSource ) {
2019-05-12 13:28:53 -07:00
assert . strictEqual ( typeof options , 'object' ) ;
2018-07-31 11:35:23 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2021-08-20 09:19:44 -07:00
const boxUpdateInfo = updateChecker . getUpdateInfo ( ) . box ;
if ( ! boxUpdateInfo ) throw new BoxError ( BoxError . NOT _FOUND , 'No update available' ) ;
if ( ! boxUpdateInfo . sourceTarballUrl ) throw new BoxError ( BoxError . BAD _STATE , 'No automatic update available' ) ;
2023-09-09 20:46:24 +05:30
if ( semver . gte ( constants . VERSION , boxUpdateInfo . version ) ) throw new BoxError ( BoxError . NOT _FOUND , 'No update available' ) ; // can happen after update completed or hotfix
2018-07-31 11:35:23 -07:00
2023-09-09 20:46:24 +05:30
await checkUpdateRequirements ( boxUpdateInfo ) ;
2018-12-04 14:04:43 -08:00
2021-08-20 09:19:44 -07:00
const error = locker . lock ( locker . OP _BOX _UPDATE ) ;
if ( error ) throw new BoxError ( BoxError . BAD _STATE , ` Cannot update now: ${ error . message } ` ) ;
2018-12-09 03:20:00 -08:00
2023-08-04 11:24:28 +05:30
const backupConfig = await backups . getConfig ( ) ;
2023-07-13 11:50:57 +05:30
const memoryLimit = backupConfig . limits ? . memoryLimit ? Math . max ( backupConfig . limits . memoryLimit / 1024 / 1024 , 400 ) : 400 ;
2019-10-14 09:30:20 -07:00
2023-08-04 11:24:28 +05:30
const taskId = await tasks . add ( tasks . TASK _UPDATE , [ boxUpdateInfo , options ] ) ;
2022-02-24 20:04:46 -08:00
await eventlog . add ( eventlog . ACTION _UPDATE , auditSource , { taskId , boxUpdateInfo } ) ;
2019-08-27 22:39:59 -07:00
2022-02-24 20:04:46 -08:00
tasks . startTask ( taskId , { timeout : 20 * 60 * 60 * 1000 /* 20 hours */ , nice : 15 , memoryLimit } , async ( error ) => {
2021-08-20 09:19:44 -07:00
locker . unlock ( locker . OP _BOX _UPDATE ) ;
2021-02-01 14:07:23 -08:00
2023-04-16 10:49:59 +02:00
debug ( 'Update failed with error. %o' , error ) ;
2021-07-12 23:35:30 -07:00
2021-08-20 09:19:44 -07:00
const timedOut = error . code === tasks . ETIMEOUT ;
2022-02-24 20:04:46 -08:00
await safe ( eventlog . add ( eventlog . ACTION _UPDATE _FINISH , auditSource , { taskId , errorMessage : error . message , timedOut } ) ) ;
2018-12-09 03:20:00 -08:00
} ) ;
2021-08-20 09:19:44 -07:00
return taskId ;
2018-07-31 11:35:23 -07:00
}
2023-08-12 19:28:07 +05:30
async function notifyUpdate ( ) {
const version = safe . fs . readFileSync ( paths . VERSION _FILE , 'utf8' ) ;
if ( version === constants . VERSION ) return ;
if ( ! version ) {
await eventlog . add ( eventlog . ACTION _INSTALL _FINISH , AuditSource . CRON , { version : constants . VERSION } ) ;
} else {
await eventlog . add ( eventlog . ACTION _UPDATE _FINISH , AuditSource . CRON , { errorMessage : '' , oldVersion : version || 'dev' , newVersion : constants . VERSION } ) ;
const [ error ] = await safe ( tasks . setCompletedByType ( tasks . TASK _UPDATE , { error : null } ) ) ;
if ( error && error . reason !== BoxError . NOT _FOUND ) throw error ; // when hotfixing, task may not exist
}
safe . fs . writeFileSync ( paths . VERSION _FILE , constants . VERSION , 'utf8' ) ;
}