2015-07-20 00:09:47 -07:00
'use strict' ;
exports = module . exports = {
2020-10-27 17:11:50 -07:00
hasAccessTo ,
removeInternalFields ,
removeRestrictedFields ,
get ,
getByIpAddress ,
getByFqdn ,
getAll ,
getAllByUser ,
install ,
uninstall ,
setAccessRestriction ,
setLabel ,
setIcon ,
setTags ,
setMemoryLimit ,
setCpuShares ,
2020-10-28 19:42:48 -07:00
setMounts ,
2020-10-27 17:11:50 -07:00
setAutomaticBackup ,
setAutomaticUpdate ,
setReverseProxyConfig ,
setCertificate ,
setDebugMode ,
setEnvironment ,
setMailbox ,
setLocation ,
setDataDir ,
repair ,
restore ,
importApp ,
2020-12-06 19:38:50 -08:00
exportApp ,
2020-10-27 17:11:50 -07:00
clone ,
update ,
backup ,
listBackups ,
getLocalLogfilePaths ,
getLogs ,
2021-05-07 21:37:23 -07:00
getCertificate ,
2020-10-27 17:11:50 -07:00
start ,
stop ,
restart ,
exec ,
checkManifestConstraints ,
downloadManifest ,
canAutoupdateApp ,
autoupdateApps ,
restoreInstalledApps ,
configureInstalledApps ,
schedulePendingTasks ,
restartAppsUsingAddons ,
getDataDir ,
2021-04-30 13:18:15 -07:00
getIcon ,
2021-01-20 12:12:14 -08:00
getMemoryLimit ,
2020-10-27 17:11:50 -07:00
downloadFile ,
uploadFile ,
2017-08-18 20:45:52 -07:00
2021-05-25 21:31:48 -07:00
backupConfig ,
2021-05-26 09:48:34 -07:00
restoreConfig ,
2021-05-25 21:31:48 -07:00
2018-08-12 22:08:19 -07:00
PORT _TYPE _TCP : 'tcp' ,
2018-08-13 08:33:09 -07:00
PORT _TYPE _UDP : 'udp' ,
2018-08-12 22:08:19 -07:00
2019-09-22 00:20:12 -07:00
// task codes - the installation state is now a misnomer (keep in sync in UI)
2019-08-30 13:12:49 -07:00
ISTATE _PENDING _INSTALL : 'pending_install' , // installs and fresh reinstalls
ISTATE _PENDING _CLONE : 'pending_clone' , // clone
2019-09-19 17:04:11 -07:00
ISTATE _PENDING _CONFIGURE : 'pending_configure' , // infra update
2019-09-10 14:25:12 -07:00
ISTATE _PENDING _RECREATE _CONTAINER : 'pending_recreate_container' , // env change or addon change
2019-09-08 16:57:08 -07:00
ISTATE _PENDING _LOCATION _CHANGE : 'pending_location_change' ,
ISTATE _PENDING _DATA _DIR _MIGRATION : 'pending_data_dir_migration' ,
2019-09-10 14:25:12 -07:00
ISTATE _PENDING _RESIZE : 'pending_resize' ,
ISTATE _PENDING _DEBUG : 'pending_debug' ,
2019-08-30 13:12:49 -07:00
ISTATE _PENDING _UNINSTALL : 'pending_uninstall' , // uninstallation
ISTATE _PENDING _RESTORE : 'pending_restore' , // restore to previous backup or on upgrade
2021-05-26 09:27:15 -07:00
ISTATE _PENDING _IMPORT : 'pending_import' , // import from external backup
2019-08-30 13:12:49 -07:00
ISTATE _PENDING _UPDATE : 'pending_update' , // update from installed state preserving data
ISTATE _PENDING _BACKUP : 'pending_backup' , // backup the app. this is state because it blocks other operations
2019-09-22 00:20:12 -07:00
ISTATE _PENDING _START : 'pending_start' ,
ISTATE _PENDING _STOP : 'pending_stop' ,
2019-12-20 10:29:29 -08:00
ISTATE _PENDING _RESTART : 'pending_restart' ,
2019-08-30 13:12:49 -07:00
ISTATE _ERROR : 'error' , // error executing last pending_* command
ISTATE _INSTALLED : 'installed' , // app is installed
// run states
RSTATE _RUNNING : 'running' ,
RSTATE _STOPPED : 'stopped' , // app stopped by us
// health states (keep in sync in UI)
HEALTH _HEALTHY : 'healthy' ,
HEALTH _UNHEALTHY : 'unhealthy' ,
HEALTH _ERROR : 'error' ,
HEALTH _DEAD : 'dead' ,
2015-07-20 00:09:47 -07:00
// exported for testing
2015-10-15 12:26:48 +02:00
_validatePortBindings : validatePortBindings ,
2018-08-12 19:33:11 -07:00
_validateAccessRestriction : validateAccessRestriction ,
2019-08-20 11:45:00 -07:00
_translatePortBindings : translatePortBindings ,
2015-07-20 00:09:47 -07:00
} ;
2021-05-03 22:55:43 -07:00
const appdb = require ( './appdb.js' ) ,
2017-04-13 00:42:44 -07:00
appstore = require ( './appstore.js' ) ,
2019-08-28 15:00:55 -07:00
appTaskManager = require ( './apptaskmanager.js' ) ,
2015-07-20 00:09:47 -07:00
assert = require ( 'assert' ) ,
async = require ( 'async' ) ,
backups = require ( './backups.js' ) ,
2019-09-19 23:13:04 -07:00
BoxError = require ( './boxerror.js' ) ,
2015-07-20 00:09:47 -07:00
constants = require ( './constants.js' ) ,
2021-05-07 21:37:23 -07:00
database = require ( './database.js' ) ,
2015-07-20 00:09:47 -07:00
debug = require ( 'debug' ) ( 'box:apps' ) ,
2015-11-10 12:47:48 -08:00
docker = require ( './docker.js' ) ,
2018-01-09 21:03:59 -08:00
domaindb = require ( './domaindb.js' ) ,
domains = require ( './domains.js' ) ,
2016-05-01 21:37:08 -07:00
eventlog = require ( './eventlog.js' ) ,
2015-07-20 00:09:47 -07:00
fs = require ( 'fs' ) ,
2018-05-24 16:25:32 -07:00
mail = require ( './mail.js' ) ,
2015-07-20 00:09:47 -07:00
manifestFormat = require ( 'cloudron-manifestformat' ) ,
2019-03-04 12:28:27 -08:00
once = require ( 'once' ) ,
2017-11-07 09:09:30 -08:00
os = require ( 'os' ) ,
2015-07-20 00:09:47 -07:00
path = require ( 'path' ) ,
paths = require ( './paths.js' ) ,
2018-01-30 12:23:27 -08:00
reverseProxy = require ( './reverseproxy.js' ) ,
2015-07-20 00:09:47 -07:00
safe = require ( 'safetydance' ) ,
semver = require ( 'semver' ) ,
2021-06-24 16:19:30 -07:00
services = require ( './services.js' ) ,
2019-07-26 10:49:29 -07:00
settings = require ( './settings.js' ) ,
2015-11-02 11:20:50 -08:00
spawn = require ( 'child_process' ) . spawn ,
2015-07-20 00:09:47 -07:00
split = require ( 'split' ) ,
superagent = require ( 'superagent' ) ,
2019-08-26 15:55:57 -07:00
tasks = require ( './tasks.js' ) ,
2017-08-20 23:39:49 -07:00
TransformStream = require ( 'stream' ) . Transform ,
2020-02-21 12:17:06 -08:00
users = require ( './users.js' ) ,
2015-07-20 00:09:47 -07:00
util = require ( 'util' ) ,
2017-08-13 17:44:31 -07:00
uuid = require ( 'uuid' ) ,
2018-04-29 10:47:34 -07:00
validator = require ( 'validator' ) ,
_ = require ( 'underscore' ) ;
2015-07-20 00:09:47 -07:00
2019-08-27 22:39:59 -07:00
const NOOP _CALLBACK = function ( error ) { if ( error ) debug ( error ) ; } ;
2015-07-20 00:09:47 -07:00
// validate the port bindings
2018-08-12 22:37:36 -07:00
function validatePortBindings ( portBindings , manifest ) {
2017-01-29 13:01:09 -08:00
assert . strictEqual ( typeof portBindings , 'object' ) ;
2018-08-12 22:37:36 -07:00
assert . strictEqual ( typeof manifest , 'object' ) ;
2017-01-29 13:01:09 -08:00
2018-06-04 21:24:14 +02:00
// keep the public ports in sync with firewall rules in setup/start/cloudron-firewall.sh
2015-07-20 00:09:47 -07:00
// these ports are reserved even if we listen only on 127.0.0.1 because we setup HostIp to be 127.0.0.1
// for custom tcp ports
2020-03-30 10:01:52 +02:00
const RESERVED _PORTS = [
2017-01-29 12:38:54 -08:00
22 , /* ssh */
2015-07-20 00:09:47 -07:00
25 , /* smtp */
80 , /* http */
2016-05-05 15:00:07 -07:00
143 , /* imap */
2019-05-08 17:30:41 -07:00
202 , /* alternate ssh */
2019-03-19 22:59:29 -07:00
222 , /* proftd */
2015-07-20 00:09:47 -07:00
443 , /* https */
2016-05-05 15:00:07 -07:00
465 , /* smtps */
587 , /* submission */
993 , /* imaps */
2015-07-20 00:09:47 -07:00
2003 , /* graphite (lo) */
2004 , /* graphite (lo) */
2018-06-04 21:12:55 +02:00
2514 , /* cloudron-syslog (lo) */
2019-07-25 15:43:51 -07:00
constants . PORT , /* app server (lo) */
2020-11-09 20:34:48 -08:00
constants . AUTHWALL _PORT , /* protected sites */
2019-07-25 15:27:28 -07:00
constants . INTERNAL _SMTP _PORT , /* internal smtp port (lo) */
2019-07-25 15:33:34 -07:00
constants . LDAP _PORT ,
2015-07-20 00:09:47 -07:00
3306 , /* mysql (lo) */
2020-03-30 08:30:01 +02:00
3478 , /* turn,stun */
2016-05-13 18:48:05 -07:00
4190 , /* managesieve */
2020-03-30 08:30:01 +02:00
5349 , /* turn,stun TLS */
2018-11-16 19:23:09 -08:00
8000 , /* ESXi monitoring */
8417 , /* graphite (lo) */
2015-07-20 00:09:47 -07:00
] ;
2020-03-30 10:01:52 +02:00
const RESERVED _PORT _RANGES = [
[ 50000 , 51000 ] /* turn udp ports */
] ;
2021-02-17 13:11:00 -08:00
const ALLOWED _PORTS = [
53 , // dns 53 is special and adblocker apps can use them
853 // dns over tls
] ;
2015-07-20 00:09:47 -07:00
if ( ! portBindings ) return null ;
2018-08-12 19:33:11 -07:00
for ( let portName in portBindings ) {
2019-10-24 10:39:47 -07:00
if ( ! /^[a-zA-Z0-9_]+$/ . test ( portName ) ) return new BoxError ( BoxError . BAD _FIELD , ` ${ portName } is not a valid environment variable ` , { field : 'portBindings' , portName : portName } ) ;
2015-07-20 00:09:47 -07:00
2018-08-12 19:33:11 -07:00
const hostPort = portBindings [ portName ] ;
2019-10-24 10:39:47 -07:00
if ( ! Number . isInteger ( hostPort ) ) return new BoxError ( BoxError . BAD _FIELD , ` ${ hostPort } is not an integer ` , { field : 'portBindings' , portName : portName } ) ;
if ( RESERVED _PORTS . indexOf ( hostPort ) !== - 1 ) return new BoxError ( BoxError . BAD _FIELD , ` Port ${ hostPort } is reserved. ` , { field : 'portBindings' , portName : portName } ) ;
2020-03-30 10:01:52 +02:00
if ( RESERVED _PORT _RANGES . find ( range => ( hostPort >= range [ 0 ] && hostPort <= range [ 1 ] ) ) ) return new BoxError ( BoxError . BAD _FIELD , ` Port ${ hostPort } is reserved. ` , { field : 'portBindings' , portName : portName } ) ;
2021-02-17 13:11:00 -08:00
if ( ALLOWED _PORTS . indexOf ( hostPort ) === - 1 && ( hostPort <= 1023 || hostPort > 65535 ) ) return new BoxError ( BoxError . BAD _FIELD , ` ${ hostPort } is not in permitted range ` , { field : 'portBindings' , portName : portName } ) ;
2015-07-20 00:09:47 -07:00
}
// it is OK if there is no 1-1 mapping between values in manifest.tcpPorts and portBindings. missing values implies
// that the user wants the service disabled
2018-08-12 22:37:36 -07:00
const tcpPorts = manifest . tcpPorts || { } ;
2018-08-12 22:47:59 -07:00
const udpPorts = manifest . udpPorts || { } ;
2018-08-12 19:33:11 -07:00
for ( let portName in portBindings ) {
2019-10-24 10:39:47 -07:00
if ( ! ( portName in tcpPorts ) && ! ( portName in udpPorts ) ) return new BoxError ( BoxError . BAD _FIELD , ` Invalid portBindings ${ portName } ` , { field : 'portBindings' , portName : portName } ) ;
2015-07-20 00:09:47 -07:00
}
return null ;
}
2018-08-13 08:33:09 -07:00
function translatePortBindings ( portBindings , manifest ) {
assert . strictEqual ( typeof portBindings , 'object' ) ;
assert . strictEqual ( typeof manifest , 'object' ) ;
2018-08-12 19:33:11 -07:00
if ( ! portBindings ) return null ;
let result = { } ;
2018-08-13 08:33:09 -07:00
const tcpPorts = manifest . tcpPorts || { } ;
2018-08-12 19:33:11 -07:00
for ( let portName in portBindings ) {
2018-08-13 08:33:09 -07:00
const portType = portName in tcpPorts ? exports . PORT _TYPE _TCP : exports . PORT _TYPE _UDP ;
result [ portName ] = { hostPort : portBindings [ portName ] , type : portType } ;
2018-08-12 19:33:11 -07:00
}
return result ;
}
2015-10-15 12:26:48 +02:00
function validateAccessRestriction ( accessRestriction ) {
2015-10-16 15:11:54 +02:00
assert . strictEqual ( typeof accessRestriction , 'object' ) ;
2015-10-15 12:26:48 +02:00
2015-10-16 15:11:54 +02:00
if ( accessRestriction === null ) return null ;
2015-10-15 12:26:48 +02:00
2016-02-09 13:03:52 -08:00
if ( accessRestriction . users ) {
2019-10-24 10:39:47 -07:00
if ( ! Array . isArray ( accessRestriction . users ) ) return new BoxError ( BoxError . BAD _FIELD , 'users array property required' ) ;
if ( ! accessRestriction . users . every ( function ( e ) { return typeof e === 'string' ; } ) ) return new BoxError ( BoxError . BAD _FIELD , 'All users have to be strings' ) ;
2016-02-09 13:03:52 -08:00
}
if ( accessRestriction . groups ) {
2019-10-24 10:39:47 -07:00
if ( ! Array . isArray ( accessRestriction . groups ) ) return new BoxError ( BoxError . BAD _FIELD , 'groups array property required' ) ;
if ( ! accessRestriction . groups . every ( function ( e ) { return typeof e === 'string' ; } ) ) return new BoxError ( BoxError . BAD _FIELD , 'All groups have to be strings' ) ;
2016-02-09 13:03:52 -08:00
}
2016-06-04 13:20:10 -07:00
// TODO: maybe validate if the users and groups actually exist
2015-10-15 12:26:48 +02:00
return null ;
}
2016-02-11 17:39:15 +01:00
function validateMemoryLimit ( manifest , memoryLimit ) {
assert . strictEqual ( typeof manifest , 'object' ) ;
assert . strictEqual ( typeof memoryLimit , 'number' ) ;
2016-02-14 12:13:49 +01:00
var min = manifest . memoryLimit || constants . DEFAULT _MEMORY _LIMIT ;
2017-11-07 09:09:30 -08:00
var max = os . totalmem ( ) * 2 ; // this will overallocate since we don't allocate equal swap always (#466)
2016-02-11 17:39:15 +01:00
// allow 0, which indicates that it is not set, the one from the manifest will be choosen but we don't commit any user value
// this is needed so an app update can change the value in the manifest, and if not set by the user, the new value should be used
if ( memoryLimit === 0 ) return null ;
2017-01-19 15:02:12 -08:00
// a special value that indicates unlimited memory
if ( memoryLimit === - 1 ) return null ;
2019-10-24 10:39:47 -07:00
if ( memoryLimit < min ) return new BoxError ( BoxError . BAD _FIELD , 'memoryLimit too small' ) ;
if ( memoryLimit > max ) return new BoxError ( BoxError . BAD _FIELD , 'memoryLimit too large' ) ;
2016-02-11 17:39:15 +01:00
return null ;
}
2020-01-28 21:30:35 -08:00
function validateCpuShares ( cpuShares ) {
assert . strictEqual ( typeof cpuShares , 'number' ) ;
2020-01-28 22:38:54 -08:00
if ( cpuShares < 2 || cpuShares > 1024 ) return new BoxError ( BoxError . BAD _FIELD , 'cpuShares has to be between 2 and 1024' ) ;
2020-01-28 21:30:35 -08:00
return null ;
}
2017-01-20 05:48:25 -08:00
function validateDebugMode ( debugMode ) {
assert . strictEqual ( typeof debugMode , 'object' ) ;
if ( debugMode === null ) return null ;
2019-10-24 10:39:47 -07:00
if ( 'cmd' in debugMode && debugMode . cmd !== null && ! Array . isArray ( debugMode . cmd ) ) return new BoxError ( BoxError . BAD _FIELD , 'debugMode.cmd must be an array or null' ) ;
if ( 'readonlyRootfs' in debugMode && typeof debugMode . readonlyRootfs !== 'boolean' ) return new BoxError ( BoxError . BAD _FIELD , 'debugMode.readonlyRootfs must be a boolean' ) ;
2017-01-20 05:48:25 -08:00
return null ;
}
2017-07-14 12:19:27 -05:00
function validateRobotsTxt ( robotsTxt ) {
if ( robotsTxt === null ) return null ;
2017-07-23 18:55:31 -07:00
// this is the nginx limit on inline strings. if we really hit this, we have to generate a file
2019-10-24 10:39:47 -07:00
if ( robotsTxt . length > 4096 ) return new BoxError ( BoxError . BAD _FIELD , 'robotsTxt must be less than 4096' , { field : 'robotsTxt' } ) ;
2017-07-23 18:55:31 -07:00
// TODO: validate the robots file? we escape the string when templating the nginx config right now
2017-07-14 12:19:27 -05:00
return null ;
}
2019-10-14 16:59:22 -07:00
function validateCsp ( csp ) {
if ( csp === null ) return null ;
2019-10-13 18:22:03 -07:00
2019-10-24 10:39:47 -07:00
if ( csp . length > 4096 ) return new BoxError ( BoxError . BAD _FIELD , 'CSP must be less than 4096' , { field : 'csp' } ) ;
2019-10-13 18:22:03 -07:00
2019-10-24 10:39:47 -07:00
if ( csp . includes ( '"' ) ) return new BoxError ( BoxError . BAD _FIELD , 'CSP cannot contains double quotes' , { field : 'csp' } ) ;
2019-10-13 18:22:03 -07:00
return null ;
}
2019-10-11 20:30:30 -07:00
function validateBackupFormat ( format ) {
assert . strictEqual ( typeof format , 'string' ) ;
if ( format === 'tgz' || format == 'rsync' ) return null ;
2019-10-24 10:39:47 -07:00
return new BoxError ( BoxError . BAD _FIELD , 'Invalid backup format' ) ;
2019-10-11 20:30:30 -07:00
}
2019-03-22 07:48:31 -07:00
function validateLabel ( label ) {
if ( label === null ) return null ;
2019-10-24 10:39:47 -07:00
if ( label . length > 128 ) return new BoxError ( BoxError . BAD _FIELD , 'label must be less than 128' , { field : 'label' } ) ;
2019-03-22 07:48:31 -07:00
return null ;
}
function validateTags ( tags ) {
2019-10-24 10:39:47 -07:00
if ( tags . length > 64 ) return new BoxError ( BoxError . BAD _FIELD , 'Can only set up to 64 tags' , { field : 'tags' } ) ;
2019-03-22 07:48:31 -07:00
2019-10-24 10:39:47 -07:00
if ( tags . some ( tag => tag . length == 0 ) ) return new BoxError ( BoxError . BAD _FIELD , 'tag cannot be empty' , { field : 'tags' } ) ;
if ( tags . some ( tag => tag . length > 128 ) ) return new BoxError ( BoxError . BAD _FIELD , 'tag must be less than 128' , { field : 'tags' } ) ;
2019-03-22 07:48:31 -07:00
return null ;
}
2018-10-18 11:19:32 -07:00
function validateEnv ( env ) {
for ( let key in env ) {
2019-10-24 10:39:47 -07:00
if ( key . length > 512 ) return new BoxError ( BoxError . BAD _FIELD , 'Max env var key length is 512' , { field : 'env' , env : env } ) ;
2018-10-18 11:19:32 -07:00
// http://pubs.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap08.html
2019-10-24 10:39:47 -07:00
if ( ! /^[a-zA-Z_][a-zA-Z0-9_]*$/ . test ( key ) ) return new BoxError ( BoxError . BAD _FIELD , ` " ${ key } " is not a valid environment variable ` , { field : 'env' , env : env } ) ;
2018-10-18 11:19:32 -07:00
}
return null ;
}
2018-12-20 14:33:29 -08:00
function validateDataDir ( dataDir ) {
2019-09-09 16:37:59 -07:00
if ( dataDir === null ) return null ;
2018-12-20 14:33:29 -08:00
2020-01-28 14:09:31 -08:00
if ( ! path . isAbsolute ( dataDir ) ) return new BoxError ( BoxError . BAD _FIELD , ` ${ dataDir } is not an absolute path ` , { field : 'dataDir' } ) ;
if ( dataDir . endsWith ( '/' ) ) return new BoxError ( BoxError . BAD _FIELD , ` ${ dataDir } contains trailing slash ` , { field : 'dataDir' } ) ;
if ( path . normalize ( dataDir ) !== dataDir ) return new BoxError ( BoxError . BAD _FIELD , ` ${ dataDir } is not a normalized path ` , { field : 'dataDir' } ) ;
2018-12-20 14:33:29 -08:00
// nfs shares will have the directory mounted already
let stat = safe . fs . lstatSync ( dataDir ) ;
if ( stat ) {
2020-01-28 14:09:31 -08:00
if ( ! stat . isDirectory ( ) ) return new BoxError ( BoxError . BAD _FIELD , ` ${ dataDir } is not a directory ` , { field : 'dataDir' } ) ;
2018-12-20 14:33:29 -08:00
let entries = safe . fs . readdirSync ( dataDir ) ;
2020-01-28 14:09:31 -08:00
if ( ! entries ) return new BoxError ( BoxError . BAD _FIELD , ` ${ dataDir } could not be listed ` , { field : 'dataDir' } ) ;
if ( entries . length !== 0 ) return new BoxError ( BoxError . BAD _FIELD , ` ${ dataDir } is not empty. If this is the root of a mounted volume, provide a subdirectory. ` , { field : 'dataDir' } ) ;
2018-12-20 14:33:29 -08:00
}
2019-01-19 22:19:43 -08:00
// backup logic relies on paths not overlapping (because it recurses)
2020-01-28 14:09:31 -08:00
if ( dataDir . startsWith ( paths . APPS _DATA _DIR ) ) return new BoxError ( BoxError . BAD _FIELD , ` ${ dataDir } cannot be inside apps data ` , { field : 'dataDir' } ) ;
2018-12-20 14:33:29 -08:00
2019-01-19 22:19:43 -08:00
// if we made it this far, it cannot start with any of these realistically
const fhs = [ '/bin' , '/boot' , '/etc' , '/lib' , '/lib32' , '/lib64' , '/proc' , '/run' , '/sbin' , '/tmp' , '/usr' ] ;
2020-01-28 14:09:31 -08:00
if ( fhs . some ( ( p ) => dataDir . startsWith ( p ) ) ) return new BoxError ( BoxError . BAD _FIELD , ` ${ dataDir } cannot be placed inside this location ` , { field : 'dataDir' } ) ;
2019-01-19 22:19:43 -08:00
2018-12-20 14:33:29 -08:00
return null ;
}
2019-09-27 10:25:26 -07:00
function getDuplicateErrorDetails ( errorMessage , locations , domainObjectMap , portBindings ) {
2019-08-29 12:14:42 -07:00
assert . strictEqual ( typeof errorMessage , 'string' ) ;
2019-09-27 10:25:26 -07:00
assert ( Array . isArray ( locations ) ) ;
assert . strictEqual ( typeof domainObjectMap , 'object' ) ;
2015-07-20 00:09:47 -07:00
assert . strictEqual ( typeof portBindings , 'object' ) ;
2019-08-29 12:14:42 -07:00
var match = errorMessage . match ( /ER_DUP_ENTRY: Duplicate entry '(.*)' for key '(.*)'/ ) ;
2015-07-20 00:09:47 -07:00
if ( ! match ) {
2019-08-29 12:14:42 -07:00
debug ( 'Unexpected SQL error message.' , errorMessage ) ;
2019-10-24 18:32:33 -07:00
return new BoxError ( BoxError . DATABASE _ERROR , errorMessage ) ;
2015-07-20 00:09:47 -07:00
}
2019-09-27 10:25:26 -07:00
// check if a location conflicts
2019-03-19 20:43:42 -07:00
if ( match [ 2 ] === 'subdomain' ) {
2019-09-27 10:25:26 -07:00
for ( let i = 0 ; i < locations . length ; i ++ ) {
const { subdomain , domain } = locations [ i ] ;
if ( match [ 1 ] !== ` ${ subdomain } - ${ domain } ` ) continue ;
2019-06-05 16:01:44 +02:00
2019-10-24 10:39:47 -07:00
return new BoxError ( BoxError . ALREADY _EXISTS , ` Domain ' ${ domains . fqdn ( subdomain , domainObjectMap [ domain ] ) } ' is in use ` , { subdomain , domain } ) ;
2019-09-01 21:34:27 -07:00
}
2019-03-19 20:43:42 -07:00
}
2015-07-20 00:09:47 -07:00
// check if any of the port bindings conflict
2018-08-12 19:33:11 -07:00
for ( let portName in portBindings ) {
2020-12-03 22:27:59 -08:00
if ( portBindings [ portName ] === parseInt ( match [ 1 ] ) ) return new BoxError ( BoxError . ALREADY _EXISTS , ` Port ${ match [ 1 ] } is in use ` , { portName } ) ;
2015-07-20 00:09:47 -07:00
}
2019-09-03 15:17:48 -07:00
if ( match [ 2 ] === 'dataDir' ) {
2019-10-24 10:39:47 -07:00
return new BoxError ( BoxError . BAD _FIELD , ` Data directory ${ match [ 1 ] } is in use ` , { field : 'dataDir' } ) ;
2019-09-03 15:17:48 -07:00
}
2019-10-24 10:39:47 -07:00
return new BoxError ( BoxError . ALREADY _EXISTS , ` ${ match [ 2 ] } ' ${ match [ 1 ] } ' is in use ` ) ;
2015-07-20 00:09:47 -07:00
}
2018-12-20 14:33:29 -08:00
function getDataDir ( app , dataDir ) {
2019-09-15 21:51:38 -07:00
assert ( dataDir === null || typeof dataDir === 'string' ) ;
2018-12-20 14:33:29 -08:00
return dataDir || path . join ( paths . APPS _DATA _DIR , app . id , 'data' ) ;
}
2018-06-25 16:40:16 -07:00
function removeInternalFields ( app ) {
2018-05-24 15:27:55 -07:00
return _ . pick ( app ,
2019-08-30 11:18:42 -07:00
'id' , 'appStoreId' , 'installationState' , 'error' , 'runState' , 'health' , 'taskId' ,
2019-11-14 21:43:14 -08:00
'location' , 'domain' , 'fqdn' , 'mailboxName' , 'mailboxDomain' ,
2020-01-28 21:30:35 -08:00
'accessRestriction' , 'manifest' , 'portBindings' , 'iconUrl' , 'memoryLimit' , 'cpuShares' ,
2019-10-14 15:57:41 -07:00
'sso' , 'debugMode' , 'reverseProxyConfig' , 'enableBackup' , 'creationTime' , 'updateTime' , 'ts' , 'tags' ,
2021-03-16 22:38:59 -07:00
'label' , 'alternateDomains' , 'aliasDomains' , 'env' , 'enableAutomaticUpdate' , 'dataDir' , 'mounts' , 'enableMailbox' ) ;
2018-04-29 10:47:34 -07:00
}
2019-02-11 14:37:49 -08:00
// non-admins can only see these
2018-06-25 16:45:15 -07:00
function removeRestrictedFields ( app ) {
return _ . pick ( app ,
2021-02-16 20:08:41 +01:00
'id' , 'appStoreId' , 'installationState' , 'error' , 'runState' , 'health' , 'taskId' , 'accessRestriction' , 'alternateDomains' , 'aliasDomains' , 'sso' ,
2020-06-25 12:31:00 +02:00
'location' , 'domain' , 'fqdn' , 'manifest' , 'portBindings' , 'iconUrl' , 'creationTime' , 'ts' , 'tags' , 'label' , 'enableBackup' ) ;
2018-06-25 16:45:15 -07:00
}
2021-04-30 13:18:15 -07:00
function getIcon ( app , options , callback ) {
2020-03-29 17:11:10 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2019-09-01 17:44:24 -07:00
assert . strictEqual ( typeof options , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2021-04-30 13:18:15 -07:00
appdb . getIcons ( app . id , function ( error , icons ) {
if ( error ) return callback ( error ) ;
2020-03-29 17:11:10 -07:00
2021-04-30 13:18:15 -07:00
if ( ! options . original && icons . icon ) return callback ( null , icons . icon ) ;
2019-09-01 17:44:24 -07:00
2021-04-30 13:18:15 -07:00
if ( icons . appStoreIcon ) return callback ( null , icons . appStoreIcon ) ;
2019-09-01 17:44:24 -07:00
2021-04-30 13:18:15 -07:00
callback ( new BoxError ( BoxError . NOT _FOUND , 'No icon' ) ) ;
} ) ;
2019-09-01 17:44:24 -07:00
}
2021-01-20 12:12:14 -08:00
function getMemoryLimit ( app ) {
assert . strictEqual ( typeof app , 'object' ) ;
let memoryLimit = app . memoryLimit || app . manifest . memoryLimit || 0 ;
if ( memoryLimit === - 1 ) { // unrestricted
memoryLimit = 0 ;
} else if ( memoryLimit === 0 || memoryLimit < constants . DEFAULT _MEMORY _LIMIT ) { // ensure we never go below minimum (in case we change the default)
memoryLimit = constants . DEFAULT _MEMORY _LIMIT ;
}
return memoryLimit ;
}
2019-03-06 11:12:39 -08:00
function postProcess ( app , domainObjectMap ) {
let result = { } ;
for ( let portName in app . portBindings ) {
result [ portName ] = app . portBindings [ portName ] . hostPort ;
}
app . portBindings = result ;
2021-04-30 13:18:15 -07:00
app . iconUrl = app . hasIcon || app . hasAppStoreIcon ? ` /api/v1/apps/ ${ app . id } /icon ` : null ;
2019-03-06 11:12:39 -08:00
app . fqdn = domains . fqdn ( app . location , domainObjectMap [ app . domain ] ) ;
app . alternateDomains . forEach ( function ( ad ) { ad . fqdn = domains . fqdn ( ad . subdomain , domainObjectMap [ ad . domain ] ) ; } ) ;
2021-01-18 17:26:26 -08:00
app . aliasDomains . forEach ( function ( ad ) { ad . fqdn = domains . fqdn ( ad . subdomain , domainObjectMap [ ad . domain ] ) ; } ) ;
2019-03-06 11:12:39 -08:00
}
2016-02-09 12:48:21 -08:00
function hasAccessTo ( app , user , callback ) {
2015-10-15 15:06:34 +02:00
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof user , 'object' ) ;
2016-02-09 12:48:21 -08:00
assert . strictEqual ( typeof callback , 'function' ) ;
2015-10-15 15:06:34 +02:00
2016-02-09 12:48:21 -08:00
if ( app . accessRestriction === null ) return callback ( null , true ) ;
2016-02-09 13:03:52 -08:00
// check user access
if ( app . accessRestriction . users . some ( function ( e ) { return e === user . id ; } ) ) return callback ( null , true ) ;
2020-02-21 12:17:06 -08:00
if ( users . compareRoles ( user . role , users . ROLE _ADMIN ) >= 0 ) return callback ( null , true ) ; // admins can always access any app
2017-11-15 18:07:10 -08:00
2018-07-26 11:15:57 -07:00
if ( ! app . accessRestriction . groups ) return callback ( null , false ) ;
2017-11-15 18:07:10 -08:00
2021-07-09 13:25:27 +02:00
if ( app . accessRestriction . groups . some ( function ( gid ) { return Array . isArray ( user . groupIds ) && user . groupIds . indexOf ( gid ) !== - 1 ; } ) ) return callback ( null , true ) ;
2017-11-15 18:07:10 -08:00
2018-07-26 11:15:57 -07:00
callback ( null , false ) ;
2015-10-15 15:06:34 +02:00
}
2019-03-06 11:12:39 -08:00
function getDomainObjectMap ( callback ) {
2015-07-20 00:09:47 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2018-09-22 16:09:11 -07:00
domaindb . getAll ( function ( error , domainObjects ) {
2019-10-24 10:39:47 -07:00
if ( error ) return callback ( error ) ;
2015-07-20 00:09:47 -07:00
2019-03-06 11:12:39 -08:00
let domainObjectMap = { } ;
for ( let d of domainObjects ) { domainObjectMap [ d . domain ] = d ; }
callback ( null , domainObjectMap ) ;
} ) ;
}
function get ( appId , callback ) {
assert . strictEqual ( typeof appId , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
getDomainObjectMap ( function ( error , domainObjectMap ) {
if ( error ) return callback ( error ) ;
2018-09-22 16:09:11 -07:00
appdb . get ( appId , function ( error , app ) {
2019-10-24 11:13:48 -07:00
if ( error ) return callback ( error ) ;
2018-01-09 21:03:59 -08:00
2019-03-06 11:12:39 -08:00
postProcess ( app , domainObjectMap ) ;
2015-07-20 00:09:47 -07:00
2018-12-06 21:08:19 -08:00
callback ( null , app ) ;
2018-01-09 21:03:59 -08:00
} ) ;
2015-07-20 00:09:47 -07:00
} ) ;
}
2019-06-27 16:11:17 -07:00
// returns the app associated with this IP (app or scheduler)
2019-03-06 11:15:12 -08:00
function getByIpAddress ( ip , callback ) {
assert . strictEqual ( typeof ip , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2020-12-03 11:48:25 -08:00
appdb . getByIpAddress ( ip , function ( error , app ) {
if ( error ) return callback ( error ) ;
getDomainObjectMap ( function ( error , domainObjectMap ) {
if ( error ) return callback ( error ) ;
postProcess ( app , domainObjectMap ) ;
callback ( null , app ) ;
} ) ;
} ) ;
2019-03-06 11:15:12 -08:00
}
2019-03-19 16:23:03 -07:00
function getByFqdn ( fqdn , callback ) {
assert . strictEqual ( typeof fqdn , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
getAll ( function ( error , result ) {
if ( error ) return callback ( error ) ;
var app = result . find ( function ( a ) { return a . fqdn === fqdn ; } ) ;
2019-10-24 10:39:47 -07:00
if ( ! app ) return callback ( new BoxError ( BoxError . NOT _FOUND , 'No such app' ) ) ;
2019-03-19 16:23:03 -07:00
callback ( null , app ) ;
} ) ;
}
2015-07-20 00:09:47 -07:00
function getAll ( callback ) {
assert . strictEqual ( typeof callback , 'function' ) ;
2019-03-06 11:12:39 -08:00
getDomainObjectMap ( function ( error , domainObjectMap ) {
if ( error ) return callback ( error ) ;
2018-08-12 19:33:11 -07:00
2018-09-22 16:09:11 -07:00
appdb . getAll ( function ( error , apps ) {
2019-10-24 11:13:48 -07:00
if ( error ) return callback ( error ) ;
2018-01-09 21:03:59 -08:00
2019-03-06 11:12:39 -08:00
apps . forEach ( ( app ) => postProcess ( app , domainObjectMap ) ) ;
2015-07-20 00:09:47 -07:00
2019-03-06 11:12:39 -08:00
callback ( null , apps ) ;
2018-01-09 21:03:59 -08:00
} ) ;
2015-07-20 00:09:47 -07:00
} ) ;
}
2016-02-25 11:28:29 +01:00
function getAllByUser ( user , callback ) {
assert . strictEqual ( typeof user , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
getAll ( function ( error , result ) {
if ( error ) return callback ( error ) ;
2017-02-16 20:11:09 -08:00
async . filter ( result , function ( app , iteratorDone ) {
hasAccessTo ( app , user , iteratorDone ) ;
} , callback ) ;
2016-02-25 11:28:29 +01:00
} ) ;
}
2016-06-04 01:07:43 -07:00
function downloadManifest ( appStoreId , manifest , callback ) {
2019-10-24 10:39:47 -07:00
if ( ! appStoreId && ! manifest ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'Neither manifest nor appStoreId provided' ) ) ;
2016-06-07 15:36:45 -07:00
2016-06-04 01:07:43 -07:00
if ( ! appStoreId ) return callback ( null , '' , manifest ) ;
var parts = appStoreId . split ( '@' ) ;
2019-07-26 10:49:29 -07:00
var url = settings . apiServerOrigin ( ) + '/api/v1/apps/' + parts [ 0 ] + ( parts [ 1 ] ? '/versions/' + parts [ 1 ] : '' ) ;
2016-06-04 01:07:43 -07:00
debug ( 'downloading manifest from %s' , url ) ;
2016-09-12 12:53:51 -07:00
superagent . get ( url ) . timeout ( 30 * 1000 ) . end ( function ( error , result ) {
2019-10-24 10:39:47 -07:00
if ( error && ! error . response ) return callback ( new BoxError ( BoxError . EXTERNAL _ERROR , 'Network error downloading manifest:' + error . message ) ) ;
2016-06-04 01:07:43 -07:00
2019-10-24 10:39:47 -07:00
if ( result . statusCode !== 200 ) return callback ( new BoxError ( BoxError . NOT _FOUND , util . format ( 'Failed to get app info from store.' , result . statusCode , result . text ) ) ) ;
2016-06-04 01:07:43 -07:00
2020-05-11 14:43:08 -07:00
if ( ! result . body . manifest || typeof result . body . manifest !== 'object' ) return callback ( new BoxError ( BoxError . NOT _FOUND , util . format ( 'Missing manifest. Failed to get app info from store.' , result . statusCode , result . text ) ) ) ;
2016-06-04 01:07:43 -07:00
callback ( null , parts [ 0 ] , result . body . manifest ) ;
} ) ;
}
2018-06-09 11:05:54 -07:00
function mailboxNameForLocation ( location , manifest ) {
2019-11-14 21:43:14 -08:00
if ( location ) return ` ${ location } .app ` ;
if ( manifest . title ) return manifest . title . toLowerCase ( ) . replace ( /[^a-zA-Z0-9]/g , '' ) + '.app' ;
return 'noreply.app' ;
2018-06-09 11:05:54 -07:00
}
2019-09-24 00:07:20 -07:00
function scheduleTask ( appId , installationState , taskId , callback ) {
assert . strictEqual ( typeof appId , 'string' ) ;
assert . strictEqual ( typeof installationState , 'string' ) ;
2019-09-24 10:28:50 -07:00
assert . strictEqual ( typeof taskId , 'string' ) ;
2019-09-24 00:07:20 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2021-01-06 14:46:46 -08:00
settings . getBackupConfig ( function ( error , backupConfig ) {
if ( error ) return callback ( error ) ;
let memoryLimit = 400 ;
2021-05-26 09:27:15 -07:00
if ( installationState === exports . ISTATE _PENDING _BACKUP || installationState === exports . ISTATE _PENDING _CLONE || installationState === exports . ISTATE _PENDING _RESTORE
|| installationState === exports . ISTATE _PENDING _IMPORT || installationState === exports . ISTATE _PENDING _UPDATE ) {
2021-01-06 14:46:46 -08:00
memoryLimit = 'memoryLimit' in backupConfig ? Math . max ( backupConfig . memoryLimit / 1024 / 1024 , 400 ) : 400 ;
2019-09-24 00:07:20 -07:00
}
2021-01-06 14:46:46 -08:00
const options = { timeout : 20 * 60 * 60 * 1000 /* 20 hours */ , nice : 15 , memoryLimit } ;
appTaskManager . scheduleTask ( appId , taskId , options , function ( error ) {
debug ( ` scheduleTask: task ${ taskId } of ${ appId } completed ` ) ;
if ( error && ( error . code === tasks . ECRASHED || error . code === tasks . ESTOPPED ) ) { // if task crashed, update the error
debug ( ` Apptask crashed/stopped: ${ error . message } ` ) ;
let boxError = new BoxError ( BoxError . TASK _ERROR , error . message ) ;
boxError . details . crashed = error . code === tasks . ECRASHED ;
boxError . details . stopped = error . code === tasks . ESTOPPED ;
// see also apptask makeTaskError
boxError . details . taskId = taskId ;
boxError . details . installationState = installationState ;
2021-07-13 13:29:42 -07:00
appdb . update ( appId , { installationState : exports . ISTATE _ERROR , error : boxError . toPlainObject ( ) , taskId : null } , callback . bind ( null , error ) ) ;
2021-01-06 14:46:46 -08:00
} else if ( ! ( installationState === exports . ISTATE _PENDING _UNINSTALL && ! error ) ) { // clear out taskId except for successful uninstall
2021-07-13 13:29:42 -07:00
appdb . update ( appId , { taskId : null } , callback . bind ( null , error ) ) ;
2021-04-15 16:33:21 -07:00
} else {
2021-07-13 13:29:42 -07:00
callback ( error ) ;
2021-01-06 14:46:46 -08:00
}
} ) ;
2019-09-24 00:07:20 -07:00
} ) ;
}
function addTask ( appId , installationState , task , callback ) {
2019-08-28 15:00:55 -07:00
assert . strictEqual ( typeof appId , 'string' ) ;
2019-09-23 14:17:12 -07:00
assert . strictEqual ( typeof installationState , 'string' ) ;
assert . strictEqual ( typeof task , 'object' ) ; // { args, values }
2019-08-28 15:00:55 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2019-08-27 16:12:24 -07:00
2019-09-23 14:17:12 -07:00
const { args , values } = task ;
2019-12-06 11:29:33 -08:00
// TODO: match the SQL logic to match checkAppState. this means checking the error.installationState and installationState. Unfortunately, former is JSON right now
const requiredState = null ; // 'requiredState' in task ? task.requiredState : exports.ISTATE_INSTALLED;
2019-09-24 20:29:01 -07:00
const scheduleNow = 'scheduleNow' in task ? task . scheduleNow : true ;
const requireNullTaskId = 'requireNullTaskId' in task ? task . requireNullTaskId : true ;
2019-08-29 12:14:42 -07:00
2021-07-12 23:35:30 -07:00
const tasksAdd = util . callbackify ( tasks . add ) ;
tasksAdd ( tasks . TASK _APP , [ appId , args ] , function ( error , taskId ) {
2019-10-24 10:39:47 -07:00
if ( error ) return callback ( error ) ;
2019-08-27 22:39:59 -07:00
2019-09-24 20:29:01 -07:00
appdb . setTask ( appId , _ . extend ( { installationState , taskId , error : null } , values ) , { requiredState , requireNullTaskId } , function ( error ) {
2019-10-24 11:13:48 -07:00
if ( error && error . reason === BoxError . NOT _FOUND ) return callback ( new BoxError ( BoxError . BAD _STATE , 'Another task is scheduled for this app' ) ) ; // could be because app went away OR a taskId exists
if ( error ) return callback ( error ) ;
2019-08-26 15:55:57 -07:00
2021-04-19 14:22:22 -07:00
if ( scheduleNow ) scheduleTask ( appId , installationState , taskId , task . onFinished || NOOP _CALLBACK ) ;
2019-08-26 15:55:57 -07:00
2019-08-28 12:51:00 -07:00
callback ( null , { taskId } ) ;
2019-08-26 15:55:57 -07:00
} ) ;
} ) ;
}
2019-09-21 19:45:55 -07:00
function checkAppState ( app , state ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof state , 'string' ) ;
2019-12-05 16:31:11 -08:00
if ( app . taskId ) return new BoxError ( BoxError . BAD _STATE , ` Locked by task ${ app . taskId } : ${ app . installationState } / ${ app . runState } ` ) ;
2019-09-21 19:45:55 -07:00
2019-09-23 10:35:42 -07:00
if ( app . installationState === exports . ISTATE _ERROR ) {
2019-12-06 08:40:16 -08:00
// allow task to be called again if that was the errored task
2019-11-23 18:06:31 -08:00
if ( app . error . installationState === state ) return null ;
// allow uninstall from any state
2021-05-26 09:27:15 -07:00
if ( state !== exports . ISTATE _PENDING _UNINSTALL && state !== exports . ISTATE _PENDING _RESTORE && state !== exports . ISTATE _PENDING _IMPORT ) return new BoxError ( BoxError . BAD _STATE , 'Not allowed in error state' ) ;
2019-09-23 10:35:42 -07:00
}
2019-09-21 19:45:55 -07:00
2020-05-28 12:16:28 -07:00
if ( app . runState === exports . RSTATE _STOPPED ) {
// can't backup or restore since app addons are down. can't update because migration scripts won't run
2021-05-26 09:27:15 -07:00
if ( state === exports . ISTATE _PENDING _UPDATE || state === exports . ISTATE _PENDING _BACKUP || state === exports . ISTATE _PENDING _RESTORE || state === exports . ISTATE _PENDING _IMPORT ) return new BoxError ( BoxError . BAD _STATE , 'Not allowed in stopped state' ) ;
2020-05-28 12:16:28 -07:00
}
2019-09-21 19:45:55 -07:00
return null ;
}
2019-09-27 10:25:26 -07:00
function validateLocations ( locations , callback ) {
assert ( Array . isArray ( locations ) ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
getDomainObjectMap ( function ( error , domainObjectMap ) {
if ( error ) return callback ( error ) ;
for ( let location of locations ) {
2019-10-24 10:39:47 -07:00
if ( ! ( location . domain in domainObjectMap ) ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'No such domain' , { field : 'location' , domain : location . domain , subdomain : location . subdomain } ) ) ;
2019-09-27 10:25:26 -07:00
2021-01-18 22:47:53 -08:00
let subdomain = location . subdomain ;
if ( location . type === 'alias' && subdomain . startsWith ( '*' ) ) {
if ( subdomain === '*' ) continue ;
subdomain = subdomain . replace ( /^\*\./ , '' ) ; // remove *.
}
error = domains . validateHostname ( subdomain , domainObjectMap [ location . domain ] ) ;
2019-10-24 10:39:47 -07:00
if ( error ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'Bad location: ' + error . message , { field : 'location' , domain : location . domain , subdomain : location . subdomain } ) ) ;
2019-09-27 10:25:26 -07:00
}
callback ( null , domainObjectMap ) ;
} ) ;
}
2020-03-30 22:18:39 -07:00
function hasMailAddon ( manifest ) {
return manifest . addons . sendmail || manifest . addons . recvmail ;
}
2020-03-28 22:05:43 -07:00
function install ( data , auditSource , callback ) {
2016-06-03 23:22:38 -07:00
assert ( data && typeof data === 'object' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2020-03-29 19:12:07 -07:00
assert . strictEqual ( typeof data . manifest , 'object' ) ; // manifest is already downloaded
2020-03-29 16:24:04 -07:00
2021-06-15 11:12:46 -07:00
let location = data . location . toLowerCase ( ) ,
2017-11-02 22:17:44 +01:00
domain = data . domain . toLowerCase ( ) ,
2016-06-03 23:22:38 -07:00
portBindings = data . portBindings || null ,
accessRestriction = data . accessRestriction || null ,
icon = data . icon || null ,
memoryLimit = data . memoryLimit || 0 ,
2017-01-19 11:20:24 -08:00
sso = 'sso' in data ? data . sso : null ,
2017-04-11 12:49:21 -07:00
debugMode = data . debugMode || null ,
2017-08-16 14:12:07 -07:00
enableBackup = 'enableBackup' in data ? data . enableBackup : true ,
2018-12-07 09:03:28 -08:00
enableAutomaticUpdate = 'enableAutomaticUpdate' in data ? data . enableAutomaticUpdate : true ,
2018-10-11 14:07:43 -07:00
alternateDomains = data . alternateDomains || [ ] ,
2021-01-18 17:26:26 -08:00
aliasDomains = data . aliasDomains || [ ] ,
2018-12-12 12:55:35 -08:00
env = data . env || { } ,
2019-03-22 07:48:31 -07:00
label = data . label || null ,
2019-09-16 09:31:34 -07:00
tags = data . tags || [ ] ,
2020-03-29 16:24:04 -07:00
overwriteDns = 'overwriteDns' in data ? data . overwriteDns : false ,
2021-02-24 14:49:30 -08:00
skipDnsSetup = 'skipDnsSetup' in data ? data . skipDnsSetup : false ,
2020-03-29 16:24:04 -07:00
appStoreId = data . appStoreId ,
2021-06-16 23:09:30 -07:00
enableMailbox = 'enabledMailbox' in data ? data . enableMailbox : true ,
2020-03-29 16:24:04 -07:00
manifest = data . manifest ;
2016-06-03 23:22:38 -07:00
2020-03-29 16:24:04 -07:00
let error = manifestFormat . parse ( manifest ) ;
if ( error ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'Manifest error: ' + error . message ) ) ;
2015-07-20 00:09:47 -07:00
2020-03-29 16:24:04 -07:00
error = checkManifestConstraints ( manifest ) ;
if ( error ) return callback ( error ) ;
2015-07-20 00:09:47 -07:00
2020-03-29 16:24:04 -07:00
error = validatePortBindings ( portBindings , manifest ) ;
if ( error ) return callback ( error ) ;
2015-07-20 00:09:47 -07:00
2020-03-29 16:24:04 -07:00
error = validateAccessRestriction ( accessRestriction ) ;
if ( error ) return callback ( error ) ;
2015-07-20 00:09:47 -07:00
2020-03-29 16:24:04 -07:00
error = validateMemoryLimit ( manifest , memoryLimit ) ;
if ( error ) return callback ( error ) ;
2015-07-20 00:09:47 -07:00
2020-03-29 16:24:04 -07:00
error = validateDebugMode ( debugMode ) ;
if ( error ) return callback ( error ) ;
2016-02-05 15:07:27 +01:00
2020-03-29 16:24:04 -07:00
error = validateLabel ( label ) ;
if ( error ) return callback ( error ) ;
2016-02-11 18:14:16 +01:00
2020-03-29 16:24:04 -07:00
error = validateTags ( tags ) ;
if ( error ) return callback ( error ) ;
2019-10-11 20:21:32 -07:00
2020-03-29 16:24:04 -07:00
if ( 'sso' in data && ! ( 'optionalSso' in manifest ) ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'sso can only be specified for apps with optionalSso' ) ) ;
// if sso was unspecified, enable it by default if possible
2020-11-10 09:59:28 -08:00
if ( sso === null ) sso = ! ! manifest . addons [ 'ldap' ] || ! ! manifest . addons [ 'proxyAuth' ] ;
2019-03-22 07:48:31 -07:00
2020-03-29 16:24:04 -07:00
error = validateEnv ( env ) ;
if ( error ) return callback ( error ) ;
2019-03-22 07:48:31 -07:00
2020-05-04 14:56:10 -07:00
if ( settings . isDemo ( ) && constants . DEMO _BLACKLISTED _APPS . includes ( appStoreId ) ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'This app is blacklisted in the demo' ) ) ;
2020-03-30 22:18:39 -07:00
const mailboxName = hasMailAddon ( manifest ) ? mailboxNameForLocation ( location , manifest ) : null ;
const mailboxDomain = hasMailAddon ( manifest ) ? domain : null ;
2020-03-29 16:24:04 -07:00
const appId = uuid . v4 ( ) ;
2016-11-11 10:55:44 +05:30
2020-03-29 16:24:04 -07:00
if ( icon ) {
if ( ! validator . isBase64 ( icon ) ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'icon is not base64' , { field : 'icon' } ) ) ;
2021-04-30 13:18:15 -07:00
icon = Buffer . from ( icon , 'base64' ) ;
2020-03-29 16:24:04 -07:00
}
2016-07-09 12:25:00 -07:00
2021-01-18 22:47:53 -08:00
const locations = [ { subdomain : location , domain , type : 'primary' } ]
. concat ( alternateDomains . map ( ad => _ . extend ( ad , { type : 'redirect' } ) ) )
. concat ( aliasDomains . map ( ad => _ . extend ( ad , { type : 'alias' } ) ) ) ;
2020-03-29 16:24:04 -07:00
validateLocations ( locations , function ( error , domainObjectMap ) {
if ( error ) return callback ( error ) ;
2016-06-04 01:07:43 -07:00
2020-03-29 16:24:04 -07:00
debug ( 'Will install app with id : ' + appId ) ;
2021-06-15 11:12:46 -07:00
const data = {
2021-01-18 17:26:26 -08:00
accessRestriction ,
memoryLimit ,
sso ,
debugMode ,
mailboxName ,
mailboxDomain ,
enableBackup ,
enableAutomaticUpdate ,
alternateDomains ,
aliasDomains ,
env ,
label ,
tags ,
2021-04-30 13:18:15 -07:00
icon ,
2021-06-16 23:09:30 -07:00
enableMailbox ,
2020-03-29 16:24:04 -07:00
runState : exports . RSTATE _RUNNING ,
installationState : exports . ISTATE _PENDING _INSTALL
} ;
2018-01-09 21:03:59 -08:00
2020-03-29 16:24:04 -07:00
appdb . add ( appId , appStoreId , manifest , location , domain , translatePortBindings ( portBindings , manifest ) , data , function ( error ) {
if ( error && error . reason === BoxError . ALREADY _EXISTS ) return callback ( getDuplicateErrorDetails ( error . message , locations , domainObjectMap , portBindings ) ) ;
if ( error ) return callback ( error ) ;
2018-05-03 13:20:34 +02:00
2020-03-29 16:24:04 -07:00
purchaseApp ( { appId : appId , appstoreId : appStoreId , manifestId : manifest . id || 'customapp' } , function ( error ) {
2019-10-24 11:13:48 -07:00
if ( error ) return callback ( error ) ;
2015-07-20 00:09:47 -07:00
2020-03-29 16:24:04 -07:00
const task = {
2021-02-24 14:49:30 -08:00
args : { restoreConfig : null , skipDnsSetup , overwriteDns } ,
2020-03-29 16:24:04 -07:00
values : { } ,
requiredState : data . installationState
} ;
2019-08-27 15:18:16 -07:00
2020-03-29 16:24:04 -07:00
addTask ( appId , data . installationState , task , function ( error , result ) {
if ( error ) return callback ( error ) ;
2018-02-20 10:34:09 -08:00
2021-06-15 11:12:46 -07:00
const newApp = _ . extend ( { } , _ . omit ( data , 'icon' ) , { appStoreId , manifest , location , domain , portBindings } ) ;
2020-03-29 16:24:04 -07:00
newApp . fqdn = domains . fqdn ( newApp . location , domainObjectMap [ newApp . domain ] ) ;
newApp . alternateDomains . forEach ( function ( ad ) { ad . fqdn = domains . fqdn ( ad . subdomain , domainObjectMap [ ad . domain ] ) ; } ) ;
2021-01-18 17:26:26 -08:00
newApp . aliasDomains . forEach ( function ( ad ) { ad . fqdn = domains . fqdn ( ad . subdomain , domainObjectMap [ ad . domain ] ) ; } ) ;
2019-09-27 11:24:21 -07:00
2020-03-29 16:24:04 -07:00
eventlog . add ( eventlog . ACTION _APP _INSTALL , auditSource , { appId , app : newApp , taskId : result . taskId } ) ;
2018-01-09 21:03:59 -08:00
2020-03-29 16:24:04 -07:00
callback ( null , { id : appId , taskId : result . taskId } ) ;
2018-01-09 21:03:59 -08:00
} ) ;
2016-06-04 01:07:43 -07:00
} ) ;
2015-07-20 00:09:47 -07:00
} ) ;
} ) ;
}
2020-03-29 17:11:10 -07:00
function setAccessRestriction ( app , accessRestriction , auditSource , callback ) {
assert . strictEqual ( typeof app , 'object' ) ;
2019-09-08 16:57:08 -07:00
assert . strictEqual ( typeof accessRestriction , 'object' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
let error = validateAccessRestriction ( accessRestriction ) ;
if ( error ) return callback ( error ) ;
2019-09-08 16:57:08 -07:00
2021-05-25 21:31:48 -07:00
appdb . update ( appId , { accessRestriction } , function ( error ) {
2019-09-08 16:57:08 -07:00
if ( error ) return callback ( error ) ;
2020-03-29 17:11:10 -07:00
eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , accessRestriction } ) ;
2019-09-08 16:57:08 -07:00
2020-03-29 17:11:10 -07:00
callback ( ) ;
2019-09-08 16:57:08 -07:00
} ) ;
}
2020-03-29 17:11:10 -07:00
function setLabel ( app , label , auditSource , callback ) {
assert . strictEqual ( typeof app , 'object' ) ;
2019-09-08 16:57:08 -07:00
assert . strictEqual ( typeof label , 'string' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
let error = validateLabel ( label ) ;
if ( error ) return callback ( error ) ;
2019-09-08 16:57:08 -07:00
2021-05-25 21:31:48 -07:00
appdb . update ( appId , { label } , function ( error ) {
2019-09-08 16:57:08 -07:00
if ( error ) return callback ( error ) ;
2020-03-29 17:11:10 -07:00
eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , label } ) ;
2019-09-08 16:57:08 -07:00
2020-03-29 17:11:10 -07:00
callback ( ) ;
2019-09-08 16:57:08 -07:00
} ) ;
}
2020-03-29 17:11:10 -07:00
function setTags ( app , tags , auditSource , callback ) {
assert . strictEqual ( typeof app , 'object' ) ;
2019-09-08 16:57:08 -07:00
assert ( Array . isArray ( tags ) ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
let error = validateTags ( tags ) ;
if ( error ) return callback ( error ) ;
2019-09-08 16:57:08 -07:00
2021-05-25 21:31:48 -07:00
appdb . update ( appId , { tags } , function ( error ) {
2019-09-08 16:57:08 -07:00
if ( error ) return callback ( error ) ;
2020-03-29 17:11:10 -07:00
eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , tags } ) ;
2019-09-08 16:57:08 -07:00
2020-03-29 17:11:10 -07:00
callback ( ) ;
2019-09-08 16:57:08 -07:00
} ) ;
}
2020-03-29 17:11:10 -07:00
function setIcon ( app , icon , auditSource , callback ) {
assert . strictEqual ( typeof app , 'object' ) ;
2019-09-08 16:57:08 -07:00
assert ( icon === null || typeof icon === 'string' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
2019-09-08 16:57:08 -07:00
2020-03-29 17:11:10 -07:00
if ( icon ) {
if ( ! validator . isBase64 ( icon ) ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'icon is not base64' , { field : 'icon' } ) ) ;
2021-04-30 13:18:15 -07:00
icon = Buffer . from ( icon , 'base64' ) ;
2020-03-29 17:11:10 -07:00
}
2019-09-08 16:57:08 -07:00
2021-04-30 13:18:15 -07:00
appdb . update ( appId , { icon } , function ( error ) {
if ( error ) return callback ( error ) ;
2019-09-08 16:57:08 -07:00
2021-04-30 13:18:15 -07:00
eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , iconChanged : true } ) ;
callback ( ) ;
} ) ;
2019-09-08 16:57:08 -07:00
}
2020-03-29 17:11:10 -07:00
function setMemoryLimit ( app , memoryLimit , auditSource , callback ) {
assert . strictEqual ( typeof app , 'object' ) ;
2019-09-08 16:57:08 -07:00
assert . strictEqual ( typeof memoryLimit , 'number' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
let error = checkAppState ( app , exports . ISTATE _PENDING _RESIZE ) ;
if ( error ) return callback ( error ) ;
2019-09-08 16:57:08 -07:00
2020-03-29 17:11:10 -07:00
error = validateMemoryLimit ( app . manifest , memoryLimit ) ;
if ( error ) return callback ( error ) ;
2019-09-21 19:45:55 -07:00
2020-03-29 17:11:10 -07:00
const task = {
args : { } ,
values : { memoryLimit }
} ;
addTask ( appId , exports . ISTATE _PENDING _RESIZE , task , function ( error , result ) {
2019-09-08 16:57:08 -07:00
if ( error ) return callback ( error ) ;
2020-03-29 17:11:10 -07:00
eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , memoryLimit , taskId : result . taskId } ) ;
2019-09-08 16:57:08 -07:00
2020-03-29 17:11:10 -07:00
callback ( null , { taskId : result . taskId } ) ;
2019-09-08 16:57:08 -07:00
} ) ;
}
2020-03-29 17:11:10 -07:00
function setCpuShares ( app , cpuShares , auditSource , callback ) {
assert . strictEqual ( typeof app , 'object' ) ;
2020-01-28 21:30:35 -08:00
assert . strictEqual ( typeof cpuShares , 'number' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
let error = checkAppState ( app , exports . ISTATE _PENDING _RESIZE ) ;
if ( error ) return callback ( error ) ;
2020-01-28 21:30:35 -08:00
2020-03-29 17:11:10 -07:00
error = validateCpuShares ( cpuShares ) ;
if ( error ) return callback ( error ) ;
2020-01-28 21:30:35 -08:00
2020-03-29 17:11:10 -07:00
const task = {
args : { } ,
values : { cpuShares }
} ;
addTask ( appId , exports . ISTATE _PENDING _RESIZE , task , function ( error , result ) {
2020-01-28 21:30:35 -08:00
if ( error ) return callback ( error ) ;
2020-03-29 17:11:10 -07:00
eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , cpuShares , taskId : result . taskId } ) ;
2020-01-28 21:30:35 -08:00
2020-03-29 17:11:10 -07:00
callback ( null , { taskId : result . taskId } ) ;
2020-01-28 21:30:35 -08:00
} ) ;
}
2020-10-28 19:42:48 -07:00
function setMounts ( app , mounts , auditSource , callback ) {
2020-04-29 21:55:21 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2020-10-28 19:42:48 -07:00
assert ( Array . isArray ( mounts ) ) ;
2020-04-29 21:55:21 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
const appId = app . id ;
let error = checkAppState ( app , exports . ISTATE _PENDING _RECREATE _CONTAINER ) ;
if ( error ) return callback ( error ) ;
const task = {
args : { } ,
2020-10-28 19:42:48 -07:00
values : { mounts }
2020-04-29 21:55:21 -07:00
} ;
addTask ( appId , exports . ISTATE _PENDING _RECREATE _CONTAINER , task , function ( error , result ) {
2020-10-29 22:08:31 -07:00
if ( error && error . reason === BoxError . ALREADY _EXISTS ) return callback ( new BoxError ( BoxError . CONFLICT , 'Duplicate mount points' ) ) ;
2020-04-29 21:55:21 -07:00
if ( error ) return callback ( error ) ;
2020-10-28 19:42:48 -07:00
eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , mounts , taskId : result . taskId } ) ;
2020-04-29 21:55:21 -07:00
callback ( null , { taskId : result . taskId } ) ;
} ) ;
}
2020-03-29 17:11:10 -07:00
function setEnvironment ( app , env , auditSource , callback ) {
assert . strictEqual ( typeof app , 'object' ) ;
2019-09-08 16:57:08 -07:00
assert . strictEqual ( typeof env , 'object' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
let error = checkAppState ( app , exports . ISTATE _PENDING _RECREATE _CONTAINER ) ;
if ( error ) return callback ( error ) ;
2019-09-08 16:57:08 -07:00
2020-03-29 17:11:10 -07:00
error = validateEnv ( env ) ;
if ( error ) return callback ( error ) ;
2019-09-21 19:45:55 -07:00
2020-03-29 17:11:10 -07:00
const task = {
args : { } ,
values : { env }
} ;
addTask ( appId , exports . ISTATE _PENDING _RECREATE _CONTAINER , task , function ( error , result ) {
2019-09-08 16:57:08 -07:00
if ( error ) return callback ( error ) ;
2020-03-29 17:11:10 -07:00
eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , env , taskId : result . taskId } ) ;
2019-09-08 16:57:08 -07:00
2020-03-29 17:11:10 -07:00
callback ( null , { taskId : result . taskId } ) ;
2019-09-08 16:57:08 -07:00
} ) ;
}
2020-03-29 17:11:10 -07:00
function setDebugMode ( app , debugMode , auditSource , callback ) {
assert . strictEqual ( typeof app , 'object' ) ;
2019-09-08 16:57:08 -07:00
assert . strictEqual ( typeof debugMode , 'object' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
let error = checkAppState ( app , exports . ISTATE _PENDING _DEBUG ) ;
if ( error ) return callback ( error ) ;
2019-09-08 16:57:08 -07:00
2020-03-29 17:11:10 -07:00
error = validateDebugMode ( debugMode ) ;
if ( error ) return callback ( error ) ;
2019-09-21 19:45:55 -07:00
2020-03-29 17:11:10 -07:00
const task = {
args : { } ,
values : { debugMode }
} ;
addTask ( appId , exports . ISTATE _PENDING _DEBUG , task , function ( error , result ) {
2019-09-08 16:57:08 -07:00
if ( error ) return callback ( error ) ;
2020-03-29 17:11:10 -07:00
eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , debugMode , taskId : result . taskId } ) ;
2019-09-08 16:57:08 -07:00
2020-03-29 17:11:10 -07:00
callback ( null , { taskId : result . taskId } ) ;
2019-09-08 16:57:08 -07:00
} ) ;
}
2021-03-16 22:38:59 -07:00
function setMailbox ( app , data , auditSource , callback ) {
2020-03-29 17:11:10 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2021-03-16 22:38:59 -07:00
assert . strictEqual ( typeof data , 'object' ) ;
2019-09-08 16:57:08 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2021-03-16 22:38:59 -07:00
const { enable , mailboxDomain } = data ;
let mailboxName = data . mailboxName ;
assert . strictEqual ( typeof enable , 'boolean' ) ;
assert ( mailboxName === null || typeof mailboxName === 'string' ) ;
assert . strictEqual ( typeof mailboxDomain , 'string' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
let error = checkAppState ( app , exports . ISTATE _PENDING _RECREATE _CONTAINER ) ;
if ( error ) return callback ( error ) ;
2019-09-08 16:57:08 -07:00
2020-03-30 22:18:39 -07:00
if ( ! hasMailAddon ( app . manifest ) ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'App does not use mail addons' ) ) ;
2021-06-29 14:26:34 -07:00
const getDomainFunc = util . callbackify ( mail . getDomain ) ;
getDomainFunc ( mailboxDomain , function ( error ) {
2019-09-21 19:45:55 -07:00
if ( error ) return callback ( error ) ;
2020-03-29 17:11:10 -07:00
if ( mailboxName ) {
error = mail . validateName ( mailboxName ) ;
if ( error ) return callback ( new BoxError ( BoxError . BAD _FIELD , error . message , { field : 'mailboxName' } ) ) ;
} else {
2020-03-30 22:18:39 -07:00
mailboxName = mailboxNameForLocation ( app . location , app . domain , app . manifest ) ;
2020-03-29 17:11:10 -07:00
}
2019-09-08 16:57:08 -07:00
2020-03-29 17:11:10 -07:00
const task = {
args : { } ,
2021-03-16 22:38:59 -07:00
values : { enableMailbox : enable , mailboxName , mailboxDomain }
2020-03-29 17:11:10 -07:00
} ;
addTask ( appId , exports . ISTATE _PENDING _RECREATE _CONTAINER , task , function ( error , result ) {
if ( error ) return callback ( error ) ;
2019-11-14 21:43:14 -08:00
2020-03-29 17:11:10 -07:00
eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , mailboxName , taskId : result . taskId } ) ;
2019-11-14 21:43:14 -08:00
2020-03-29 17:11:10 -07:00
callback ( null , { taskId : result . taskId } ) ;
2019-09-08 16:57:08 -07:00
} ) ;
} ) ;
}
2020-03-29 17:11:10 -07:00
function setAutomaticBackup ( app , enable , auditSource , callback ) {
assert . strictEqual ( typeof app , 'object' ) ;
2019-09-08 16:57:08 -07:00
assert . strictEqual ( typeof enable , 'boolean' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
appdb . update ( appId , { enableBackup : enable } , function ( error ) {
2019-09-08 16:57:08 -07:00
if ( error ) return callback ( error ) ;
2020-03-29 17:11:10 -07:00
eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , enableBackup : enable } ) ;
2019-09-08 16:57:08 -07:00
2020-03-29 17:11:10 -07:00
callback ( ) ;
2019-09-08 16:57:08 -07:00
} ) ;
}
2020-03-29 17:11:10 -07:00
function setAutomaticUpdate ( app , enable , auditSource , callback ) {
assert . strictEqual ( typeof app , 'object' ) ;
2019-09-08 16:57:08 -07:00
assert . strictEqual ( typeof enable , 'boolean' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
appdb . update ( appId , { enableAutomaticUpdate : enable } , function ( error ) {
2019-09-08 16:57:08 -07:00
if ( error ) return callback ( error ) ;
2020-03-29 17:11:10 -07:00
eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , enableAutomaticUpdate : enable } ) ;
2019-09-08 16:57:08 -07:00
2020-03-29 17:11:10 -07:00
callback ( ) ;
2019-09-08 16:57:08 -07:00
} ) ;
}
2020-03-29 17:11:10 -07:00
function setReverseProxyConfig ( app , reverseProxyConfig , auditSource , callback ) {
assert . strictEqual ( typeof app , 'object' ) ;
2019-10-13 18:22:03 -07:00
assert . strictEqual ( typeof reverseProxyConfig , 'object' ) ;
2019-09-08 16:57:08 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2019-10-14 16:59:22 -07:00
reverseProxyConfig = _ . extend ( { robotsTxt : null , csp : null } , reverseProxyConfig ) ;
2019-10-13 18:22:03 -07:00
2020-03-29 17:11:10 -07:00
const appId = app . id ;
let error = validateCsp ( reverseProxyConfig . csp ) ;
if ( error ) return callback ( error ) ;
2019-09-08 16:57:08 -07:00
2020-03-29 17:11:10 -07:00
error = validateRobotsTxt ( reverseProxyConfig . robotsTxt ) ;
if ( error ) return callback ( error ) ;
2019-10-13 18:22:03 -07:00
2020-03-29 17:11:10 -07:00
reverseProxy . writeAppConfig ( _ . extend ( { } , app , { reverseProxyConfig } ) , function ( error ) {
2019-10-13 18:22:03 -07:00
if ( error ) return callback ( error ) ;
2020-03-29 17:11:10 -07:00
appdb . update ( appId , { reverseProxyConfig } , function ( error ) {
2019-10-24 10:39:47 -07:00
if ( error ) return callback ( error ) ;
2019-09-09 21:41:55 -07:00
2020-03-29 17:11:10 -07:00
eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , reverseProxyConfig } ) ;
2019-09-08 16:57:08 -07:00
2020-03-29 17:11:10 -07:00
callback ( ) ;
2019-09-08 16:57:08 -07:00
} ) ;
} ) ;
}
2021-05-05 10:34:22 -07:00
function setCertificate ( app , data , auditSource , callback ) {
2020-03-29 17:11:10 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2021-05-05 10:34:22 -07:00
assert ( data && typeof data === 'object' ) ;
2019-09-08 16:57:08 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
2021-05-05 10:34:22 -07:00
const { location , domain , cert , key } = data ;
2019-09-08 16:57:08 -07:00
2021-05-05 10:34:22 -07:00
domains . get ( domain , function ( error , domainObject ) {
2020-03-29 17:11:10 -07:00
if ( error ) return callback ( error ) ;
2019-09-08 16:57:08 -07:00
2021-05-05 10:34:22 -07:00
if ( cert && key ) {
error = reverseProxy . validateCertificate ( location , domainObject , { cert , key } ) ;
if ( error ) return callback ( error ) ;
2020-03-29 17:11:10 -07:00
}
2019-09-08 16:57:08 -07:00
2021-05-05 10:34:22 -07:00
reverseProxy . setAppCertificateSync ( location , domainObject , { cert , key } , function ( error ) {
if ( error ) return callback ( error ) ;
2019-09-08 16:57:08 -07:00
2021-05-05 10:34:22 -07:00
eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , cert , key } ) ;
2019-09-08 16:57:08 -07:00
2021-05-05 10:34:22 -07:00
callback ( ) ;
} ) ;
2019-09-08 16:57:08 -07:00
} ) ;
}
2020-03-29 17:11:10 -07:00
function setLocation ( app , data , auditSource , callback ) {
assert . strictEqual ( typeof app , 'object' ) ;
2019-09-08 16:57:08 -07:00
assert . strictEqual ( typeof data , 'object' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
let error = checkAppState ( app , exports . ISTATE _PENDING _LOCATION _CHANGE ) ;
if ( error ) return callback ( error ) ;
2019-09-08 16:57:08 -07:00
2020-03-29 17:11:10 -07:00
let values = {
location : data . location . toLowerCase ( ) ,
domain : data . domain . toLowerCase ( ) ,
// these are intentionally reset, if not set
portBindings : null ,
2021-01-18 17:26:26 -08:00
alternateDomains : [ ] ,
aliasDomains : [ ]
2020-03-29 17:11:10 -07:00
} ;
2019-09-21 19:45:55 -07:00
2020-03-29 17:11:10 -07:00
if ( 'portBindings' in data ) {
error = validatePortBindings ( data . portBindings , app . manifest ) ;
if ( error ) return callback ( error ) ;
2019-09-08 16:57:08 -07:00
2020-03-29 17:11:10 -07:00
values . portBindings = translatePortBindings ( data . portBindings || null , app . manifest ) ;
}
2019-09-08 16:57:08 -07:00
2020-03-29 17:11:10 -07:00
// move the mailbox name to match the new location
2020-03-30 22:18:39 -07:00
if ( hasMailAddon ( app . manifest ) && app . mailboxName . endsWith ( '.app' ) ) {
values . mailboxName = mailboxNameForLocation ( values . location , app . manifest ) ;
values . mailboxDomain = values . domain ;
}
2019-09-08 16:57:08 -07:00
2020-03-29 17:11:10 -07:00
if ( 'alternateDomains' in data ) {
values . alternateDomains = data . alternateDomains ;
}
2019-09-27 10:25:26 -07:00
2021-01-18 17:26:26 -08:00
if ( 'aliasDomains' in data ) {
values . aliasDomains = data . aliasDomains ;
}
2021-01-18 22:47:53 -08:00
const locations = [ { subdomain : values . location , domain : values . domain , type : 'primary' } ]
. concat ( values . alternateDomains . map ( ad => _ . extend ( ad , { type : 'redirect' } ) ) )
. concat ( values . aliasDomains . map ( ad => _ . extend ( ad , { type : 'alias' } ) ) ) ;
2019-09-08 16:57:08 -07:00
2020-03-29 17:11:10 -07:00
validateLocations ( locations , function ( error , domainObjectMap ) {
if ( error ) return callback ( error ) ;
2019-09-08 16:57:08 -07:00
2020-03-29 17:11:10 -07:00
const task = {
args : {
2021-01-18 17:26:26 -08:00
oldConfig : _ . pick ( app , 'location' , 'domain' , 'fqdn' , 'alternateDomains' , 'aliasDomains' , 'portBindings' ) ,
2021-02-24 14:49:30 -08:00
skipDnsSetup : ! ! data . skipDnsSetup ,
2020-03-29 17:11:10 -07:00
overwriteDns : ! ! data . overwriteDns
} ,
values
} ;
addTask ( appId , exports . ISTATE _PENDING _LOCATION _CHANGE , task , function ( error , result ) {
if ( error && error . reason === BoxError . ALREADY _EXISTS ) error = getDuplicateErrorDetails ( error . message , locations , domainObjectMap , data . portBindings ) ;
2019-09-27 10:25:26 -07:00
if ( error ) return callback ( error ) ;
2019-09-08 16:57:08 -07:00
2020-03-29 17:11:10 -07:00
values . fqdn = domains . fqdn ( values . location , domainObjectMap [ values . domain ] ) ;
values . alternateDomains . forEach ( function ( ad ) { ad . fqdn = domains . fqdn ( ad . subdomain , domainObjectMap [ ad . domain ] ) ; } ) ;
2021-01-18 17:26:26 -08:00
values . aliasDomains . forEach ( function ( ad ) { ad . fqdn = domains . fqdn ( ad . subdomain , domainObjectMap [ ad . domain ] ) ; } ) ;
2019-09-27 11:24:21 -07:00
2020-03-29 17:11:10 -07:00
eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , _ . extend ( { appId , app , taskId : result . taskId } , values ) ) ;
2019-09-08 16:57:08 -07:00
2020-03-29 17:11:10 -07:00
callback ( null , { taskId : result . taskId } ) ;
2019-09-08 16:57:08 -07:00
} ) ;
} ) ;
}
2020-03-29 17:11:10 -07:00
function setDataDir ( app , dataDir , auditSource , callback ) {
assert . strictEqual ( typeof app , 'object' ) ;
2019-09-09 16:37:59 -07:00
assert ( dataDir === null || typeof dataDir === 'string' ) ;
2019-09-08 16:57:08 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
let error = checkAppState ( app , exports . ISTATE _PENDING _DATA _DIR _MIGRATION ) ;
if ( error ) return callback ( error ) ;
2019-09-08 16:57:08 -07:00
2020-03-29 17:11:10 -07:00
error = validateDataDir ( dataDir ) ;
if ( error ) return callback ( error ) ;
2019-09-21 19:45:55 -07:00
2020-03-29 17:11:10 -07:00
const task = {
args : { newDataDir : dataDir } ,
2021-06-24 16:19:30 -07:00
values : { } ,
onFinished : ( error ) => {
if ( ! error ) services . rebuildService ( 'sftp' , NOOP _CALLBACK ) ;
}
2020-03-29 17:11:10 -07:00
} ;
addTask ( appId , exports . ISTATE _PENDING _DATA _DIR _MIGRATION , task , function ( error , result ) {
2019-09-08 16:57:08 -07:00
if ( error ) return callback ( error ) ;
2020-03-29 17:11:10 -07:00
eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , dataDir , taskId : result . taskId } ) ;
2019-09-08 16:57:08 -07:00
2020-03-29 17:11:10 -07:00
callback ( null , { taskId : result . taskId } ) ;
2019-09-08 16:57:08 -07:00
} ) ;
}
2020-03-29 17:11:10 -07:00
function update ( app , data , auditSource , callback ) {
assert . strictEqual ( typeof app , 'object' ) ;
2016-06-04 19:06:16 -07:00
assert ( data && typeof data === 'object' ) ;
2020-03-30 15:05:37 -07:00
assert ( data . manifest && typeof data . manifest === 'object' ) ;
2016-05-01 21:37:08 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2015-07-20 00:09:47 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2020-03-29 16:24:04 -07:00
const skipBackup = ! ! data . skipBackup ,
2020-03-29 18:40:49 -07:00
appId = app . id ,
2020-12-09 16:51:49 -08:00
manifest = data . manifest ,
appStoreId = data . appStoreId ;
2020-03-29 18:40:49 -07:00
2020-03-31 15:44:46 -07:00
let values = { } ;
2020-03-29 18:40:49 -07:00
if ( app . runState === exports . RSTATE _STOPPED ) return callback ( new BoxError ( BoxError . BAD _STATE , 'Stopped apps cannot be updated' ) ) ;
2019-09-26 20:10:11 -07:00
2020-03-29 17:11:10 -07:00
let error = checkAppState ( app , exports . ISTATE _PENDING _UPDATE ) ;
if ( error ) return callback ( error ) ;
2016-06-04 19:06:16 -07:00
2020-03-29 17:11:10 -07:00
error = manifestFormat . parse ( manifest ) ;
if ( error ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'Manifest error:' + error . message ) ) ;
2015-07-20 00:09:47 -07:00
2020-03-29 17:11:10 -07:00
error = checkManifestConstraints ( manifest ) ;
if ( error ) return callback ( error ) ;
2015-07-20 00:09:47 -07:00
2020-12-09 16:51:49 -08:00
var updateConfig = { skipBackup , manifest , appStoreId } ; // this will clear appStoreId when updating from a repo and set it if passed in for update route
2016-03-25 11:35:47 -07:00
2020-03-29 17:11:10 -07:00
// prevent user from installing a app with different manifest id over an existing app
// this allows cloudron install -f --app <appid> for an app installed from the appStore
if ( app . manifest . id !== updateConfig . manifest . id ) {
if ( ! data . force ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'manifest id does not match. force to override' ) ) ;
}
2019-01-11 14:19:32 -08:00
2020-03-29 17:11:10 -07:00
// suffix '0' if prerelease is missing for semver.lte to work as expected
const currentVersion = semver . prerelease ( app . manifest . version ) ? app . manifest . version : ` ${ app . manifest . version } -0 ` ;
const updateVersion = semver . prerelease ( updateConfig . manifest . version ) ? updateConfig . manifest . version : ` ${ updateConfig . manifest . version } -0 ` ;
if ( app . appStoreId !== '' && semver . lte ( updateVersion , currentVersion ) ) {
if ( ! data . force ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'Downgrades are not permitted for apps installed from AppStore. force to override' ) ) ;
}
2019-01-11 14:19:32 -08:00
2020-03-29 17:11:10 -07:00
if ( 'icon' in data ) {
if ( data . icon ) {
if ( ! validator . isBase64 ( data . icon ) ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'icon is not base64' , { field : 'icon' } ) ) ;
2021-04-30 13:18:15 -07:00
data . icon = Buffer . from ( data . icon , 'base64' ) ;
2020-03-29 16:24:04 -07:00
}
2021-04-30 13:18:15 -07:00
values . icon = data . icon ;
2020-03-29 17:11:10 -07:00
}
2016-06-04 19:19:00 -07:00
2020-03-29 17:11:10 -07:00
// do not update apps in debug mode
if ( app . debugMode && ! data . force ) return callback ( new BoxError ( BoxError . BAD _STATE , 'debug mode enabled. force to override' ) ) ;
2017-01-19 11:20:24 -08:00
2020-03-29 17:11:10 -07:00
// Ensure we update the memory limit in case the new app requires more memory as a minimum
// 0 and -1 are special updateConfig for memory limit indicating unset and unlimited
if ( app . memoryLimit > 0 && updateConfig . manifest . memoryLimit && app . memoryLimit < updateConfig . manifest . memoryLimit ) {
updateConfig . memoryLimit = updateConfig . manifest . memoryLimit ;
}
2016-02-14 12:10:22 +01:00
2020-03-31 15:44:46 -07:00
if ( ! hasMailAddon ( manifest ) ) { // clear if the update removed addon
values . mailboxName = values . mailboxDomain = null ;
} else if ( ! app . mailboxName || app . mailboxName . endsWith ( '.app' ) ) { // allocate since restore added the addon
values . mailboxName = mailboxNameForLocation ( app . location , manifest ) ;
values . mailboxDomain = app . domain ;
}
2020-03-29 17:11:10 -07:00
const task = {
args : { updateConfig } ,
2021-04-19 14:22:22 -07:00
values ,
onFinished : ( error ) => {
2021-07-13 13:29:42 -07:00
eventlog . add ( eventlog . ACTION _APP _UPDATE _FINISH , auditSource , { app , success : ! error , errorMessage : error ? error . message : null } ) ;
2021-04-19 14:22:22 -07:00
}
2020-03-29 17:11:10 -07:00
} ;
addTask ( appId , exports . ISTATE _PENDING _UPDATE , task , function ( error , result ) {
if ( error ) return callback ( error ) ;
2015-07-20 00:09:47 -07:00
2020-03-29 17:11:10 -07:00
eventlog . add ( eventlog . ACTION _APP _UPDATE , auditSource , { appId , app , skipBackup , toManifest : manifest , fromManifest : app . manifest , force : data . force , taskId : result . taskId } ) ;
2016-05-01 21:37:08 -07:00
2020-03-29 17:11:10 -07:00
callback ( null , { taskId : result . taskId } ) ;
2015-07-20 00:09:47 -07:00
} ) ;
}
2020-10-06 17:53:04 +02:00
function getLocalLogfilePaths ( app ) {
assert . strictEqual ( typeof app , 'object' ) ;
const appId = app . id ;
var filePaths = [ ] ;
filePaths . push ( path . join ( paths . LOG _DIR , appId , 'apptask.log' ) ) ;
filePaths . push ( path . join ( paths . LOG _DIR , appId , 'app.log' ) ) ;
if ( app . manifest . addons && app . manifest . addons . redis ) filePaths . push ( path . join ( paths . LOG _DIR , ` redis- ${ appId } /app.log ` ) ) ;
return filePaths ;
}
2020-03-29 17:11:10 -07:00
function getLogs ( app , options , callback ) {
assert . strictEqual ( typeof app , 'object' ) ;
2017-04-18 20:32:57 -07:00
assert ( options && typeof options === 'object' ) ;
2015-07-20 00:09:47 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2019-01-08 12:10:53 -08:00
assert . strictEqual ( typeof options . lines , 'number' ) ;
assert . strictEqual ( typeof options . format , 'string' ) ;
assert . strictEqual ( typeof options . follow , 'boolean' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
2015-11-02 11:20:50 -08:00
2020-03-29 17:11:10 -07:00
var lines = options . lines === - 1 ? '+1' : options . lines ,
format = options . format || 'json' ,
follow = options . follow ;
2017-04-18 20:32:57 -07:00
2020-03-29 17:11:10 -07:00
assert . strictEqual ( typeof format , 'string' ) ;
2018-06-11 20:09:38 +02:00
2020-03-29 17:11:10 -07:00
var args = [ '--lines=' + lines ] ;
if ( follow ) args . push ( '--follow' , '--retry' , '--quiet' ) ; // same as -F. to make it work if file doesn't exist, --quiet to not output file headers, which are no logs
2015-07-20 00:09:47 -07:00
2020-10-06 17:53:04 +02:00
var cp = spawn ( '/usr/bin/tail' , args . concat ( getLocalLogfilePaths ( app ) ) ) ;
2015-07-20 00:09:47 -07:00
2020-03-29 17:11:10 -07:00
var transformStream = split ( function mapper ( line ) {
if ( format !== 'json' ) return line + '\n' ;
2018-06-04 21:24:02 +02:00
2020-03-29 17:11:10 -07:00
var data = line . split ( ' ' ) ; // logs are <ISOtimestamp> <msg>
var timestamp = ( new Date ( data [ 0 ] ) ) . getTime ( ) ;
if ( isNaN ( timestamp ) ) timestamp = 0 ;
var message = line . slice ( data [ 0 ] . length + 1 ) ;
2018-06-14 12:21:43 +02:00
2020-03-29 17:11:10 -07:00
// ignore faulty empty logs
if ( ! timestamp && ! message ) return ;
2018-06-04 21:34:05 +02:00
2020-03-29 17:11:10 -07:00
return JSON . stringify ( {
realtimeTimestamp : timestamp * 1000 ,
message : message ,
source : appId
} ) + '\n' ;
} ) ;
2015-07-20 00:09:47 -07:00
2020-03-29 17:11:10 -07:00
transformStream . close = cp . kill . bind ( cp , 'SIGKILL' ) ; // closing stream kills the child process
2015-07-20 00:09:47 -07:00
2020-03-29 17:11:10 -07:00
cp . stdout . pipe ( transformStream ) ;
2015-07-20 00:09:47 -07:00
2020-03-29 17:11:10 -07:00
return callback ( null , transformStream ) ;
2015-07-20 00:09:47 -07:00
}
2021-05-07 21:37:23 -07:00
async function getCertificate ( subdomain , domain ) {
assert . strictEqual ( typeof subdomain , 'string' ) ;
assert . strictEqual ( typeof domain , 'string' ) ;
const result = await database . query ( 'SELECT certificateJson FROM subdomains WHERE subdomain=? AND domain=?' , [ subdomain , domain ] ) ;
if ( result . length === 0 ) return null ;
return JSON . parse ( result [ 0 ] . certificateJson ) ;
}
2019-11-23 18:35:51 -08:00
// does a re-configure when called from most states. for install/clone errors, it re-installs with an optional manifest
2020-02-11 21:05:01 -08:00
// re-configure can take a dockerImage but not a manifest because re-configure does not clean up addons
2020-03-29 17:11:10 -07:00
function repair ( app , data , auditSource , callback ) {
assert . strictEqual ( typeof app , 'object' ) ;
2019-11-23 18:35:51 -08:00
assert . strictEqual ( typeof data , 'object' ) ; // { manifest }
2019-09-19 17:04:11 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
let errorState = ( app . error && app . error . installationState ) || exports . ISTATE _PENDING _CONFIGURE ;
2019-09-19 17:04:11 -07:00
2020-03-29 17:11:10 -07:00
const task = {
args : { } ,
values : { } ,
requiredState : null
} ;
2019-09-21 19:45:55 -07:00
2020-03-29 17:11:10 -07:00
// maybe split this into a separate route like reinstall?
if ( errorState === exports . ISTATE _PENDING _INSTALL || errorState === exports . ISTATE _PENDING _CLONE ) {
2021-02-24 14:49:30 -08:00
task . args = { skipDnsSetup : false , overwriteDns : true } ;
2020-03-29 17:11:10 -07:00
if ( data . manifest ) {
let error = manifestFormat . parse ( data . manifest ) ;
if ( error ) return callback ( new BoxError ( BoxError . BAD _FIELD , ` manifest error: ${ error . message } ` ) ) ;
2019-09-19 17:04:11 -07:00
2020-03-29 17:11:10 -07:00
error = checkManifestConstraints ( data . manifest ) ;
if ( error ) return callback ( error ) ;
2019-09-21 19:45:55 -07:00
2020-03-31 15:44:46 -07:00
if ( ! hasMailAddon ( data . manifest ) ) { // clear if repair removed addon
task . values . mailboxName = task . values . mailboxDomain = null ;
} else if ( ! app . mailboxName || app . mailboxName . endsWith ( '.app' ) ) { // allocate since repair added the addon
task . values . mailboxName = mailboxNameForLocation ( app . location , data . manifest ) ;
task . values . mailboxDomain = app . domain ;
}
2020-03-29 17:11:10 -07:00
task . values . manifest = data . manifest ;
task . args . oldManifest = app . manifest ;
}
} else {
errorState = exports . ISTATE _PENDING _CONFIGURE ;
if ( data . dockerImage ) {
let newManifest = _ . extend ( { } , app . manifest , { dockerImage : data . dockerImage } ) ;
task . values . manifest = newManifest ;
2019-11-23 18:35:51 -08:00
}
2020-03-29 17:11:10 -07:00
}
2019-09-21 19:45:55 -07:00
2020-03-29 17:11:10 -07:00
addTask ( appId , errorState , task , function ( error , result ) {
if ( error ) return callback ( error ) ;
2019-10-03 11:35:27 -07:00
2020-03-29 17:11:10 -07:00
eventlog . add ( eventlog . ACTION _APP _REPAIR , auditSource , { app , taskId : result . taskId } ) ;
2019-10-03 11:35:27 -07:00
2020-03-29 17:11:10 -07:00
callback ( null , { taskId : result . taskId } ) ;
2019-09-19 17:04:11 -07:00
} ) ;
}
2021-07-14 11:07:19 -07:00
async function restore ( app , backupId , auditSource ) {
2020-03-29 17:11:10 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2019-12-05 21:15:09 -08:00
assert . strictEqual ( typeof backupId , 'string' ) ;
2016-05-01 21:37:08 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2015-07-20 00:09:47 -07:00
2020-03-29 17:11:10 -07:00
const appId = app . id ;
2015-07-20 00:09:47 -07:00
2020-03-29 17:11:10 -07:00
let error = checkAppState ( app , exports . ISTATE _PENDING _RESTORE ) ;
2021-07-14 11:07:19 -07:00
if ( error ) throw error ;
2020-03-29 17:11:10 -07:00
// for empty or null backupId, use existing manifest to mimic a reinstall
2021-07-14 11:07:19 -07:00
const backupInfo = backupId ? await backups . get ( backupId ) : { manifest : app . manifest } ;
2019-09-21 19:45:55 -07:00
2021-07-14 11:07:19 -07:00
if ( ! backupInfo . manifest ) throw new BoxError ( BoxError . EXTERNAL _ERROR , 'Could not get restore manifest' ) ;
if ( backupInfo . encryptionVersion === 1 ) throw new BoxError ( BoxError . BAD _FIELD , 'This encrypted backup was created with an older Cloudron version and has to be restored using the CLI tool' ) ;
2015-07-20 00:09:47 -07:00
2021-07-14 11:07:19 -07:00
// re-validate because this new box version may not accept old configs
error = checkManifestConstraints ( backupInfo . manifest ) ;
if ( error ) throw error ;
2016-06-13 13:44:49 -07:00
2021-07-14 11:07:19 -07:00
let values = { manifest : backupInfo . manifest } ;
if ( ! hasMailAddon ( backupInfo . manifest ) ) { // clear if restore removed addon
values . mailboxName = values . mailboxDomain = null ;
} else if ( ! app . mailboxName || app . mailboxName . endsWith ( '.app' ) ) { // allocate since restore added the addon
values . mailboxName = mailboxNameForLocation ( app . location , backupInfo . manifest ) ;
values . mailboxDomain = app . domain ;
}
2016-06-13 18:11:11 -07:00
2021-07-14 11:07:19 -07:00
const restoreConfig = { backupId , backupFormat : backupInfo . format } ;
2020-03-31 15:44:46 -07:00
2021-07-14 11:07:19 -07:00
const task = {
args : {
restoreConfig ,
oldManifest : app . manifest ,
skipDnsSetup : ! ! backupId , // if this is a restore, just skip dns setup. only re-installs should setup dns
overwriteDns : true
} ,
values
} ;
2015-08-19 10:54:39 -07:00
2021-07-14 11:07:19 -07:00
return new Promise ( ( resolve , reject ) => {
2020-03-29 17:11:10 -07:00
addTask ( appId , exports . ISTATE _PENDING _RESTORE , task , function ( error , result ) {
2021-07-14 11:07:19 -07:00
if ( error ) return reject ( error ) ;
2015-08-19 10:54:39 -07:00
2020-03-29 17:11:10 -07:00
eventlog . add ( eventlog . ACTION _APP _RESTORE , auditSource , { app : app , backupId : backupInfo . id , fromManifest : app . manifest , toManifest : backupInfo . manifest , taskId : result . taskId } ) ;
2016-05-01 21:37:08 -07:00
2021-07-14 11:07:19 -07:00
resolve ( { taskId : result . taskId } ) ;
2015-07-20 00:09:47 -07:00
} ) ;
} ) ;
}
2020-03-29 17:11:10 -07:00
function importApp ( app , data , auditSource , callback ) {
assert . strictEqual ( typeof app , 'object' ) ;
2019-10-15 09:16:29 -07:00
assert . strictEqual ( typeof data , 'object' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
2019-10-15 09:16:29 -07:00
2020-03-29 17:11:10 -07:00
// all fields are optional
data . backupId = data . backupId || null ;
data . backupFormat = data . backupFormat || null ;
data . backupConfig = data . backupConfig || null ;
const { backupId , backupFormat , backupConfig } = data ;
2019-10-15 09:16:29 -07:00
2020-03-29 17:11:10 -07:00
let error = backupFormat ? validateBackupFormat ( backupFormat ) : null ;
if ( error ) return callback ( error ) ;
2019-12-04 18:54:25 -08:00
2021-05-26 09:27:15 -07:00
error = checkAppState ( app , exports . ISTATE _PENDING _IMPORT ) ;
2020-03-29 17:11:10 -07:00
if ( error ) return callback ( error ) ;
// TODO: make this smarter to do a read-only test and check if the file exists in the storage backend
const testBackupConfig = backupConfig ? backups . testProviderConfig . bind ( null , backupConfig ) : ( next ) => next ( ) ;
2019-10-15 09:16:29 -07:00
2020-03-29 17:11:10 -07:00
testBackupConfig ( function ( error ) {
2019-10-15 09:16:29 -07:00
if ( error ) return callback ( error ) ;
2020-05-24 18:42:04 -07:00
if ( backupConfig ) {
if ( 'password' in backupConfig ) {
backupConfig . encryption = backups . generateEncryptionKeysSync ( backupConfig . password ) ;
delete backupConfig . password ;
} else {
backupConfig . encryption = null ;
}
2020-05-12 15:49:43 -07:00
}
2020-03-29 17:11:10 -07:00
const restoreConfig = { backupId , backupFormat , backupConfig } ;
2019-10-15 09:16:29 -07:00
2020-03-29 17:11:10 -07:00
const task = {
args : {
restoreConfig ,
oldManifest : app . manifest ,
2021-02-24 14:49:30 -08:00
skipDnsSetup : false ,
2020-03-29 17:11:10 -07:00
overwriteDns : true
} ,
values : { }
} ;
2021-05-26 09:27:15 -07:00
addTask ( appId , exports . ISTATE _PENDING _IMPORT , task , function ( error , result ) {
2019-10-15 09:16:29 -07:00
if ( error ) return callback ( error ) ;
2021-05-26 09:27:15 -07:00
eventlog . add ( eventlog . ACTION _APP _IMPORT , auditSource , { app : app , backupId , fromManifest : app . manifest , toManifest : app . manifest , taskId : result . taskId } ) ;
2019-12-04 18:54:25 -08:00
2020-03-29 17:11:10 -07:00
callback ( null , { taskId : result . taskId } ) ;
2019-10-15 09:16:29 -07:00
} ) ;
} ) ;
}
2020-12-06 19:38:50 -08:00
function exportApp ( app , data , auditSource , callback ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
const appId = app . id ;
let error = checkAppState ( app , exports . ISTATE _PENDING _BACKUP ) ;
if ( error ) return callback ( error ) ;
const task = {
args : { snapshotOnly : true } ,
values : { }
} ;
addTask ( appId , exports . ISTATE _PENDING _BACKUP , task , ( error , result ) => {
if ( error ) return callback ( error ) ;
callback ( null , { taskId : result . taskId } ) ;
} ) ;
}
2019-05-05 10:31:42 -07:00
function purchaseApp ( data , callback ) {
assert . strictEqual ( typeof data , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2019-05-06 14:29:56 -07:00
appstore . purchaseApp ( data , function ( error ) {
2019-05-05 10:31:42 -07:00
if ( ! error ) return callback ( ) ;
// if purchase failed, rollback the appdb record
2019-05-05 10:46:43 -07:00
appdb . del ( data . appId , function ( delError ) {
if ( delError ) debug ( 'install: Failed to rollback app installation.' , delError ) ;
2019-05-05 10:31:42 -07:00
2019-10-24 17:28:59 -07:00
callback ( error ) ;
2019-05-05 10:31:42 -07:00
} ) ;
} ) ;
}
2020-03-29 17:11:10 -07:00
function clone ( app , data , user , auditSource , callback ) {
assert . strictEqual ( typeof app , 'object' ) ;
2016-06-17 17:12:55 -05:00
assert . strictEqual ( typeof data , 'object' ) ;
2018-09-04 16:37:08 -07:00
assert ( user && typeof user === 'object' ) ;
2016-06-17 17:12:55 -05:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2021-05-25 21:31:48 -07:00
const location = data . location . toLowerCase ( ) ,
2017-11-02 22:17:44 +01:00
domain = data . domain . toLowerCase ( ) ,
2016-06-17 17:12:55 -05:00
portBindings = data . portBindings || null ,
2018-05-13 21:02:57 -07:00
backupId = data . backupId ,
2020-03-29 17:11:10 -07:00
overwriteDns = 'overwriteDns' in data ? data . overwriteDns : false ,
2021-02-24 14:49:30 -08:00
skipDnsSetup = 'skipDnsSetup' in data ? data . skipDnsSetup : false ,
2020-03-29 17:11:10 -07:00
appId = app . id ;
2016-06-17 17:12:55 -05:00
assert . strictEqual ( typeof backupId , 'string' ) ;
assert . strictEqual ( typeof location , 'string' ) ;
2017-11-02 22:17:44 +01:00
assert . strictEqual ( typeof domain , 'string' ) ;
2016-06-17 17:12:55 -05:00
assert . strictEqual ( typeof portBindings , 'object' ) ;
2021-07-14 11:07:19 -07:00
const locations = [ { subdomain : location , domain , type : 'primary' } ] ;
validateLocations ( locations , async function ( error , domainObjectMap ) {
2018-03-02 09:57:41 +01:00
if ( error ) return callback ( error ) ;
2016-06-17 17:12:55 -05:00
2021-07-14 11:07:19 -07:00
const [ backupsError , backupInfo ] = await safe ( backups . get ( backupId ) ) ;
if ( backupsError ) return callback ( backupsError ) ;
2020-05-13 22:09:33 -07:00
if ( ! backupInfo . manifest ) return callback ( new BoxError ( BoxError . EXTERNAL _ERROR , 'Could not get restore config' ) ) ;
if ( backupInfo . encryptionVersion === 1 ) return callback ( new BoxError ( BoxError . BAD _FIELD , 'This encrypted backup was created with an older Cloudron version and cannot be cloned' ) ) ;
2016-06-17 17:12:55 -05:00
2020-03-29 17:11:10 -07:00
const manifest = backupInfo . manifest , appStoreId = app . appStoreId ;
2016-06-17 17:12:55 -05:00
2020-03-29 17:11:10 -07:00
// re-validate because this new box version may not accept old configs
error = checkManifestConstraints ( manifest ) ;
if ( error ) return callback ( error ) ;
2018-12-12 12:55:35 -08:00
2020-03-29 17:11:10 -07:00
error = validatePortBindings ( portBindings , manifest ) ;
if ( error ) return callback ( error ) ;
2016-06-17 17:12:55 -05:00
2020-03-31 15:44:46 -07:00
// should we copy the original app's mailbox settings instead?
let mailboxName = hasMailAddon ( manifest ) ? mailboxNameForLocation ( location , manifest ) : null ;
let mailboxDomain = hasMailAddon ( manifest ) ? domain : null ;
2020-03-30 22:18:39 -07:00
2021-07-14 11:07:19 -07:00
const newAppId = uuid . v4 ( ) ;
appdb . getIcons ( app . id , function ( error , icons ) {
2016-06-17 17:12:55 -05:00
if ( error ) return callback ( error ) ;
2021-07-14 11:07:19 -07:00
const data = {
installationState : exports . ISTATE _PENDING _CLONE ,
runState : exports . RSTATE _RUNNING ,
memoryLimit : app . memoryLimit ,
cpuShares : app . cpuShares ,
accessRestriction : app . accessRestriction ,
sso : ! ! app . sso ,
mailboxName : mailboxName ,
mailboxDomain : mailboxDomain ,
enableBackup : app . enableBackup ,
reverseProxyConfig : app . reverseProxyConfig ,
env : app . env ,
alternateDomains : [ ] ,
aliasDomains : [ ] ,
servicesConfig : app . servicesConfig ,
label : app . label ? ` ${ app . label } -clone ` : '' ,
tags : app . tags ,
enableAutomaticUpdate : app . enableAutomaticUpdate ,
icon : icons . icon ,
enableMailbox : app . enableMailbox
} ;
2016-06-17 17:12:55 -05:00
2021-07-14 11:07:19 -07:00
appdb . add ( newAppId , appStoreId , manifest , location , domain , translatePortBindings ( portBindings , manifest ) , data , function ( error ) {
if ( error && error . reason === BoxError . ALREADY _EXISTS ) return callback ( getDuplicateErrorDetails ( error . message , locations , domainObjectMap , portBindings ) ) ;
2020-03-29 17:11:10 -07:00
if ( error ) return callback ( error ) ;
2018-05-03 13:20:34 +02:00
2021-07-14 11:07:19 -07:00
purchaseApp ( { appId : newAppId , appstoreId : app . appStoreId , manifestId : manifest . id || 'customapp' } , function ( error ) {
2019-10-24 11:13:48 -07:00
if ( error ) return callback ( error ) ;
2016-06-17 17:12:55 -05:00
2021-07-14 11:07:19 -07:00
const restoreConfig = { backupId : backupId , backupFormat : backupInfo . format } ;
const task = {
args : { restoreConfig , overwriteDns , skipDnsSetup , oldManifest : null } ,
values : { } ,
requiredState : exports . ISTATE _PENDING _CLONE
} ;
addTask ( newAppId , exports . ISTATE _PENDING _CLONE , task , function ( error , result ) {
2019-05-05 10:31:42 -07:00
if ( error ) return callback ( error ) ;
2018-01-11 10:59:30 -08:00
2021-07-14 11:07:19 -07:00
const newApp = _ . extend ( { } , _ . omit ( data , 'icon' ) , { appStoreId , manifest , location , domain , portBindings } ) ;
newApp . fqdn = domains . fqdn ( newApp . location , domainObjectMap [ newApp . domain ] ) ;
newApp . alternateDomains . forEach ( function ( ad ) { ad . fqdn = domains . fqdn ( ad . subdomain , domainObjectMap [ ad . domain ] ) ; } ) ;
newApp . aliasDomains . forEach ( function ( ad ) { ad . fqdn = domains . fqdn ( ad . subdomain , domainObjectMap [ ad . domain ] ) ; } ) ;
2021-01-18 17:26:26 -08:00
2021-07-14 11:07:19 -07:00
eventlog . add ( eventlog . ACTION _APP _CLONE , auditSource , { appId : newAppId , oldAppId : appId , backupId : backupId , oldApp : app , newApp : newApp , taskId : result . taskId } ) ;
2020-03-29 17:11:10 -07:00
2021-07-14 11:07:19 -07:00
callback ( null , { id : newAppId , taskId : result . taskId } ) ;
2018-01-11 10:59:30 -08:00
} ) ;
2016-06-17 17:12:55 -05:00
} ) ;
} ) ;
} ) ;
} ) ;
}
2020-03-29 17:11:10 -07:00
function uninstall ( app , auditSource , callback ) {
assert . strictEqual ( typeof app , 'object' ) ;
2016-05-01 21:37:08 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2015-07-20 00:09:47 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
let error = checkAppState ( app , exports . ISTATE _PENDING _UNINSTALL ) ;
if ( error ) return callback ( error ) ;
2019-09-21 19:45:55 -07:00
2020-03-29 17:11:10 -07:00
appstore . unpurchaseApp ( appId , { appstoreId : app . appStoreId , manifestId : app . manifest . id || 'customapp' } , function ( error ) {
2019-09-21 19:45:55 -07:00
if ( error ) return callback ( error ) ;
2015-07-20 00:09:47 -07:00
2020-03-29 17:11:10 -07:00
const task = {
args : { } ,
values : { } ,
requiredState : null // can run in any state, as long as no task is active
} ;
addTask ( appId , exports . ISTATE _PENDING _UNINSTALL , task , function ( error , result ) {
2019-10-24 11:13:48 -07:00
if ( error ) return callback ( error ) ;
2016-08-04 15:53:55 +02:00
2020-03-29 17:11:10 -07:00
eventlog . add ( eventlog . ACTION _APP _UNINSTALL , auditSource , { appId , app , taskId : result . taskId } ) ;
2016-05-01 21:37:08 -07:00
2020-03-29 17:11:10 -07:00
callback ( null , { taskId : result . taskId } ) ;
2016-02-09 11:41:59 -08:00
} ) ;
2015-07-20 00:09:47 -07:00
} ) ;
}
2020-03-29 17:11:10 -07:00
function start ( app , auditSource , callback ) {
assert . strictEqual ( typeof app , 'object' ) ;
2020-03-19 17:02:42 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2015-07-20 00:09:47 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
let error = checkAppState ( app , exports . ISTATE _PENDING _START ) ;
if ( error ) return callback ( error ) ;
2019-09-21 19:45:55 -07:00
2020-03-29 17:11:10 -07:00
const task = {
args : { } ,
values : { runState : exports . RSTATE _RUNNING }
} ;
addTask ( appId , exports . ISTATE _PENDING _START , task , function ( error , result ) {
2019-09-21 19:45:55 -07:00
if ( error ) return callback ( error ) ;
2019-08-29 09:10:39 -07:00
2020-03-29 17:11:10 -07:00
eventlog . add ( eventlog . ACTION _APP _START , auditSource , { appId , app , taskId : result . taskId } ) ;
2020-03-19 17:02:42 -07:00
2020-03-29 17:11:10 -07:00
callback ( null , { taskId : result . taskId } ) ;
2015-07-20 00:09:47 -07:00
} ) ;
}
2020-03-29 17:11:10 -07:00
function stop ( app , auditSource , callback ) {
assert . strictEqual ( typeof app , 'object' ) ;
2020-03-19 17:02:42 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2015-07-20 00:09:47 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
let error = checkAppState ( app , exports . ISTATE _PENDING _STOP ) ;
if ( error ) return callback ( error ) ;
2019-09-21 19:45:55 -07:00
2020-03-29 17:11:10 -07:00
const task = {
args : { } ,
values : { runState : exports . RSTATE _STOPPED }
} ;
addTask ( appId , exports . ISTATE _PENDING _STOP , task , function ( error , result ) {
2019-09-21 19:45:55 -07:00
if ( error ) return callback ( error ) ;
2019-08-29 09:10:39 -07:00
2020-03-29 17:11:10 -07:00
eventlog . add ( eventlog . ACTION _APP _STOP , auditSource , { appId , app , taskId : result . taskId } ) ;
2020-03-19 17:02:42 -07:00
2020-03-29 17:11:10 -07:00
callback ( null , { taskId : result . taskId } ) ;
2015-07-20 00:09:47 -07:00
} ) ;
}
2020-03-29 17:11:10 -07:00
function restart ( app , auditSource , callback ) {
assert . strictEqual ( typeof app , 'object' ) ;
2020-03-19 17:02:42 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2019-12-20 10:29:29 -08:00
assert . strictEqual ( typeof callback , 'function' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
let error = checkAppState ( app , exports . ISTATE _PENDING _RESTART ) ;
if ( error ) return callback ( error ) ;
2019-12-20 10:29:29 -08:00
2020-03-29 17:11:10 -07:00
const task = {
args : { } ,
values : { runState : exports . RSTATE _RUNNING }
} ;
addTask ( appId , exports . ISTATE _PENDING _RESTART , task , function ( error , result ) {
2019-12-20 10:29:29 -08:00
if ( error ) return callback ( error ) ;
2020-03-29 17:11:10 -07:00
eventlog . add ( eventlog . ACTION _APP _RESTART , auditSource , { appId , app , taskId : result . taskId } ) ;
2020-03-19 17:02:42 -07:00
2020-03-29 17:11:10 -07:00
callback ( null , { taskId : result . taskId } ) ;
2019-12-20 10:29:29 -08:00
} ) ;
}
2015-07-20 00:09:47 -07:00
function checkManifestConstraints ( manifest ) {
2016-06-13 18:02:57 -07:00
assert ( manifest && typeof manifest === 'object' ) ;
2019-10-24 10:39:47 -07:00
if ( manifest . manifestVersion > 2 ) return new BoxError ( BoxError . BAD _FIELD , 'Manifest version must be <= 2' ) ;
2019-06-03 14:02:45 -07:00
2019-10-24 10:39:47 -07:00
if ( ! manifest . dockerImage ) return new BoxError ( BoxError . BAD _FIELD , 'Missing dockerImage' ) ; // dockerImage is optional in manifest
2015-08-19 11:08:45 -07:00
2019-07-25 14:40:52 -07:00
if ( semver . valid ( manifest . maxBoxVersion ) && semver . gt ( constants . VERSION , manifest . maxBoxVersion ) ) {
2019-10-24 10:39:47 -07:00
return new BoxError ( BoxError . BAD _FIELD , 'Box version exceeds Apps maxBoxVersion' ) ;
2015-07-20 00:09:47 -07:00
}
2019-07-25 14:40:52 -07:00
if ( semver . valid ( manifest . minBoxVersion ) && semver . gt ( manifest . minBoxVersion , constants . VERSION ) ) {
2019-10-24 10:39:47 -07:00
return new BoxError ( BoxError . BAD _FIELD , 'App version requires a new platform version' ) ;
2015-07-20 00:09:47 -07:00
}
return null ;
}
2020-03-29 17:11:10 -07:00
function exec ( app , options , callback ) {
assert . strictEqual ( typeof app , 'object' ) ;
2015-07-20 00:09:47 -07:00
assert ( options && typeof options === 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2021-05-03 22:55:43 -07:00
let cmd = options . cmd || [ '/bin/bash' ] ;
2021-05-02 11:26:08 -07:00
assert ( Array . isArray ( cmd ) && cmd . length > 0 ) ;
2015-07-20 00:09:47 -07:00
2020-03-29 17:11:10 -07:00
if ( app . installationState !== exports . ISTATE _INSTALLED || app . runState !== exports . RSTATE _RUNNING ) {
return callback ( new BoxError ( BoxError . BAD _STATE , 'App not installed or running' ) ) ;
}
2018-10-27 14:15:52 -07:00
2020-03-29 17:11:10 -07:00
var execOptions = {
AttachStdin : true ,
AttachStdout : true ,
AttachStderr : true ,
// A pseudo tty is a terminal which processes can detect (for example, disable colored output)
// Creating a pseudo terminal also assigns a terminal driver which detects control sequences
// When passing binary data, tty must be disabled. In addition, the stdout/stderr becomes a single
// unified stream because of the nature of a tty (see https://github.com/docker/docker/issues/19696)
Tty : options . tty ,
Cmd : cmd
} ;
var startOptions = {
Detach : false ,
Tty : options . tty ,
// hijacking upgrades the docker connection from http to tcp. because of this upgrade,
// we can work with half-close connections (not defined in http). this way, the client
// can properly signal that stdin is EOF by closing it's side of the socket. In http,
// the whole connection will be dropped when stdin get EOF.
// https://github.com/apocas/dockerode/commit/b4ae8a03707fad5de893f302e4972c1e758592fe
hijack : true ,
stream : true ,
stdin : true ,
stdout : true ,
stderr : true
} ;
docker . execContainer ( app . containerId , { execOptions , startOptions , rows : options . rows , columns : options . columns } , function ( error , stream ) {
if ( error && error . statusCode === 409 ) return callback ( new BoxError ( BoxError . BAD _STATE , error . message ) ) ; // container restarting/not running
if ( error ) return callback ( error ) ;
callback ( null , stream ) ;
2015-07-20 00:09:47 -07:00
} ) ;
}
2020-06-05 16:06:35 -07:00
function canAutoupdateApp ( app , updateInfo ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof updateInfo , 'object' ) ;
const manifest = updateInfo . manifest ;
2019-12-04 11:18:39 -08:00
if ( ! app . enableAutomaticUpdate ) return false ;
2020-05-11 23:20:02 +02:00
// for invalid subscriptions the appstore does not return a dockerImage
2020-06-05 16:06:35 -07:00
if ( ! manifest . dockerImage ) return false ;
2020-05-11 23:20:02 +02:00
2020-06-05 16:09:12 -07:00
if ( updateInfo . unstable ) return false ; // only manual update allowed for unstable updates
2020-06-05 16:06:35 -07:00
if ( ( semver . major ( app . manifest . version ) !== 0 ) && ( semver . major ( app . manifest . version ) !== semver . major ( manifest . version ) ) ) return false ; // major changes are blocking
2015-07-20 00:09:47 -07:00
2019-12-16 13:20:56 -08:00
if ( app . runState === exports . RSTATE _STOPPED ) return false ; // stopped apps won't run migration scripts and shouldn't be updated
2020-06-05 16:06:35 -07:00
const newTcpPorts = manifest . tcpPorts || { } ;
const newUdpPorts = manifest . udpPorts || { } ;
2019-05-06 16:31:57 +02:00
const portBindings = app . portBindings ; // this is never null
2017-07-27 13:35:07 -07:00
2019-05-06 16:31:57 +02:00
for ( let portName in portBindings ) {
2019-12-04 10:29:06 -08:00
if ( ! ( portName in newTcpPorts ) && ! ( portName in newUdpPorts ) ) return false ; // portName was in use but new update removes it
2019-05-06 16:31:57 +02:00
}
2015-07-20 00:09:47 -07:00
2019-05-06 16:31:57 +02:00
// it's fine if one or more (unused) keys got removed
2019-12-04 10:29:06 -08:00
return true ;
2019-05-06 16:31:57 +02:00
}
2015-09-10 11:39:03 -07:00
2019-05-06 16:31:57 +02:00
function autoupdateApps ( updateInfo , auditSource , callback ) { // updateInfo is { appId -> { manifest } }
assert . strictEqual ( typeof updateInfo , 'object' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2015-07-20 00:09:47 -07:00
async . eachSeries ( Object . keys ( updateInfo ) , function iterator ( appId , iteratorDone ) {
get ( appId , function ( error , app ) {
2015-09-10 11:39:03 -07:00
if ( error ) {
debug ( 'Cannot autoupdate app %s : %s' , appId , error . message ) ;
return iteratorDone ( ) ;
2017-11-15 18:45:17 -08:00
}
2015-09-10 11:39:03 -07:00
2020-06-05 16:06:35 -07:00
if ( ! canAutoupdateApp ( app , updateInfo [ appId ] ) ) {
2019-12-08 16:55:56 -08:00
debug ( ` app ${ app . fqdn } requires manual update ` ) ;
2015-07-20 00:09:47 -07:00
return iteratorDone ( ) ;
}
2016-06-04 19:06:16 -07:00
var data = {
2016-06-18 13:24:27 -05:00
manifest : updateInfo [ appId ] . manifest ,
force : false
2016-06-04 19:06:16 -07:00
} ;
2020-04-02 19:47:29 -07:00
update ( app , data , auditSource , function ( error ) {
2015-09-10 11:39:03 -07:00
if ( error ) debug ( 'Error initiating autoupdate of %s. %s' , appId , error . message ) ;
2015-07-20 00:09:47 -07:00
iteratorDone ( null ) ;
} ) ;
} ) ;
} , callback ) ;
}
2020-03-29 17:11:10 -07:00
function backup ( app , callback ) {
assert . strictEqual ( typeof app , 'object' ) ;
2015-07-20 00:09:47 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
2019-09-21 19:45:55 -07:00
2020-03-29 17:11:10 -07:00
let error = checkAppState ( app , exports . ISTATE _PENDING _BACKUP ) ;
if ( error ) return callback ( error ) ;
2015-07-20 00:09:47 -07:00
2020-03-29 17:11:10 -07:00
const task = {
args : { } ,
values : { }
} ;
addTask ( appId , exports . ISTATE _PENDING _BACKUP , task , ( error , result ) => {
if ( error ) return callback ( error ) ;
2019-08-27 20:55:49 -07:00
2020-03-29 17:11:10 -07:00
callback ( null , { taskId : result . taskId } ) ;
2015-07-20 00:09:47 -07:00
} ) ;
}
2021-07-14 11:07:19 -07:00
async function listBackups ( app , page , perPage ) {
2020-03-29 17:11:10 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2016-03-08 08:57:28 -08:00
assert ( typeof page === 'number' && page > 0 ) ;
assert ( typeof perPage === 'number' && perPage > 0 ) ;
2016-01-19 13:35:18 +01:00
2021-07-14 11:07:19 -07:00
return await backups . getByIdentifierAndStatePaged ( app . id , backups . BACKUP _STATE _NORMAL , page , perPage ) ;
2016-01-19 13:35:18 +01:00
}
2016-05-24 10:33:10 -07:00
2021-02-24 16:29:43 -08:00
function restoreInstalledApps ( options , callback ) {
assert . strictEqual ( typeof options , 'object' ) ;
2016-05-24 10:33:10 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2021-07-14 11:07:19 -07:00
const addTaskAsync = util . promisify ( addTask ) ;
getAll ( async function ( error , apps ) {
2019-10-24 10:39:47 -07:00
if ( error ) return callback ( error ) ;
2019-09-22 22:34:57 -07:00
apps = apps . filter ( app => app . installationState !== exports . ISTATE _ERROR ) ; // remove errored apps. let them be 'repaired' by hand
2019-09-24 20:29:01 -07:00
apps = apps . filter ( app => app . installationState !== exports . ISTATE _PENDING _RESTORE ) ; // safeguard against tasks being created non-stop if we crash on startup
2016-05-24 10:33:10 -07:00
2021-07-14 11:07:19 -07:00
for ( const app of apps ) {
const [ error , results ] = await safe ( backups . getByIdentifierAndStatePaged ( app . id , backups . BACKUP _STATE _NORMAL , 1 , 1 ) ) ;
let installationState , restoreConfig , oldManifest ;
if ( ! error && results . length ) {
installationState = exports . ISTATE _PENDING _RESTORE ;
restoreConfig = { backupId : results [ 0 ] . id , backupFormat : results [ 0 ] . format } ;
oldManifest = app . manifest ;
} else {
installationState = exports . ISTATE _PENDING _INSTALL ;
restoreConfig = null ;
oldManifest = null ;
}
2019-09-30 09:30:49 -07:00
2021-07-14 11:07:19 -07:00
const task = {
args : { restoreConfig , skipDnsSetup : options . skipDnsSetup , overwriteDns : true , oldManifest } ,
values : { } ,
scheduleNow : false , // task will be scheduled by autoRestartTasks when platform is ready
requireNullTaskId : false // ignore existing stale taskId
} ;
2016-06-16 06:38:47 -07:00
2021-07-14 11:07:19 -07:00
debug ( ` restoreInstalledApps: marking ${ app . fqdn } for restore using restore config ${ JSON . stringify ( restoreConfig ) } ` ) ;
2018-03-07 13:39:40 -08:00
2021-07-14 11:07:19 -07:00
const [ addTaskError , result ] = await safe ( addTaskAsync ( app . id , installationState , task ) ) ;
if ( addTaskError ) debug ( ` restoreInstalledApps: error marking ${ app . fqdn } for restore: ${ JSON . stringify ( addTaskError ) } ` ) ;
else debug ( ` restoreInstalledApps: marked ${ app . id } for restore with taskId ${ result . taskId } ` ) ;
}
2017-11-17 22:29:13 -08:00
2021-07-14 11:07:19 -07:00
callback ( null ) ;
2016-05-24 10:33:10 -07:00
} ) ;
}
function configureInstalledApps ( callback ) {
assert . strictEqual ( typeof callback , 'function' ) ;
2018-03-07 13:39:40 -08:00
getAll ( function ( error , apps ) {
2019-10-24 10:39:47 -07:00
if ( error ) return callback ( error ) ;
2019-09-22 22:34:57 -07:00
apps = apps . filter ( app => app . installationState !== exports . ISTATE _ERROR ) ; // remove errored apps. let them be 'repaired' by hand
2019-09-24 20:29:01 -07:00
apps = apps . filter ( app => app . installationState !== exports . ISTATE _PENDING _CONFIGURE ) ; // safeguard against tasks being created non-stop if we crash on startup
2016-05-24 10:33:10 -07:00
2019-09-24 20:29:01 -07:00
async . eachSeries ( apps , function ( app , iteratorDone ) {
debug ( ` configureInstalledApps: marking ${ app . fqdn } for reconfigure ` ) ;
2016-05-24 10:33:10 -07:00
2019-09-24 20:29:01 -07:00
const task = {
2019-12-06 09:23:58 -08:00
args : { } ,
2019-09-24 20:29:01 -07:00
values : { } ,
scheduleNow : false , // task will be scheduled by autoRestartTasks when platform is ready
requireNullTaskId : false // ignore existing stale taskId
} ;
2016-06-16 06:38:47 -07:00
2019-09-24 20:29:01 -07:00
addTask ( app . id , exports . ISTATE _PENDING _CONFIGURE , task , function ( error , result ) {
if ( error ) debug ( ` configureInstalledApps: error marking ${ app . fqdn } for configure: ${ JSON . stringify ( error ) } ` ) ;
else debug ( ` configureInstalledApps: marked ${ app . id } for re-configure with taskId ${ result . taskId } ` ) ;
iteratorDone ( ) ; // ignore error
2016-06-16 06:38:47 -07:00
} ) ;
2016-05-24 10:33:10 -07:00
} , callback ) ;
} ) ;
}
2017-08-18 20:45:52 -07:00
2020-05-22 16:43:16 -07:00
function restartAppsUsingAddons ( changedAddons , callback ) {
assert ( Array . isArray ( changedAddons ) ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
getAll ( function ( error , apps ) {
if ( error ) return callback ( error ) ;
apps = apps . filter ( app => app . manifest . addons && _ . intersection ( Object . keys ( app . manifest . addons ) , changedAddons ) . length !== 0 ) ;
apps = apps . filter ( app => app . installationState !== exports . ISTATE _ERROR ) ; // remove errored apps. let them be 'repaired' by hand
apps = apps . filter ( app => app . installationState !== exports . ISTATE _PENDING _RESTART ) ; // safeguard against tasks being created non-stop restart if we crash on startup
2020-05-26 07:47:57 -07:00
apps = apps . filter ( app => app . runState !== exports . RSTATE _STOPPED ) ; // don't start stopped apps
2020-05-22 16:43:16 -07:00
async . eachSeries ( apps , function ( app , iteratorDone ) {
debug ( ` restartAppsUsingAddons: marking ${ app . fqdn } for restart ` ) ;
const task = {
args : { } ,
values : { runState : exports . RSTATE _RUNNING }
} ;
2020-08-31 17:53:29 -07:00
// stop apps before updating the databases because postgres will "lock" them preventing import
docker . stopContainers ( app . id , function ( error ) {
if ( error ) debug ( ` restartAppsUsingAddons: error stopping ${ app . fqdn } ` , error ) ;
addTask ( app . id , exports . ISTATE _PENDING _RESTART , task , function ( error , result ) {
if ( error ) debug ( ` restartAppsUsingAddons: error marking ${ app . fqdn } for restart: ${ JSON . stringify ( error ) } ` ) ;
else debug ( ` restartAppsUsingAddons: marked ${ app . id } for restart with taskId ${ result . taskId } ` ) ;
iteratorDone ( ) ; // ignore error
} ) ;
2020-05-22 16:43:16 -07:00
} ) ;
} , callback ) ;
} ) ;
}
2019-09-24 20:29:01 -07:00
// auto-restart app tasks after a crash
function schedulePendingTasks ( callback ) {
2019-09-24 10:28:50 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2019-09-24 20:29:01 -07:00
debug ( 'schedulePendingTasks: scheduling app tasks' ) ;
2019-09-24 10:28:50 -07:00
getAll ( function ( error , result ) {
if ( error ) return callback ( error ) ;
result . forEach ( function ( app ) {
if ( ! app . taskId ) return ; // if not in any pending state, do nothing
2019-09-24 20:29:01 -07:00
debug ( ` schedulePendingTasks: schedule task for ${ app . fqdn } ${ app . id } : state= ${ app . installationState } ,taskId= ${ app . taskId } ` ) ;
2019-09-24 10:28:50 -07:00
scheduleTask ( app . id , app . installationState , app . taskId , NOOP _CALLBACK ) ;
} ) ;
callback ( null ) ;
} ) ;
}
2020-03-29 17:11:10 -07:00
function downloadFile ( app , filePath , callback ) {
assert . strictEqual ( typeof app , 'object' ) ;
2017-08-18 20:45:52 -07:00
assert . strictEqual ( typeof filePath , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2020-03-29 17:11:10 -07:00
exec ( app , { cmd : [ 'stat' , '--printf=%F-%s' , filePath ] , tty : true } , function ( error , stream ) {
2017-08-18 20:45:52 -07:00
if ( error ) return callback ( error ) ;
2017-08-19 10:48:12 +02:00
var data = '' ;
stream . setEncoding ( 'utf8' ) ;
stream . on ( 'data' , function ( d ) { data += d ; } ) ;
stream . on ( 'end' , function ( ) {
var parts = data . split ( '-' ) ;
2019-10-24 10:39:47 -07:00
if ( parts . length !== 2 ) return callback ( new BoxError ( BoxError . NOT _FOUND , 'file does not exist' ) ) ;
2017-08-19 10:48:12 +02:00
2017-08-20 18:44:26 -07:00
var type = parts [ 0 ] , filename , cmd , size ;
if ( type === 'regular file' ) {
cmd = [ 'cat' , filePath ] ;
size = parseInt ( parts [ 1 ] , 10 ) ;
filename = path . basename ( filePath ) ;
2019-10-24 10:39:47 -07:00
if ( isNaN ( size ) ) return callback ( new BoxError ( BoxError . NOT _FOUND , 'file does not exist' ) ) ;
2017-08-20 18:44:26 -07:00
} else if ( type === 'directory' ) {
cmd = [ 'tar' , 'zcf' , '-' , '-C' , filePath , '.' ] ;
filename = path . basename ( filePath ) + '.tar.gz' ;
size = 0 ; // unknown
} else {
2019-10-24 10:39:47 -07:00
return callback ( new BoxError ( BoxError . NOT _FOUND , 'only files or dirs can be downloaded' ) ) ;
2017-08-20 18:44:26 -07:00
}
2017-08-19 10:48:12 +02:00
2020-03-29 17:11:10 -07:00
exec ( app , { cmd : cmd , tty : false } , function ( error , stream ) {
2017-08-19 10:48:12 +02:00
if ( error ) return callback ( error ) ;
2017-08-20 23:39:49 -07:00
var stdoutStream = new TransformStream ( {
transform : function ( chunk , ignoredEncoding , callback ) {
this . _buffer = this . _buffer ? Buffer . concat ( [ this . _buffer , chunk ] ) : chunk ;
2019-06-11 12:32:15 -07:00
for ( ; ; ) {
2017-08-20 23:39:49 -07:00
if ( this . _buffer . length < 8 ) break ; // header is 8 bytes
var type = this . _buffer . readUInt8 ( 0 ) ;
var len = this . _buffer . readUInt32BE ( 4 ) ;
if ( this . _buffer . length < ( 8 + len ) ) break ; // not enough
var payload = this . _buffer . slice ( 8 , 8 + len ) ;
this . _buffer = this . _buffer . slice ( 8 + len ) ; // consumed
if ( type === 1 ) this . push ( payload ) ;
}
callback ( ) ;
}
} ) ;
stream . pipe ( stdoutStream ) ;
2020-03-29 17:11:10 -07:00
callback ( null , stdoutStream , { filename : filename , size : size } ) ;
2017-08-19 10:48:12 +02:00
} ) ;
} ) ;
2017-08-18 20:45:52 -07:00
} ) ;
}
2020-03-29 17:11:10 -07:00
function uploadFile ( app , sourceFilePath , destFilePath , callback ) {
assert . strictEqual ( typeof app , 'object' ) ;
2017-08-18 20:45:52 -07:00
assert . strictEqual ( typeof sourceFilePath , 'string' ) ;
assert . strictEqual ( typeof destFilePath , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2019-03-04 12:28:27 -08:00
const done = once ( function ( error ) {
safe . fs . unlinkSync ( sourceFilePath ) ; // remove file in /tmp
2019-12-09 15:02:51 -08:00
if ( error ) return callback ( new BoxError ( BoxError . FS _ERROR , error . message ) ) ; // blame it on filesystem for now
callback ( null ) ;
2019-03-04 12:28:27 -08:00
} ) ;
2019-03-04 11:54:48 -08:00
// the built-in bash printf understands "%q" but not /usr/bin/printf.
// ' gets replaced with '\'' . the first closes the quote and last one starts a new one
const escapedDestFilePath = safe . child _process . execSync ( ` printf %q ' ${ destFilePath . replace ( /'/g , '\'\\\'\'' ) } ' ` , { shell : '/bin/bash' , encoding : 'utf8' } ) ;
debug ( ` uploadFile: ${ sourceFilePath } -> ${ escapedDestFilePath } ` ) ;
2020-03-29 17:11:10 -07:00
exec ( app , { cmd : [ 'bash' , '-c' , ` cat - > ${ escapedDestFilePath } ` ] , tty : false } , function ( error , stream ) {
2019-03-04 12:28:27 -08:00
if ( error ) return done ( error ) ;
2017-08-18 20:45:52 -07:00
var readFile = fs . createReadStream ( sourceFilePath ) ;
2019-03-04 12:28:27 -08:00
readFile . on ( 'error' , done ) ;
2017-08-18 20:45:52 -07:00
2019-03-04 12:28:27 -08:00
stream . on ( 'error' , done ) ;
stream . on ( 'finish' , done ) ;
2017-08-18 20:45:52 -07:00
2019-03-04 12:28:27 -08:00
readFile . pipe ( stream ) ;
2017-08-18 20:45:52 -07:00
} ) ;
}
2021-05-25 21:31:48 -07:00
function backupConfig ( app , callback ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
if ( ! safe . fs . writeFileSync ( path . join ( paths . APPS _DATA _DIR , app . id + '/config.json' ) , JSON . stringify ( app ) ) ) {
return callback ( new BoxError ( BoxError . FS _ERROR , 'Error creating config.json: ' + safe . error . message ) ) ;
}
appdb . getIcons ( app . id , function ( error , icons ) {
if ( ! error && icons . icon ) safe . fs . writeFileSync ( path . join ( paths . APPS _DATA _DIR , app . id + '/icon.png' ) , icons . icon ) ;
callback ( null ) ;
} ) ;
}
2021-05-26 09:48:34 -07:00
function restoreConfig ( app , callback ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
const appConfig = safe . JSON . parse ( safe . fs . readFileSync ( path . join ( paths . APPS _DATA _DIR , app . id + '/config.json' ) ) ) ;
let data = { } ;
if ( appConfig ) {
data = _ . pick ( appConfig , 'memoryLimit' , 'cpuShares' , 'enableBackup' , 'reverseProxyConfig' , 'env' , 'servicesConfig' , 'label' , 'tags' , 'enableAutomaticUpdate' ) ;
}
const icon = safe . fs . readFileSync ( path . join ( paths . APPS _DATA _DIR , app . id + '/icon.png' ) ) ;
if ( icon ) data . icon = icon ;
appdb . update ( app . id , data , callback ) ;
}