2015-07-20 00:09:47 -07:00
'use strict' ;
exports = module . exports = {
2021-09-21 10:00:47 -07:00
canAccess ,
2021-09-21 10:11:27 -07:00
isOperator ,
2021-09-21 17:28:58 -07:00
accessLevel ,
2024-12-19 12:10:22 +01:00
pickFields ,
2020-10-27 17:11:50 -07:00
2021-08-20 09:19:44 -07:00
// database crud
add ,
update ,
setHealth ,
del ,
2020-10-27 17:11:50 -07:00
get ,
getByIpAddress ,
getByFqdn ,
2021-08-20 09:19:44 -07:00
list ,
listByUser ,
// user actions
2020-10-27 17:11:50 -07:00
install ,
2024-12-10 17:19:12 +01:00
unarchive ,
2020-10-27 17:11:50 -07:00
uninstall ,
2024-12-09 18:28:35 +01:00
archive ,
2020-10-27 17:11:50 -07:00
setAccessRestriction ,
2021-09-21 10:11:27 -07:00
setOperators ,
2021-09-27 14:21:42 -07:00
setCrontab ,
2022-06-08 11:21:09 +02:00
setUpstreamUri ,
2020-10-27 17:11:50 -07:00
setLabel ,
setIcon ,
setTags ,
2024-04-10 17:02:32 +02:00
setNotes ,
2024-06-24 18:39:37 +02:00
setChecklistItem ,
2020-10-27 17:11:50 -07:00
setMemoryLimit ,
2024-04-10 17:38:49 +02:00
setCpuQuota ,
2020-10-28 19:42:48 -07:00
setMounts ,
2024-12-05 13:47:59 +01:00
setDevices ,
2020-10-27 17:11:50 -07:00
setAutomaticBackup ,
setAutomaticUpdate ,
setReverseProxyConfig ,
setCertificate ,
setDebugMode ,
setEnvironment ,
setMailbox ,
2021-10-01 12:09:13 -07:00
setInbox ,
2023-07-13 15:06:07 +05:30
setTurn ,
2023-07-13 16:37:33 +05:30
setRedis ,
2020-10-27 17:11:50 -07:00
setLocation ,
2022-06-01 22:44:52 -07:00
setStorage ,
2020-10-27 17:11:50 -07:00
repair ,
restore ,
importApp ,
2020-12-06 19:38:50 -08:00
exportApp ,
2020-10-27 17:11:50 -07:00
clone ,
2021-08-20 09:19:44 -07:00
updateApp ,
2020-10-27 17:11:50 -07:00
backup ,
listBackups ,
2022-04-02 17:09:08 -07:00
updateBackup ,
2022-11-03 22:13:57 +01:00
getBackupDownloadStream ,
2020-10-27 17:11:50 -07:00
2021-09-21 22:19:20 -07:00
getTask ,
2022-01-04 09:12:45 -08:00
getLogPaths ,
2020-10-27 17:11:50 -07:00
getLogs ,
2022-06-09 14:56:40 +02:00
appendLogLine ,
2020-10-27 17:11:50 -07:00
start ,
stop ,
restart ,
2022-05-16 10:26:30 -07:00
createExec ,
startExec ,
getExec ,
2020-10-27 17:11:50 -07:00
2024-03-30 18:51:19 +01:00
checkManifest ,
2020-10-27 17:11:50 -07:00
2023-08-21 18:18:03 +05:30
restoreApps ,
configureApps ,
2020-10-27 17:11:50 -07:00
schedulePendingTasks ,
restartAppsUsingAddons ,
2022-06-01 22:44:52 -07:00
getStorageDir ,
2021-04-30 13:18:15 -07:00
getIcon ,
2021-01-20 12:12:14 -08:00
getMemoryLimit ,
2021-09-27 14:21:42 -07:00
getSchedulerConfig ,
2020-10-27 17:11:50 -07:00
2021-09-21 19:45:29 -07:00
listEventlog ,
2020-10-27 17:11:50 -07:00
downloadFile ,
2024-07-22 22:27:41 +02:00
uploadFile ,
2017-08-18 20:45:52 -07:00
2024-02-10 11:53:25 +01:00
writeConfig ,
loadConfig ,
2021-05-25 21:31:48 -07:00
2025-07-18 10:56:52 +02:00
canBackupApp ,
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' ,
2023-07-13 15:06:07 +05:30
ISTATE _PENDING _SERVICES _CHANGE : 'pending_services_change' ,
2019-09-08 16:57:08 -07:00
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
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' ,
2024-12-19 12:03:17 +01:00
// app access levels
ACCESS _LEVEL _ADMIN : 'admin' ,
ACCESS _LEVEL _OPERATOR : 'operator' ,
ACCESS _LEVEL _USER : 'user' ,
ACCESS _LEVEL _NONE : '' ,
2015-07-20 00:09:47 -07:00
// exported for testing
2024-02-28 14:56:04 +01:00
_checkForPortBindingConflict : checkForPortBindingConflict ,
2024-07-16 22:21:36 +02:00
_validatePorts : validatePorts ,
2018-08-12 19:33:11 -07:00
_validateAccessRestriction : validateAccessRestriction ,
2022-11-23 14:36:57 +01:00
_validateUpstreamUri : validateUpstreamUri ,
2023-08-01 19:33:59 +05:30
_validateLocations : validateLocations ,
2021-09-27 14:21:42 -07:00
_parseCrontab : parseCrontab ,
2021-08-20 09:19:44 -07:00
_clear : clear
2015-07-20 00:09:47 -07:00
} ;
2025-05-21 17:15:04 +02:00
const appTaskManager = require ( './apptaskmanager.js' ) ,
2024-12-10 10:06:52 +01:00
archives = require ( './archives.js' ) ,
2015-07-20 00:09:47 -07:00
assert = require ( 'assert' ) ,
2025-07-25 01:34:29 +02:00
backups = require ( './backups.js' ) ,
2025-07-24 18:46:21 +02:00
backupTargets = require ( './backuptargets.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' ) ,
2025-07-28 12:53:27 +02:00
crypto = require ( 'crypto' ) ,
2024-04-19 18:19:41 +02:00
{ CronTime } = require ( 'cron' ) ,
2023-08-11 19:41:05 +05:30
dashboard = require ( './dashboard.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' ) ,
2021-08-13 17:22:28 -07:00
dns = require ( './dns.js' ) ,
2015-11-10 12:47:48 -08:00
docker = require ( './docker.js' ) ,
2018-01-09 21:03:59 -08:00
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' ) ,
2023-08-17 16:05:19 +05:30
Location = require ( './location.js' ) ,
2025-07-18 18:11:56 +02:00
locks = require ( './locks.js' ) ,
2023-03-27 10:38:09 +02:00
logs = require ( './logs.js' ) ,
2018-05-24 16:25:32 -07:00
mail = require ( './mail.js' ) ,
2025-07-10 11:16:48 +02:00
manifestFormat = require ( '@cloudron/manifest-format' ) ,
2022-08-10 12:19:42 +02:00
notifications = require ( './notifications.js' ) ,
2022-04-15 19:01:35 -05:00
once = require ( './once.js' ) ,
2015-07-20 00:09:47 -07:00
path = require ( 'path' ) ,
paths = require ( './paths.js' ) ,
2023-07-24 22:25:06 +05:30
PassThrough = require ( 'stream' ) . PassThrough ,
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' ) ,
2024-10-14 19:10:31 +02:00
shell = require ( './shell.js' ) ( 'apps' ) ,
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' ) ,
2022-06-01 22:44:52 -07:00
volumes = require ( './volumes.js' ) ,
2025-02-13 14:03:25 +01:00
_ = require ( './underscore.js' ) ;
2015-07-20 00:09:47 -07:00
2024-12-10 17:16:06 +01:00
// NOTE: when adding fields here, update the clone and unarchive logic as well
2021-08-20 09:19:44 -07:00
const APPS _FIELDS _PREFIXED = [ 'apps.id' , 'apps.appStoreId' , 'apps.installationState' , 'apps.errorJson' , 'apps.runState' ,
2024-04-10 17:38:49 +02:00
'apps.health' , 'apps.containerId' , 'apps.manifestJson' , 'apps.accessRestrictionJson' , 'apps.memoryLimit' , 'apps.cpuQuota' ,
2024-04-10 17:02:32 +02:00
'apps.label' , 'apps.notes' , 'apps.tagsJson' , 'apps.taskId' , 'apps.reverseProxyConfigJson' , 'apps.servicesConfigJson' , 'apps.operatorsJson' ,
2024-12-05 13:47:59 +01:00
'apps.sso' , 'apps.devicesJson' , 'apps.debugModeJson' , 'apps.enableBackup' , 'apps.proxyAuth' , 'apps.containerIp' , 'apps.crontab' ,
2025-06-26 15:19:28 +02:00
'apps.creationTime' , 'apps.updateTime' , 'apps.enableAutomaticUpdate' , 'apps.upstreamUri' , 'apps.checklistJson' , 'apps.updateInfoJson' ,
2022-05-31 17:53:09 -07:00
'apps.enableMailbox' , 'apps.mailboxDisplayName' , 'apps.mailboxName' , 'apps.mailboxDomain' , 'apps.enableInbox' , 'apps.inboxName' , 'apps.inboxDomain' ,
2023-07-13 16:37:33 +05:30
'apps.enableTurn' , 'apps.enableRedis' , 'apps.storageVolumeId' , 'apps.storageVolumePrefix' , 'apps.ts' , 'apps.healthTime' , '(apps.icon IS NOT NULL) AS hasIcon' , '(apps.appStoreIcon IS NOT NULL) AS hasAppStoreIcon' ] . join ( ',' ) ;
2021-08-20 09:19:44 -07:00
2024-07-15 22:59:16 +02:00
// const PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId', 'count' ].join(',');
2022-11-28 22:32:34 +01:00
const LOCATION _FIELDS = [ 'appId' , 'subdomain' , 'domain' , 'type' , 'certificateJson' ] ;
2019-08-27 22:39:59 -07:00
2022-06-03 10:56:50 -07:00
const CHECKVOLUME _CMD = path . join ( _ _dirname , 'scripts/checkvolume.sh' ) ;
2024-07-16 22:21:36 +02:00
// ports is a map of envvar -> hostPort
function validatePorts ( ports , manifest ) {
assert . strictEqual ( typeof ports , '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 */
2021-10-07 21:53:43 -07:00
995 , /* pop3s */
2015-07-20 00:09:47 -07:00
2003 , /* graphite (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 */
2015-07-20 00:09:47 -07:00
] ;
2020-03-30 10:01:52 +02:00
const RESERVED _PORT _RANGES = [
2025-06-04 13:23:47 +02:00
[ constants . TURN _UDP _PORT _START , constants . TURN _UDP _PORT _END ] /* turn udp ports */
2020-03-30 10:01:52 +02:00
] ;
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
] ;
2024-07-16 22:21:36 +02:00
if ( ! ports ) return null ;
2015-07-20 00:09:47 -07:00
2024-02-27 13:19:19 +01:00
const tcpPorts = manifest . tcpPorts || { } ;
const udpPorts = manifest . udpPorts || { } ;
2022-08-10 14:22:31 +02:00
2024-07-16 22:21:36 +02:00
for ( const portName in ports ) {
if ( ! /^[A-Z0-9_]+$/ . test ( portName ) ) return new BoxError ( BoxError . BAD _FIELD , ` ${ portName } is not a valid environment variable in ports ` ) ;
2015-07-20 00:09:47 -07:00
2024-07-16 22:21:36 +02:00
const hostPort = ports [ portName ] ;
if ( ! Number . isInteger ( hostPort ) ) return new BoxError ( BoxError . BAD _FIELD , ` ${ hostPort } is not an integer in ${ portName } ports ` ) ;
if ( RESERVED _PORTS . indexOf ( hostPort ) !== - 1 ) return new BoxError ( BoxError . BAD _FIELD , ` Port ${ hostPort } for ${ portName } is reserved in ports ` ) ;
if ( RESERVED _PORT _RANGES . find ( range => ( hostPort >= range [ 0 ] && hostPort <= range [ 1 ] ) ) ) return new BoxError ( BoxError . BAD _FIELD , ` Port ${ hostPort } for ${ portName } is reserved in ports ` ) ;
if ( ALLOWED _PORTS . indexOf ( hostPort ) === - 1 && ( hostPort <= 1023 || hostPort > 65535 ) ) return new BoxError ( BoxError . BAD _FIELD , ` ${ hostPort } for ${ portName } is not in permitted range in ports ` ) ;
2015-07-20 00:09:47 -07:00
2024-07-16 22:21:36 +02:00
// it is OK if there is no 1-1 mapping between values in manifest.tcpPorts and ports. missing values implies the service is disabled
2022-08-10 14:22:31 +02:00
const portSpec = tcpPorts [ portName ] || udpPorts [ portName ] ;
if ( ! portSpec ) return new BoxError ( BoxError . BAD _FIELD , ` Invalid portBinding ${ portName } ` ) ;
if ( portSpec . readOnly && portSpec . defaultValue !== hostPort ) return new BoxError ( BoxError . BAD _FIELD , ` portBinding ${ portName } is readOnly and cannot have a different value that the default ` ) ;
2024-07-16 22:21:36 +02:00
if ( ( hostPort + ( portSpec . portCount || 1 ) ) > 65535 ) return new BoxError ( BoxError . BAD _FIELD , ` ${ hostPort } + ${ portSpec . portCount } for ${ portName } exceeds valid port range ` ) ;
2015-07-20 00:09:47 -07:00
}
return null ;
}
2024-07-16 22:21:36 +02:00
// translates the REST API ports (envvar -> hostPort) to database portBindings (envvar -> { hostPort, count, type })
function translateToPortBindings ( ports , manifest ) {
assert . strictEqual ( typeof ports , 'object' ) ;
2018-08-13 08:33:09 -07:00
assert . strictEqual ( typeof manifest , 'object' ) ;
2024-07-16 22:21:36 +02:00
const portBindings = { } ;
2018-08-12 19:33:11 -07:00
2024-07-16 22:21:36 +02:00
if ( ! ports ) return portBindings ;
2018-08-13 08:33:09 -07:00
2024-07-16 22:21:36 +02:00
const tcpPorts = manifest . tcpPorts || { } ;
for ( const portName in ports ) {
2018-08-13 08:33:09 -07:00
const portType = portName in tcpPorts ? exports . PORT _TYPE _TCP : exports . PORT _TYPE _UDP ;
2024-07-16 22:21:36 +02:00
const portCount = portName in tcpPorts ? tcpPorts [ portName ] . portCount : manifest . udpPorts [ portName ] . portCount ; // since count is optional, this can be undefined
portBindings [ portName ] = { hostPort : ports [ portName ] , type : portType , count : portCount || 1 } ;
2018-08-12 19:33:11 -07:00
}
2024-02-25 14:33:57 +01:00
2024-07-16 22:21:36 +02:00
return portBindings ;
2018-08-12 19:33:11 -07:00
}
2022-01-14 22:40:51 -08:00
function validateSecondaryDomains ( secondaryDomains , manifest ) {
assert . strictEqual ( typeof secondaryDomains , 'object' ) ;
assert . strictEqual ( typeof manifest , 'object' ) ;
const httpPorts = manifest . httpPorts || { } ;
2022-01-20 16:57:30 -08:00
for ( const envName in httpPorts ) { // each httpPort is required
if ( ! ( envName in secondaryDomains ) ) return new BoxError ( BoxError . BAD _FIELD , ` secondaryDomain ${ envName } is required ` ) ;
}
2022-01-14 22:40:51 -08:00
for ( const envName in secondaryDomains ) {
2022-01-25 16:41:29 -08:00
if ( ! ( envName in httpPorts ) ) return new BoxError ( BoxError . BAD _FIELD , ` secondaryDomain ${ envName } is not listed in manifest ` ) ;
2022-01-14 22:40:51 -08:00
}
return null ;
}
function translateSecondaryDomains ( secondaryDomains ) {
2022-01-20 16:57:30 -08:00
assert ( secondaryDomains && typeof secondaryDomains === 'object' ) ;
2022-01-14 22:40:51 -08:00
const result = [ ] ;
for ( const envName in secondaryDomains ) {
2022-01-20 16:57:30 -08:00
result . push ( { domain : secondaryDomains [ envName ] . domain , subdomain : secondaryDomains [ envName ] . subdomain , environmentVariable : envName } ) ;
2022-01-14 22:40:51 -08:00
}
return result ;
}
2021-09-27 14:21:42 -07:00
function parseCrontab ( crontab ) {
assert ( crontab === null || typeof crontab === 'string' ) ;
2022-05-20 09:31:58 -07:00
// https://www.man7.org/linux/man-pages/man5/crontab.5.html#EXTENSIONS
const KNOWN _EXTENSIONS = {
2022-05-20 10:57:44 -07:00
'@service' : '@service' , // runs once
'@reboot' : '@service' ,
2022-05-20 09:31:58 -07:00
'@yearly' : '0 0 1 1 *' ,
'@annually' : '0 0 1 1 *' ,
'@monthly' : '0 0 1 * *' ,
'@weekly' : '0 0 * * 0' ,
'@daily' : '0 0 * * *' ,
'@hourly' : '0 * * * *' ,
} ;
2021-09-27 14:21:42 -07:00
const result = [ ] ;
if ( ! crontab ) return result ;
const lines = crontab . split ( '\n' ) ;
for ( let i = 0 ; i < lines . length ; i ++ ) {
const line = lines [ i ] . trim ( ) ;
if ( ! line || line . startsWith ( '#' ) ) continue ;
2022-05-20 09:31:58 -07:00
if ( line . startsWith ( '@' ) ) {
const parts = /^(@\S+)\s+(.+)$/ . exec ( line ) ;
if ( ! parts ) throw new BoxError ( BoxError . BAD _FIELD , ` Invalid cron configuration at line ${ i + 1 } ` ) ;
const [ , extension , command ] = parts ;
if ( ! KNOWN _EXTENSIONS [ extension ] ) throw new BoxError ( BoxError . BAD _FIELD , ` Unknown extension pattern at line ${ i + 1 } ` ) ;
result . push ( { schedule : KNOWN _EXTENSIONS [ extension ] , command } ) ;
} else {
const parts = /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.+)$/ . exec ( line ) ;
if ( ! parts ) throw new BoxError ( BoxError . BAD _FIELD , ` Invalid cron configuration at line ${ i + 1 } ` ) ;
const schedule = parts . slice ( 1 , 6 ) . join ( ' ' ) ;
const command = parts [ 6 ] ;
try {
2024-04-19 18:19:41 +02:00
new CronTime ( '00 ' + schedule ) ; // second is disallowed
2024-05-13 08:43:28 +02:00
} catch ( error ) {
throw new BoxError ( BoxError . BAD _FIELD , ` Invalid cron pattern at line ${ i + 1 } : ${ error . message } ` ) ;
2022-05-20 09:31:58 -07:00
}
if ( command . length === 0 ) throw new BoxError ( BoxError . BAD _FIELD , ` Invalid cron pattern. Command must not be empty at line ${ i + 1 } ` ) ; // not possible with the regexp we have
result . push ( { schedule , command } ) ;
2021-09-27 14:21:42 -07:00
}
}
return result ;
}
function getSchedulerConfig ( app ) {
assert . strictEqual ( typeof app , 'object' ) ;
let schedulerConfig = app . manifest . addons ? . scheduler || null ;
const crontab = parseCrontab ( app . crontab ) ;
if ( crontab . length === 0 ) return schedulerConfig ;
schedulerConfig = schedulerConfig || { } ;
// put a '.' because it is not a valid name for schedule name in manifestformat
crontab . forEach ( ( c , idx ) => schedulerConfig [ ` crontab. ${ idx } ` ] = c ) ;
return schedulerConfig ;
}
2021-09-21 10:11:27 -07:00
// also validates operators
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' ) ;
2024-04-09 18:59:40 +02:00
// max is not checked because docker allows any value for --memory
2022-02-07 16:09:43 -08:00
const min = manifest . memoryLimit || constants . DEFAULT _MEMORY _LIMIT ;
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' ) ;
2016-02-11 17:39:15 +01:00
return null ;
}
2024-04-10 17:38:49 +02:00
function validateCpuQuota ( cpuQuota ) {
assert . strictEqual ( typeof cpuQuota , 'number' ) ;
2020-01-28 21:30:35 -08:00
2024-04-10 17:38:49 +02:00
if ( cpuQuota < 1 || cpuQuota > 100 ) return new BoxError ( BoxError . BAD _FIELD , 'cpuQuota has to be between 1 and 100' ) ;
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
2022-02-07 13:19:59 -08:00
if ( robotsTxt . length > 4096 ) return new BoxError ( BoxError . BAD _FIELD , 'robotsTxt must be less than 4096' ) ;
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
2022-02-07 13:19:59 -08:00
if ( csp . length > 4096 ) return new BoxError ( BoxError . BAD _FIELD , 'CSP must be less than 4096' ) ;
if ( csp . includes ( '"' ) ) return new BoxError ( BoxError . BAD _FIELD , 'CSP cannot contains double quotes' ) ;
2023-10-18 13:53:21 +02:00
if ( csp . includes ( '\n' ) ) return new BoxError ( BoxError . BAD _FIELD , 'CSP cannot contain newlines' ) ;
2019-10-13 18:22:03 -07:00
return null ;
}
2022-06-08 11:21:09 +02:00
function validateUpstreamUri ( upstreamUri ) {
2022-06-10 11:23:58 -07:00
assert . strictEqual ( typeof upstreamUri , 'string' ) ;
2022-06-08 11:21:09 +02:00
2022-11-23 12:53:21 +01:00
if ( ! upstreamUri ) return new BoxError ( BoxError . BAD _FIELD , 'upstreamUri cannot be empty' ) ;
2022-09-29 18:39:58 +02:00
const uri = safe ( ( ) => new URL ( upstreamUri ) ) ;
if ( ! uri ) return new BoxError ( BoxError . BAD _FIELD , ` upstreamUri is invalid: ${ safe . error . message } ` ) ;
if ( uri . protocol !== 'http:' && uri . protocol !== 'https:' ) return new BoxError ( BoxError . BAD _FIELD , 'upstreamUri has an unsupported scheme' ) ;
if ( uri . search || uri . hash ) return new BoxError ( BoxError . BAD _FIELD , 'upstreamUri cannot have search or hash' ) ;
if ( uri . pathname !== '/' ) return new BoxError ( BoxError . BAD _FIELD , 'upstreamUri cannot have a path' ) ;
// we use the uri in a named location @wellknown-upstream. nginx does not support having paths in it
if ( upstreamUri . endsWith ( '/' ) ) return new BoxError ( BoxError . BAD _FIELD , 'upstreamUri cannot have a path' ) ;
2022-06-08 11:21:09 +02:00
return null ;
}
2019-03-22 07:48:31 -07:00
function validateLabel ( label ) {
if ( label === null ) return null ;
2022-02-07 13:19:59 -08:00
if ( label . length > 128 ) return new BoxError ( BoxError . BAD _FIELD , 'label must be less than 128' ) ;
2019-03-22 07:48:31 -07:00
return null ;
}
function validateTags ( tags ) {
2022-02-07 13:19:59 -08:00
if ( tags . length > 64 ) return new BoxError ( BoxError . BAD _FIELD , 'Can only set up to 64 tags' ) ;
2019-03-22 07:48:31 -07:00
2022-02-07 13:19:59 -08:00
if ( tags . some ( tag => tag . length == 0 ) ) return new BoxError ( BoxError . BAD _FIELD , 'tag cannot be empty' ) ;
if ( tags . some ( tag => tag . length > 128 ) ) return new BoxError ( BoxError . BAD _FIELD , 'tag must be less than 128' ) ;
2019-03-22 07:48:31 -07:00
return null ;
}
2024-12-05 13:47:59 +01:00
function validateDevices ( devices ) {
for ( const key in devices ) {
2024-12-06 12:51:42 +01:00
if ( key . indexOf ( '/dev/' ) !== 0 ) return new BoxError ( BoxError . BAD _FIELD , ` " ${ key } " must start with /dev/ ` ) ;
2024-12-05 13:47:59 +01:00
}
return null ;
}
2018-10-18 11:19:32 -07:00
function validateEnv ( env ) {
2024-05-13 08:43:28 +02:00
for ( const key in env ) {
2022-02-07 13:19:59 -08:00
if ( key . length > 512 ) return new BoxError ( BoxError . BAD _FIELD , 'Max env var key length is 512' ) ;
2018-10-18 11:19:32 -07:00
// http://pubs.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap08.html
2022-02-07 13:19:59 -08:00
if ( ! /^[a-zA-Z_][a-zA-Z0-9_]*$/ . test ( key ) ) return new BoxError ( BoxError . BAD _FIELD , ` " ${ key } " is not a valid environment variable ` ) ;
2018-10-18 11:19:32 -07:00
}
return null ;
}
2022-06-08 12:24:11 -07:00
async function checkStorage ( app , volumeId , prefix ) {
assert . strictEqual ( typeof app , 'object' ) ;
2022-06-03 10:56:50 -07:00
assert . strictEqual ( typeof volumeId , 'string' ) ;
2022-06-01 22:44:52 -07:00
assert . strictEqual ( typeof prefix , 'string' ) ;
2018-12-20 14:33:29 -08:00
2022-06-03 10:56:50 -07:00
const volume = await volumes . get ( volumeId ) ;
if ( volume === null ) throw new BoxError ( BoxError . BAD _FIELD , 'Storage volume not found' ) ;
2023-07-11 20:52:46 +05:30
// lack of file perms makes these unsupported
if ( volume . mountType === 'cifs' || volume . mountType === 'sshfs' ) throw new BoxError ( BoxError . BAD _FIELD , ` ${ volume . mountType } volumes cannot be used as data directory ` ) ;
2022-06-03 10:56:50 -07:00
const status = await volumes . getStatus ( volume ) ;
if ( status . state !== 'active' ) throw new BoxError ( BoxError . BAD _FIELD , 'Volume is not active' ) ;
if ( path . isAbsolute ( prefix ) ) throw new BoxError ( BoxError . BAD _FIELD , ` prefix " ${ prefix } " must be a relative path ` ) ;
if ( prefix . endsWith ( '/' ) ) throw new BoxError ( BoxError . BAD _FIELD , ` prefix " ${ prefix } " contains trailing slash ` ) ;
if ( prefix !== '' && path . normalize ( prefix ) !== prefix ) throw new BoxError ( BoxError . BAD _FIELD , ` prefix " ${ prefix } " is not a normalized path ` ) ;
2022-06-08 12:24:11 -07:00
const sourceDir = await getStorageDir ( app ) ;
2024-06-06 15:22:33 +02:00
if ( sourceDir === null ) throw new BoxError ( BoxError . BAD _STATE , 'App does not use localstorage addon' ) ;
2022-06-08 12:24:11 -07:00
const targetDir = path . join ( volume . hostPath , prefix ) ;
const rel = path . relative ( sourceDir , targetDir ) ;
if ( ! rel . startsWith ( '../' ) && rel . split ( '/' ) . length > 1 ) throw new BoxError ( BoxError . BAD _FIELD , 'Only one level subdirectory moves are supported' ) ;
2025-07-16 21:53:22 +02:00
const [ error ] = await safe ( shell . sudo ( [ CHECKVOLUME _CMD , targetDir , sourceDir ] , { } ) ) ;
2022-06-08 12:24:11 -07:00
if ( error && error . code === 2 ) throw new BoxError ( BoxError . BAD _FIELD , ` Target directory ${ targetDir } is not empty ` ) ;
if ( error && error . code === 3 ) throw new BoxError ( BoxError . BAD _FIELD , ` Target directory ${ targetDir } does not support chown ` ) ;
2019-01-19 22:19:43 -08:00
2018-12-20 14:33:29 -08:00
return null ;
}
2022-11-28 22:14:10 +01:00
function getDuplicateErrorDetails ( errorMessage , locations , 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 ) ) ;
2015-07-20 00:09:47 -07:00
assert . strictEqual ( typeof portBindings , 'object' ) ;
2022-02-07 16:09:43 -08:00
const 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
2022-02-07 16:09:43 -08:00
if ( match [ 2 ] === 'locations.subdomain' ) {
2019-09-27 10:25:26 -07:00
for ( let i = 0 ; i < locations . length ; i ++ ) {
2022-02-07 16:09:43 -08:00
const { subdomain , domain , type } = locations [ i ] ;
2023-01-09 13:27:02 +01:00
if ( match [ 1 ] !== ( subdomain ? ` ${ subdomain } - ${ domain } ` : domain ) ) continue ;
2019-06-05 16:01:44 +02:00
2022-11-28 21:23:06 +01:00
return new BoxError ( BoxError . ALREADY _EXISTS , ` ${ type } location ' ${ dns . fqdn ( subdomain , domain ) } ' is in use ` ) ;
2019-09-01 21:34:27 -07:00
}
2019-03-19 20:43:42 -07:00
}
2015-07-20 00:09:47 -07:00
2022-02-07 16:09:43 -08:00
for ( const portName in portBindings ) {
2024-07-16 22:21:36 +02:00
if ( portBindings [ portName ] . hostPort === parseInt ( match [ 1 ] ) ) return new BoxError ( BoxError . ALREADY _EXISTS , ` Port ${ match [ 1 ] } is in use ` ) ;
2015-07-20 00:09:47 -07:00
}
2022-06-01 22:44:52 -07:00
if ( match [ 2 ] === 'apps_storageVolume' ) {
return new BoxError ( BoxError . BAD _FIELD , ` Storage directory ${ match [ 1 ] } is in use ` ) ;
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
}
2022-06-01 22:44:52 -07:00
async function getStorageDir ( app ) {
assert . strictEqual ( typeof app , 'object' ) ;
2019-09-15 21:51:38 -07:00
2024-06-06 15:22:33 +02:00
if ( ! app . manifest . addons ? . localstorage ) return null ;
2022-06-01 22:44:52 -07:00
if ( ! app . storageVolumeId ) return path . join ( paths . APPS _DATA _DIR , app . id , 'data' ) ;
const volume = await volumes . get ( app . storageVolumeId ) ;
if ( ! volume ) throw new BoxError ( BoxError . NOT _FOUND , 'Volume not found' ) ; // not possible
return path . join ( volume . hostPath , app . storageVolumePrefix ) ;
2018-12-20 14:33:29 -08:00
}
2024-12-19 12:10:22 +01:00
function pickFields ( app , accessLevel ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof accessLevel , 'string' ) ;
if ( accessLevel === exports . ACCESS _LEVEL _NONE ) return null ; // cannot happen!
let result ;
if ( accessLevel === exports . ACCESS _LEVEL _USER ) {
2025-02-13 14:03:25 +01:00
result = _ . pick ( app , [
2024-12-19 12:10:22 +01:00
'id' , 'appStoreId' , 'installationState' , 'error' , 'runState' , 'health' , 'taskId' , 'accessRestriction' ,
'secondaryDomains' , 'redirectDomains' , 'aliasDomains' , 'sso' , 'subdomain' , 'domain' , 'fqdn' , 'certificate' ,
2025-02-13 14:03:25 +01:00
'manifest' , 'portBindings' , 'iconUrl' , 'creationTime' , 'ts' , 'tags' , 'label' , 'upstreamUri' ] ) ;
2024-12-19 12:10:22 +01:00
} else { // admin or operator
2025-02-13 14:03:25 +01:00
result = _ . pick ( app , [
2024-12-19 12:10:22 +01:00
'id' , 'appStoreId' , 'installationState' , 'error' , 'runState' , 'health' , 'taskId' ,
'subdomain' , 'domain' , 'fqdn' , 'certificate' , 'crontab' , 'upstreamUri' ,
'accessRestriction' , 'manifest' , 'portBindings' , 'iconUrl' , 'memoryLimit' , 'cpuQuota' , 'operators' ,
'sso' , 'debugMode' , 'reverseProxyConfig' , 'enableBackup' , 'creationTime' , 'updateTime' , 'ts' , 'tags' ,
'label' , 'notes' , 'secondaryDomains' , 'redirectDomains' , 'aliasDomains' , 'devices' , 'env' , 'enableAutomaticUpdate' ,
'storageVolumeId' , 'storageVolumePrefix' , 'mounts' , 'enableTurn' , 'enableRedis' , 'checklist' ,
2025-06-20 18:51:12 +02:00
'enableMailbox' , 'mailboxDisplayName' , 'mailboxName' , 'mailboxDomain' , 'enableInbox' , 'inboxName' , 'inboxDomain' , 'updateInfo' ] ) ;
2024-12-19 12:10:22 +01:00
}
2018-04-29 10:47:34 -07:00
2024-12-19 12:10:22 +01:00
// remove private certificate key
if ( result . certificate ) delete result . certificate . key ;
result . secondaryDomains . forEach ( sd => { if ( sd . certificate ) delete sd . certificate . key ; } ) ;
result . aliasDomains . forEach ( ad => { if ( ad . certificate ) delete ad . certificate . key ; } ) ;
result . redirectDomains . forEach ( rd => { if ( rd . certificate ) delete rd . certificate . key ; } ) ;
2022-07-14 11:57:04 +05:30
return result ;
2018-06-25 16:45:15 -07:00
}
2021-08-20 09:19:44 -07:00
async function getIcon ( app , options ) {
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' ) ;
2020-03-29 17:11:10 -07:00
2021-08-20 09:19:44 -07:00
const icons = await getIcons ( app . id ) ;
if ( ! icons ) throw new BoxError ( BoxError . NOT _FOUND , 'No such app' ) ;
2019-09-01 17:44:24 -07:00
2021-08-20 09:19:44 -07:00
if ( ! options . original && icons . icon ) return icons . icon ;
if ( icons . appStoreIcon ) return icons . appStoreIcon ;
2019-09-01 17:44:24 -07:00
2021-08-20 09:19:44 -07:00
return null ;
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 ;
}
2021-08-20 09:19:44 -07:00
function postProcess ( result ) {
assert . strictEqual ( typeof result , 'object' ) ;
assert ( result . manifestJson === null || typeof result . manifestJson === 'string' ) ;
result . manifest = safe . JSON . parse ( result . manifestJson ) ;
delete result . manifestJson ;
assert ( result . tagsJson === null || typeof result . tagsJson === 'string' ) ;
result . tags = safe . JSON . parse ( result . tagsJson ) || [ ] ;
delete result . tagsJson ;
2024-04-17 16:54:54 +02:00
assert ( result . checklistJson === null || typeof result . checklistJson === 'string' ) ;
result . checklist = safe . JSON . parse ( result . checklistJson ) || { } ;
delete result . checklistJson ;
2025-06-26 15:19:28 +02:00
assert ( result . updateInfoJson === null || typeof result . updateInfoJson === 'string' ) ;
result . updateInfo = safe . JSON . parse ( result . updateInfoJson ) || null ;
delete result . updateInfoJson ;
2021-08-20 09:19:44 -07:00
assert ( result . reverseProxyConfigJson === null || typeof result . reverseProxyConfigJson === 'string' ) ;
result . reverseProxyConfig = safe . JSON . parse ( result . reverseProxyConfigJson ) || { } ;
delete result . reverseProxyConfigJson ;
assert ( result . hostPorts === null || typeof result . hostPorts === 'string' ) ;
assert ( result . environmentVariables === null || typeof result . environmentVariables === 'string' ) ;
2024-07-16 22:21:36 +02:00
result . portBindings = { } ;
2024-05-13 08:43:28 +02:00
const hostPorts = result . hostPorts === null ? [ ] : result . hostPorts . split ( ',' ) ;
const environmentVariables = result . environmentVariables === null ? [ ] : result . environmentVariables . split ( ',' ) ;
const portTypes = result . portTypes === null ? [ ] : result . portTypes . split ( ',' ) ;
2024-07-15 22:59:16 +02:00
const portCounts = result . portCounts === null ? [ ] : result . portCounts . split ( ',' ) ;
2021-08-20 09:19:44 -07:00
delete result . hostPorts ;
delete result . environmentVariables ;
delete result . portTypes ;
2024-07-15 22:59:16 +02:00
delete result . portCounts ;
2021-08-20 09:19:44 -07:00
for ( let i = 0 ; i < environmentVariables . length ; i ++ ) {
2024-07-16 22:21:36 +02:00
result . portBindings [ environmentVariables [ i ] ] = { hostPort : parseInt ( hostPorts [ i ] , 10 ) , type : portTypes [ i ] , count : parseInt ( portCounts [ i ] , 10 ) } ;
2021-08-20 09:19:44 -07:00
}
assert ( result . accessRestrictionJson === null || typeof result . accessRestrictionJson === 'string' ) ;
result . accessRestriction = safe . JSON . parse ( result . accessRestrictionJson ) ;
if ( result . accessRestriction && ! result . accessRestriction . users ) result . accessRestriction . users = [ ] ;
delete result . accessRestrictionJson ;
2021-09-21 10:11:27 -07:00
result . operators = safe . JSON . parse ( result . operatorsJson ) ;
if ( result . operators && ! result . operators . users ) result . operators . users = [ ] ;
delete result . operatorsJson ;
2021-08-20 09:19:44 -07:00
result . sso = ! ! result . sso ;
result . enableBackup = ! ! result . enableBackup ;
result . enableAutomaticUpdate = ! ! result . enableAutomaticUpdate ;
result . enableMailbox = ! ! result . enableMailbox ;
2021-10-01 12:09:13 -07:00
result . enableInbox = ! ! result . enableInbox ;
2021-08-20 09:19:44 -07:00
result . proxyAuth = ! ! result . proxyAuth ;
result . hasIcon = ! ! result . hasIcon ;
result . hasAppStoreIcon = ! ! result . hasAppStoreIcon ;
assert ( result . debugModeJson === null || typeof result . debugModeJson === 'string' ) ;
result . debugMode = safe . JSON . parse ( result . debugModeJson ) ;
delete result . debugModeJson ;
assert ( result . servicesConfigJson === null || typeof result . servicesConfigJson === 'string' ) ;
result . servicesConfig = safe . JSON . parse ( result . servicesConfigJson ) || { } ;
delete result . servicesConfigJson ;
2022-01-14 22:40:51 -08:00
const subdomains = JSON . parse ( result . subdomains ) ,
domains = JSON . parse ( result . domains ) ,
subdomainTypes = JSON . parse ( result . subdomainTypes ) ,
2022-07-14 11:57:04 +05:30
subdomainEnvironmentVariables = JSON . parse ( result . subdomainEnvironmentVariables ) ,
subdomainCertificateJsons = JSON . parse ( result . subdomainCertificateJsons ) ;
2022-01-14 22:40:51 -08:00
2021-08-20 09:19:44 -07:00
delete result . subdomains ;
delete result . domains ;
delete result . subdomainTypes ;
2022-01-14 22:40:51 -08:00
delete result . subdomainEnvironmentVariables ;
2022-07-14 11:57:04 +05:30
delete result . subdomainCertificateJsons ;
2021-08-20 09:19:44 -07:00
2022-01-14 22:40:51 -08:00
result . secondaryDomains = [ ] ;
2022-01-14 22:29:47 -08:00
result . redirectDomains = [ ] ;
2021-08-20 09:19:44 -07:00
result . aliasDomains = [ ] ;
for ( let i = 0 ; i < subdomainTypes . length ; i ++ ) {
2022-07-14 11:57:04 +05:30
const subdomain = subdomains [ i ] , domain = domains [ i ] , certificate = safe . JSON . parse ( subdomainCertificateJsons [ i ] ) ;
2023-08-17 16:05:19 +05:30
if ( subdomainTypes [ i ] === Location . TYPE _PRIMARY ) {
2022-07-14 11:57:04 +05:30
result . subdomain = subdomain ;
result . domain = domain ;
result . certificate = certificate ;
2023-08-17 16:05:19 +05:30
} else if ( subdomainTypes [ i ] === Location . TYPE _SECONDARY ) {
2022-07-14 11:57:04 +05:30
result . secondaryDomains . push ( { domain , subdomain , certificate , environmentVariable : subdomainEnvironmentVariables [ i ] } ) ;
2023-08-17 16:05:19 +05:30
} else if ( subdomainTypes [ i ] === Location . TYPE _REDIRECT ) {
2022-07-14 11:57:04 +05:30
result . redirectDomains . push ( { domain , subdomain , certificate } ) ;
2023-08-17 16:05:19 +05:30
} else if ( subdomainTypes [ i ] === Location . TYPE _ALIAS ) {
2022-07-14 11:57:04 +05:30
result . aliasDomains . push ( { domain , subdomain , certificate } ) ;
2021-08-20 09:19:44 -07:00
}
}
2022-07-14 11:57:04 +05:30
const envNames = JSON . parse ( result . envNames ) , envValues = JSON . parse ( result . envValues ) ;
2021-08-20 09:19:44 -07:00
delete result . envNames ;
delete result . envValues ;
result . env = { } ;
for ( let i = 0 ; i < envNames . length ; i ++ ) { // NOTE: envNames is [ null ] when env of an app is empty
if ( envNames [ i ] ) result . env [ envNames [ i ] ] = envValues [ i ] ;
}
2022-07-14 11:57:04 +05:30
const volumeIds = JSON . parse ( result . volumeIds ) ;
2021-08-20 09:19:44 -07:00
delete result . volumeIds ;
2024-05-13 08:43:28 +02:00
const volumeReadOnlys = JSON . parse ( result . volumeReadOnlys ) ;
2021-08-20 09:19:44 -07:00
delete result . volumeReadOnlys ;
result . mounts = volumeIds [ 0 ] === null ? [ ] : volumeIds . map ( ( v , idx ) => { return { volumeId : v , readOnly : ! ! volumeReadOnlys [ idx ] } ; } ) ; // NOTE: volumeIds is [null] when volumes of an app is empty
result . error = safe . JSON . parse ( result . errorJson ) ;
delete result . errorJson ;
result . taskId = result . taskId ? String ( result . taskId ) : null ;
2024-12-05 13:47:59 +01:00
result . devices = result . devicesJson ? JSON . parse ( result . devicesJson ) : { } ;
delete result . devicesJson ;
2021-08-20 09:19:44 -07:00
}
2025-06-20 19:49:20 +02:00
// note: this value cannot be cached as it depends on enableAutomaticUpdate and runState
2025-06-26 13:41:09 +02:00
function canAutoupdateAppSync ( app , updateInfo ) {
2025-06-20 19:49:20 +02:00
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof updateInfo , 'object' ) ;
const manifest = updateInfo . manifest ;
if ( ! app . enableAutomaticUpdate ) return { code : false , reason : 'Automatic updates for the app is disabled' } ;
// for invalid subscriptions the appstore does not return a dockerImage
2025-06-26 15:19:28 +02:00
if ( ! manifest . dockerImage ) return { code : false , reason : 'Invalid or Expired subscription' } ;
2025-06-20 19:49:20 +02:00
if ( updateInfo . unstable ) return { code : false , reason : 'Update is marked as unstable' } ; // only manual update allowed for unstable updates
if ( ( semver . major ( app . manifest . version ) !== 0 ) && ( semver . major ( app . manifest . version ) !== semver . major ( manifest . version ) ) ) {
return { code : false , reason : 'Major package version requires review of breaking changes' } ; // major changes are blocking
}
if ( app . runState === exports . RSTATE _STOPPED ) return { code : false , reason : 'Stopped apps cannot run migration scripts' } ;
const newTcpPorts = manifest . tcpPorts || { } ;
const newUdpPorts = manifest . udpPorts || { } ;
const portBindings = app . portBindings ; // this is never null
for ( const portName in portBindings ) {
if ( ! ( portName in newTcpPorts ) && ! ( portName in newUdpPorts ) ) return { code : false , reason : ` ${ portName } port was in use but new update removes it ` } ;
}
// it's fine if one or more (unused) port keys got removed
return { code : true , reason : '' } ;
}
2024-07-16 22:21:36 +02:00
// attaches computed properties
2021-08-20 09:19:44 -07:00
function attachProperties ( app , domainObjectMap ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof domainObjectMap , 'object' ) ;
2021-04-30 13:18:15 -07:00
app . iconUrl = app . hasIcon || app . hasAppStoreIcon ? ` /api/v1/apps/ ${ app . id } /icon ` : null ;
2022-11-28 21:23:06 +01:00
app . fqdn = dns . fqdn ( app . subdomain , app . domain ) ;
app . secondaryDomains . forEach ( function ( ad ) { ad . fqdn = dns . fqdn ( ad . subdomain , ad . domain ) ; } ) ;
app . redirectDomains . forEach ( function ( ad ) { ad . fqdn = dns . fqdn ( ad . subdomain , ad . domain ) ; } ) ;
app . aliasDomains . forEach ( function ( ad ) { ad . fqdn = dns . fqdn ( ad . subdomain , ad . domain ) ; } ) ;
2025-06-20 19:49:20 +02:00
2025-06-26 15:19:28 +02:00
if ( app . updateInfo ) {
const { code , reason } = canAutoupdateAppSync ( app , app . updateInfo ) ; // isAutoUpdatable is not cached since it depends on enableAutomaticUpdate and runState
app . updateInfo . isAutoUpdatable = code ;
app . updateInfo . manualUpdateReason = reason ;
2025-06-20 19:49:20 +02:00
}
2019-03-06 11:12:39 -08:00
}
2021-09-21 10:11:27 -07:00
function isAdmin ( user ) {
assert . strictEqual ( typeof user , 'object' ) ;
return users . compareRoles ( user . role , users . ROLE _ADMIN ) >= 0 ;
}
function isOperator ( app , user ) {
2022-10-06 11:24:39 +02:00
assert . strictEqual ( typeof app , 'object' ) ; // IMPORTANT: can also be applink
2015-10-15 15:06:34 +02:00
assert . strictEqual ( typeof user , 'object' ) ;
2021-09-21 10:11:27 -07:00
if ( ! app . operators ) return isAdmin ( user ) ;
2016-02-09 12:48:21 -08:00
2025-02-12 12:36:50 +01:00
if ( app . operators . users . includes ( user . id ) ) return true ;
2021-09-21 10:11:27 -07:00
if ( ! app . operators . groups ) return isAdmin ( user ) ;
2025-02-12 12:36:50 +01:00
if ( app . operators . groups . some ( function ( gid ) { return Array . isArray ( user . groupIds ) && user . groupIds . includes ( gid ) ; } ) ) return true ;
2016-02-09 13:03:52 -08:00
2021-09-21 10:11:27 -07:00
return isAdmin ( user ) ;
}
2017-11-15 18:07:10 -08:00
2021-09-21 10:11:27 -07:00
function canAccess ( app , user ) {
2022-10-06 11:24:39 +02:00
assert . strictEqual ( typeof app , 'object' ) ; // IMPORTANT: can also be applink
2021-09-21 10:11:27 -07:00
assert . strictEqual ( typeof user , 'object' ) ;
2017-11-15 18:07:10 -08:00
2021-09-21 10:11:27 -07:00
if ( app . accessRestriction === null ) return true ;
2025-02-12 12:36:50 +01:00
if ( app . accessRestriction . users . includes ( user . id ) ) return true ;
2021-09-21 10:11:27 -07:00
if ( ! app . accessRestriction . groups ) return isOperator ( app , user ) ;
2025-02-12 12:36:50 +01:00
if ( app . accessRestriction . groups . some ( function ( gid ) { return Array . isArray ( user . groupIds ) && user . groupIds . includes ( gid ) ; } ) ) return true ;
2017-11-15 18:07:10 -08:00
2021-09-21 10:11:27 -07:00
return isOperator ( app , user ) ;
}
function accessLevel ( app , user ) {
2024-12-19 12:03:17 +01:00
if ( isAdmin ( user ) ) return exports . ACCESS _LEVEL _ADMIN ;
if ( isOperator ( app , user ) ) return exports . ACCESS _LEVEL _OPERATOR ;
return canAccess ( app , user ) ? exports . ACCESS _LEVEL _USER : exports . ACCESS _LEVEL _NONE ;
2015-10-15 15:06:34 +02:00
}
2024-07-16 19:31:54 +02:00
async function checkForPortBindingConflict ( portBindings , options ) {
2024-02-28 14:56:04 +01:00
assert . strictEqual ( typeof portBindings , 'object' ) ;
2024-07-16 19:31:54 +02:00
assert . strictEqual ( typeof options , 'object' ) ;
2024-02-28 14:56:04 +01:00
2024-07-16 19:31:54 +02:00
const existingPortBindings = options . appId
? await database . query ( 'SELECT * FROM appPortBindings WHERE appId != ?' , [ options . appId ] )
: await database . query ( 'SELECT * FROM appPortBindings' , [ ] ) ;
2024-02-28 14:56:04 +01:00
if ( existingPortBindings . length === 0 ) return ;
2024-07-16 22:21:36 +02:00
const tcpPortBindings = existingPortBindings . filter ( ( p ) => p . type === 'tcp' ) ;
const udpPortBindings = existingPortBindings . filter ( ( p ) => p . type === 'udp' ) ;
2024-02-28 14:56:04 +01:00
2024-05-13 08:43:28 +02:00
for ( const portName in portBindings ) {
2024-07-16 22:21:36 +02:00
const portBinding = portBindings [ portName ] ;
const existingPortBinding = portBinding . type === 'tcp' ? tcpPortBindings : udpPortBindings ;
2024-02-28 14:56:04 +01:00
2024-07-16 22:21:36 +02:00
const found = existingPortBinding . find ( ( epb ) => {
2024-02-28 14:56:04 +01:00
// if one is true we dont have a conflict
// a1 <----> a2 b1 <-------> b2
// b1 <------> b2 a1 <-----> a2
2024-07-16 22:21:36 +02:00
const a2 = ( epb . hostPort + epb . count - 1 ) ;
const b1 = portBinding . hostPort ;
const b2 = ( portBinding . hostPort + portBinding . count - 1 ) ;
const a1 = epb . hostPort ;
2024-02-28 14:56:04 +01:00
return ! ( ( a2 < b1 ) || ( b2 < a1 ) ) ;
} ) ;
2024-07-16 22:21:36 +02:00
if ( found ) throw new BoxError ( BoxError . CONFLICT , ` Conflicting ${ portBinding . type } port ${ portBinding . hostPort } ` ) ;
2024-02-28 14:56:04 +01:00
}
}
2022-01-16 12:32:12 -08:00
async function add ( id , appStoreId , manifest , subdomain , domain , portBindings , data ) {
2021-08-20 09:19:44 -07:00
assert . strictEqual ( typeof id , 'string' ) ;
assert . strictEqual ( typeof appStoreId , 'string' ) ;
assert ( manifest && typeof manifest === 'object' ) ;
assert . strictEqual ( typeof manifest . version , 'string' ) ;
2022-01-16 12:32:12 -08:00
assert . strictEqual ( typeof subdomain , 'string' ) ;
2021-08-20 09:19:44 -07:00
assert . strictEqual ( typeof domain , 'string' ) ;
2024-07-16 22:21:36 +02:00
assert ( portBindings && typeof portBindings === 'object' ) ;
2021-08-20 09:19:44 -07:00
assert ( data && typeof data === 'object' ) ;
2015-07-20 00:09:47 -07:00
2021-10-01 12:09:13 -07:00
const manifestJson = JSON . stringify ( manifest ) ,
2024-12-10 17:30:23 +01:00
accessRestrictionJson = data . accessRestriction ? JSON . stringify ( data . accessRestriction ) : null ,
operatorsJson = data . operators ? JSON . stringify ( data . operators ) : null ,
2021-10-01 12:09:13 -07:00
memoryLimit = data . memoryLimit || 0 ,
2024-04-10 17:38:49 +02:00
cpuQuota = data . cpuQuota || 100 ,
2021-10-01 12:09:13 -07:00
installationState = data . installationState ,
runState = data . runState ,
sso = 'sso' in data ? data . sso : null ,
debugModeJson = data . debugMode ? JSON . stringify ( data . debugMode ) : null ,
2024-12-05 13:47:59 +01:00
devicesJson = data . devices ? JSON . stringify ( data . devices ) : null ,
2021-10-01 12:09:13 -07:00
env = data . env || { } ,
label = data . label || null ,
tagsJson = data . tags ? JSON . stringify ( data . tags ) : null ,
2024-04-17 16:54:54 +02:00
checklistJson = data . checklist ? JSON . stringify ( data . checklist ) : null ,
2021-10-01 12:09:13 -07:00
mailboxName = data . mailboxName || null ,
mailboxDomain = data . mailboxDomain || null ,
2022-05-31 17:53:09 -07:00
mailboxDisplayName = data . mailboxDisplayName || '' ,
2021-10-01 12:09:13 -07:00
reverseProxyConfigJson = data . reverseProxyConfig ? JSON . stringify ( data . reverseProxyConfig ) : null ,
servicesConfigJson = data . servicesConfig ? JSON . stringify ( data . servicesConfig ) : null ,
enableMailbox = data . enableMailbox || false ,
2022-06-09 14:21:09 +02:00
upstreamUri = data . upstreamUri || '' ,
2023-07-13 15:06:07 +05:30
enableTurn = 'enableTurn' in data ? data . enableTurn : true ,
2024-12-10 17:30:23 +01:00
icon = data . icon || null ,
notes = data . notes || null ,
crontab = data . crontab || null ,
enableBackup = 'enableBackup' in data ? data . enableBackup : true ,
enableAutomaticUpdate = 'enableAutomaticUpdate' in data ? data . enableAutomaticUpdate : true ;
2021-08-20 09:19:44 -07:00
2025-06-14 10:28:20 +02:00
// when redis is optional, do not enable it by default. it's mostly used for caching in those setups
const enableRedis = 'enableRedis' in data ? data . enableRedis : ! manifest . addons ? . redis ? . optional ;
2024-07-16 22:21:36 +02:00
await checkForPortBindingConflict ( portBindings , { appId : null } ) ;
2024-02-28 14:56:04 +01:00
2021-10-01 12:09:13 -07:00
const queries = [ ] ;
2021-08-20 09:19:44 -07:00
queries . push ( {
2024-12-10 17:30:23 +01:00
query : 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, operatorsJson, memoryLimit, cpuQuota, '
2024-04-17 16:54:54 +02:00
+ 'sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson, checklistJson, servicesConfigJson, icon, '
2024-12-10 17:30:23 +01:00
+ 'enableMailbox, mailboxDisplayName, upstreamUri, enableTurn, enableRedis, devicesJson, notes, crontab, enableBackup, enableAutomaticUpdate) '
+ ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' ,
args : [ id , appStoreId , manifestJson , installationState , runState , accessRestrictionJson , operatorsJson , memoryLimit , cpuQuota ,
2024-04-17 16:54:54 +02:00
sso , debugModeJson , mailboxName , mailboxDomain , label , tagsJson , reverseProxyConfigJson , checklistJson , servicesConfigJson , icon ,
2024-12-10 17:30:23 +01:00
enableMailbox , mailboxDisplayName , upstreamUri , enableTurn , enableRedis , devicesJson , notes , crontab ,
enableBackup , enableAutomaticUpdate
]
2021-08-20 09:19:44 -07:00
} ) ;
2015-07-20 00:09:47 -07:00
2021-08-20 09:19:44 -07:00
queries . push ( {
2022-02-07 13:53:24 -08:00
query : 'INSERT INTO locations (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)' ,
2023-08-17 16:05:19 +05:30
args : [ id , domain , subdomain , Location . TYPE _PRIMARY ]
2021-08-20 09:19:44 -07:00
} ) ;
2019-03-06 11:12:39 -08:00
2021-08-20 09:19:44 -07:00
Object . keys ( portBindings ) . forEach ( function ( env ) {
queries . push ( {
2024-02-22 16:42:28 +01:00
query : 'INSERT INTO appPortBindings (environmentVariable, hostPort, type, appId, count) VALUES (?, ?, ?, ?, ?)' ,
2024-07-16 22:21:36 +02:00
args : [ env , portBindings [ env ] . hostPort , portBindings [ env ] . type , id , portBindings [ env ] . count ]
2021-08-20 09:19:44 -07:00
} ) ;
2019-03-06 11:12:39 -08:00
} ) ;
2021-08-20 09:19:44 -07:00
Object . keys ( env ) . forEach ( function ( name ) {
queries . push ( {
query : 'INSERT INTO appEnvVars (appId, name, value) VALUES (?, ?, ?)' ,
args : [ id , name , env [ name ] ]
} ) ;
} ) ;
2022-01-14 22:40:51 -08:00
if ( data . secondaryDomains ) {
data . secondaryDomains . forEach ( function ( d ) {
queries . push ( {
2022-02-07 13:53:24 -08:00
query : 'INSERT INTO locations (appId, domain, subdomain, type, environmentVariable) VALUES (?, ?, ?, ?, ?)' ,
2023-08-17 16:05:19 +05:30
args : [ id , d . domain , d . subdomain , Location . TYPE _SECONDARY , d . environmentVariable ]
2022-01-14 22:40:51 -08:00
} ) ;
} ) ;
}
2022-01-14 22:29:47 -08:00
if ( data . redirectDomains ) {
data . redirectDomains . forEach ( function ( d ) {
2021-08-20 09:19:44 -07:00
queries . push ( {
2022-02-07 13:53:24 -08:00
query : 'INSERT INTO locations (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)' ,
2023-08-17 16:05:19 +05:30
args : [ id , d . domain , d . subdomain , Location . TYPE _REDIRECT ]
2021-08-20 09:19:44 -07:00
} ) ;
} ) ;
}
if ( data . aliasDomains ) {
data . aliasDomains . forEach ( function ( d ) {
queries . push ( {
2022-02-07 13:53:24 -08:00
query : 'INSERT INTO locations (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)' ,
2023-08-17 16:05:19 +05:30
args : [ id , d . domain , d . subdomain , Location . TYPE _ALIAS ]
2021-08-20 09:19:44 -07:00
} ) ;
} ) ;
}
const [ error ] = await safe ( database . transaction ( queries ) ) ;
if ( error && error . code === 'ER_DUP_ENTRY' ) throw new BoxError ( BoxError . ALREADY _EXISTS , error . message ) ;
if ( error && error . code === 'ER_NO_REFERENCED_ROW_2' ) throw new BoxError ( BoxError . NOT _FOUND , 'no such domain' ) ;
if ( error ) throw new BoxError ( BoxError . DATABASE _ERROR , error ) ;
2019-03-06 11:12:39 -08:00
}
2021-08-20 09:19:44 -07:00
async function getIcons ( id ) {
assert . strictEqual ( typeof id , 'string' ) ;
2019-03-06 11:12:39 -08:00
2021-08-20 09:19:44 -07:00
const results = await database . query ( 'SELECT icon, appStoreIcon FROM apps WHERE id = ?' , [ id ] ) ;
if ( results . length === 0 ) return null ;
return { icon : results [ 0 ] . icon , appStoreIcon : results [ 0 ] . appStoreIcon } ;
}
2019-03-06 11:12:39 -08:00
2021-08-20 09:19:44 -07:00
async function updateWithConstraints ( id , app , constraints ) {
assert . strictEqual ( typeof id , 'string' ) ;
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof constraints , 'string' ) ;
assert ( ! ( 'portBindings' in app ) || typeof app . portBindings === 'object' ) ;
assert ( ! ( 'accessRestriction' in app ) || typeof app . accessRestriction === 'object' || app . accessRestriction === '' ) ;
2022-01-14 22:40:51 -08:00
assert ( ! ( 'secondaryDomains' in app ) || Array . isArray ( app . secondaryDomains ) ) ;
2022-01-14 22:29:47 -08:00
assert ( ! ( 'redirectDomains' in app ) || Array . isArray ( app . redirectDomains ) ) ;
2021-08-20 09:19:44 -07:00
assert ( ! ( 'aliasDomains' in app ) || Array . isArray ( app . aliasDomains ) ) ;
assert ( ! ( 'tags' in app ) || Array . isArray ( app . tags ) ) ;
2024-04-17 16:54:54 +02:00
assert ( ! ( 'checklist' in app ) || typeof app . checklist === 'object' ) ;
2021-08-20 09:19:44 -07:00
assert ( ! ( 'env' in app ) || typeof app . env === 'object' ) ;
const queries = [ ] ;
if ( 'portBindings' in app ) {
2024-07-16 22:21:36 +02:00
const portBindings = app . portBindings ;
2024-02-28 14:56:04 +01:00
2024-07-16 19:31:54 +02:00
await checkForPortBindingConflict ( portBindings , { appId : id } ) ;
2024-02-28 14:56:04 +01:00
2021-08-20 09:19:44 -07:00
// replace entries by app id
queries . push ( { query : 'DELETE FROM appPortBindings WHERE appId = ?' , args : [ id ] } ) ;
Object . keys ( portBindings ) . forEach ( function ( env ) {
2024-07-16 22:21:36 +02:00
const values = [ portBindings [ env ] . hostPort , portBindings [ env ] . type , env , id , portBindings [ env ] . count ] ;
2024-02-22 16:42:28 +01:00
queries . push ( { query : 'INSERT INTO appPortBindings (hostPort, type, environmentVariable, appId, count) VALUES(?, ?, ?, ?, ?)' , args : values } ) ;
2021-08-20 09:19:44 -07:00
} ) ;
}
2018-01-09 21:03:59 -08:00
2021-08-20 09:19:44 -07:00
if ( 'env' in app ) {
queries . push ( { query : 'DELETE FROM appEnvVars WHERE appId = ?' , args : [ id ] } ) ;
2015-07-20 00:09:47 -07:00
2021-08-20 09:19:44 -07:00
Object . keys ( app . env ) . forEach ( function ( name ) {
queries . push ( {
query : 'INSERT INTO appEnvVars (appId, name, value) VALUES (?, ?, ?)' ,
args : [ id , name , app . env [ name ] ]
} ) ;
2018-01-09 21:03:59 -08:00
} ) ;
2021-08-20 09:19:44 -07:00
}
2022-01-16 12:32:12 -08:00
if ( 'subdomain' in app && 'domain' in app ) { // must be updated together as they are unique together
2022-02-07 13:53:24 -08:00
queries . push ( { query : 'DELETE FROM locations WHERE appId = ?' , args : [ id ] } ) ; // all locations of an app must be updated together
2023-08-17 16:05:19 +05:30
queries . push ( { query : 'INSERT INTO locations (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)' , args : [ id , app . domain , app . subdomain , Location . TYPE _PRIMARY ] } ) ;
2021-08-20 09:19:44 -07:00
2022-01-14 22:40:51 -08:00
if ( 'secondaryDomains' in app ) {
app . secondaryDomains . forEach ( function ( d ) {
2023-08-17 16:05:19 +05:30
queries . push ( { query : 'INSERT INTO locations (appId, domain, subdomain, type, environmentVariable) VALUES (?, ?, ?, ?, ?)' , args : [ id , d . domain , d . subdomain , Location . TYPE _SECONDARY , d . environmentVariable ] } ) ;
2022-01-14 22:40:51 -08:00
} ) ;
}
2022-01-14 22:29:47 -08:00
if ( 'redirectDomains' in app ) {
app . redirectDomains . forEach ( function ( d ) {
2023-08-17 16:05:19 +05:30
queries . push ( { query : 'INSERT INTO locations (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)' , args : [ id , d . domain , d . subdomain , Location . TYPE _REDIRECT ] } ) ;
2021-08-20 09:19:44 -07:00
} ) ;
}
if ( 'aliasDomains' in app ) {
app . aliasDomains . forEach ( function ( d ) {
2023-08-17 16:05:19 +05:30
queries . push ( { query : 'INSERT INTO locations (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)' , args : [ id , d . domain , d . subdomain , Location . TYPE _ALIAS ] } ) ;
2021-08-20 09:19:44 -07:00
} ) ;
}
}
if ( 'mounts' in app ) {
queries . push ( { query : 'DELETE FROM appMounts WHERE appId = ?' , args : [ id ] } ) ;
app . mounts . forEach ( function ( m ) {
queries . push ( { query : 'INSERT INTO appMounts (appId, volumeId, readOnly) VALUES (?, ?, ?)' , args : [ id , m . volumeId , m . readOnly ] } ) ;
} ) ;
}
const fields = [ ] , values = [ ] ;
2021-09-21 17:28:58 -07:00
for ( const p in app ) {
2025-06-26 15:19:28 +02:00
if ( p === 'manifest' || p === 'tags' || p === 'checklist' || p === 'accessRestriction' || p === 'devices' || p === 'debugMode' || p === 'error'
|| p === 'reverseProxyConfig' || p === 'servicesConfig' || p === 'operators' || p === 'updateInfo' ) {
2021-08-20 09:19:44 -07:00
fields . push ( ` ${ p } Json = ? ` ) ;
values . push ( JSON . stringify ( app [ p ] ) ) ;
2022-01-16 12:32:12 -08:00
} else if ( p !== 'portBindings' && p !== 'subdomain' && p !== 'domain' && p !== 'secondaryDomains' && p !== 'redirectDomains' && p !== 'aliasDomains' && p !== 'env' && p !== 'mounts' ) {
2021-08-20 09:19:44 -07:00
fields . push ( p + ' = ?' ) ;
values . push ( app [ p ] ) ;
}
}
if ( values . length !== 0 ) {
values . push ( id ) ;
queries . push ( { query : 'UPDATE apps SET ' + fields . join ( ', ' ) + ' WHERE id = ? ' + constraints , args : values } ) ;
}
const [ error , results ] = await safe ( database . transaction ( queries ) ) ;
if ( error && error . code === 'ER_DUP_ENTRY' ) throw new BoxError ( BoxError . ALREADY _EXISTS , error . message ) ;
if ( error ) throw new BoxError ( BoxError . DATABASE _ERROR , error ) ;
if ( results [ results . length - 1 ] . affectedRows !== 1 ) throw new BoxError ( BoxError . NOT _FOUND , 'App not found' ) ;
2015-07-20 00:09:47 -07:00
}
2021-08-20 09:19:44 -07:00
async function update ( id , app ) {
// ts is useful as a versioning mechanism (for example, icon changed). update the timestamp explicity in code instead of db.
// this way health and healthTime can be updated without changing ts
app . ts = new Date ( ) ;
await updateWithConstraints ( id , app , '' ) ;
}
2019-03-06 11:15:12 -08:00
2021-08-20 09:19:44 -07:00
async function setHealth ( appId , health , healthTime ) {
assert . strictEqual ( typeof appId , 'string' ) ;
assert . strictEqual ( typeof health , 'string' ) ;
assert ( util . types . isDate ( healthTime ) ) ;
await updateWithConstraints ( appId , { health , healthTime } , '' ) ;
}
2020-12-03 11:48:25 -08:00
2021-08-20 09:19:44 -07:00
async function setTask ( appId , values , options ) {
assert . strictEqual ( typeof appId , 'string' ) ;
assert . strictEqual ( typeof values , 'object' ) ;
assert . strictEqual ( typeof options , 'object' ) ;
2020-12-03 11:48:25 -08:00
2021-08-20 09:19:44 -07:00
values . ts = new Date ( ) ;
2020-12-03 11:48:25 -08:00
2021-08-20 09:19:44 -07:00
if ( ! options . requireNullTaskId ) return await updateWithConstraints ( appId , values , '' ) ;
if ( options . requiredState === null ) {
await updateWithConstraints ( appId , values , 'AND taskId IS NULL' ) ;
} else {
await updateWithConstraints ( appId , values , ` AND taskId IS NULL AND installationState = " ${ options . requiredState } " ` ) ;
}
2019-03-06 11:15:12 -08:00
}
2021-08-20 09:19:44 -07:00
async function del ( id ) {
assert . strictEqual ( typeof id , 'string' ) ;
2019-03-19 16:23:03 -07:00
2021-08-20 09:19:44 -07:00
const queries = [
2022-02-07 13:53:24 -08:00
{ query : 'DELETE FROM locations WHERE appId = ?' , args : [ id ] } ,
2021-08-20 09:19:44 -07:00
{ query : 'DELETE FROM appPortBindings WHERE appId = ?' , args : [ id ] } ,
{ query : 'DELETE FROM appEnvVars WHERE appId = ?' , args : [ id ] } ,
{ query : 'DELETE FROM appPasswords WHERE identifier = ?' , args : [ id ] } ,
{ query : 'DELETE FROM appMounts WHERE appId = ?' , args : [ id ] } ,
{ query : 'DELETE FROM apps WHERE id = ?' , args : [ id ] }
] ;
2019-03-19 16:23:03 -07:00
2021-08-20 09:19:44 -07:00
const results = await database . transaction ( queries ) ;
if ( results [ 5 ] . affectedRows !== 1 ) throw new BoxError ( BoxError . NOT _FOUND , 'App not found' ) ;
2021-09-17 09:52:18 -07:00
}
2021-08-20 09:19:44 -07:00
async function clear ( ) {
2022-02-07 13:53:24 -08:00
await database . query ( 'DELETE FROM locations' ) ;
2021-08-20 09:19:44 -07:00
await database . query ( 'DELETE FROM appPortBindings' ) ;
await database . query ( 'DELETE FROM appAddonConfigs' ) ;
await database . query ( 'DELETE FROM appEnvVars' ) ;
await database . query ( 'DELETE FROM apps' ) ;
2019-03-19 16:23:03 -07:00
}
2021-08-20 09:19:44 -07:00
// each query simply join apps table with another table by id. we then join the full result together
2024-07-15 22:59:16 +02:00
const PB _QUERY = 'SELECT id, GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes, GROUP_CONCAT(CAST(appPortBindings.count AS CHAR(6))) AS portCounts FROM apps LEFT JOIN appPortBindings ON apps.id = appPortBindings.appId GROUP BY apps.id' ;
2021-08-20 09:19:44 -07:00
const ENV _QUERY = 'SELECT id, JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues FROM apps LEFT JOIN appEnvVars ON apps.id = appEnvVars.appId GROUP BY apps.id' ;
2022-07-14 11:57:04 +05:30
const SUBDOMAIN _QUERY = 'SELECT id, JSON_ARRAYAGG(locations.subdomain) AS subdomains, JSON_ARRAYAGG(locations.domain) AS domains, JSON_ARRAYAGG(locations.type) AS subdomainTypes, JSON_ARRAYAGG(locations.environmentVariable) AS subdomainEnvironmentVariables, JSON_ARRAYAGG(locations.certificateJson) AS subdomainCertificateJsons FROM apps LEFT JOIN locations ON apps.id = locations.appId GROUP BY apps.id' ;
2021-08-20 09:19:44 -07:00
const MOUNTS _QUERY = 'SELECT id, JSON_ARRAYAGG(appMounts.volumeId) AS volumeIds, JSON_ARRAYAGG(appMounts.readOnly) AS volumeReadOnlys FROM apps LEFT JOIN appMounts ON apps.id = appMounts.appId GROUP BY apps.id' ;
2024-07-15 22:59:16 +02:00
const APPS _QUERY = ` SELECT ${ APPS _FIELDS _PREFIXED } , hostPorts, environmentVariables, portTypes, portCounts, envNames, envValues, subdomains, domains, subdomainTypes, subdomainEnvironmentVariables, subdomainCertificateJsons, volumeIds, volumeReadOnlys FROM apps `
2021-08-20 09:19:44 -07:00
+ ` LEFT JOIN ( ${ PB _QUERY } ) AS q1 on q1.id = apps.id `
+ ` LEFT JOIN ( ${ ENV _QUERY } ) AS q2 on q2.id = apps.id `
+ ` LEFT JOIN ( ${ SUBDOMAIN _QUERY } ) AS q3 on q3.id = apps.id `
+ ` LEFT JOIN ( ${ MOUNTS _QUERY } ) AS q4 on q4.id = apps.id ` ;
2018-08-12 19:33:11 -07:00
2021-08-20 09:19:44 -07:00
async function get ( id ) {
assert . strictEqual ( typeof id , 'string' ) ;
2018-01-09 21:03:59 -08:00
2022-11-11 18:09:10 +01:00
const domainObjectMap = await domains . getDomainObjectMap ( ) ;
2015-07-20 00:09:47 -07:00
2021-08-20 09:19:44 -07:00
const result = await database . query ( ` ${ APPS _QUERY } WHERE apps.id = ? ` , [ id ] ) ;
if ( result . length === 0 ) return null ;
postProcess ( result [ 0 ] ) ;
attachProperties ( result [ 0 ] , domainObjectMap ) ;
return result [ 0 ] ;
2015-07-20 00:09:47 -07:00
}
2021-08-20 09:19:44 -07:00
// returns the app associated with this IP (app or scheduler)
async function getByIpAddress ( ip ) {
assert . strictEqual ( typeof ip , 'string' ) ;
2016-02-25 11:28:29 +01:00
2022-11-11 18:09:10 +01:00
const domainObjectMap = await domains . getDomainObjectMap ( ) ;
2016-02-25 11:28:29 +01:00
2021-08-20 09:19:44 -07:00
const result = await database . query ( ` ${ APPS _QUERY } WHERE apps.containerIp = ? ` , [ ip ] ) ;
if ( result . length === 0 ) return null ;
postProcess ( result [ 0 ] ) ;
attachProperties ( result [ 0 ] , domainObjectMap ) ;
return result [ 0 ] ;
}
async function list ( ) {
2022-11-11 18:09:10 +01:00
const domainObjectMap = await domains . getDomainObjectMap ( ) ;
2021-08-20 09:19:44 -07:00
const results = await database . query ( ` ${ APPS _QUERY } ORDER BY apps.id ` , [ ] ) ;
results . forEach ( postProcess ) ;
results . forEach ( ( app ) => attachProperties ( app , domainObjectMap ) ) ;
return results ;
}
async function getByFqdn ( fqdn ) {
assert . strictEqual ( typeof fqdn , 'string' ) ;
const result = await list ( ) ;
const app = result . find ( function ( a ) { return a . fqdn === fqdn ; } ) ;
return app ;
}
async function listByUser ( user ) {
assert . strictEqual ( typeof user , 'object' ) ;
2021-09-21 17:28:58 -07:00
const result = await list ( ) ;
return result . filter ( ( app ) => canAccess ( app , user ) ) ;
2016-02-25 11:28:29 +01:00
}
2021-09-21 22:19:20 -07:00
async function getTask ( app ) {
assert . strictEqual ( typeof app , 'object' ) ;
if ( ! app . taskId ) return null ;
return await tasks . get ( app . taskId ) ;
}
2022-01-16 12:32:12 -08:00
function mailboxNameForSubdomain ( subdomain , manifest ) {
if ( subdomain ) return ` ${ subdomain } .app ` ;
2019-11-14 21:43:14 -08:00
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
}
2021-11-17 10:42:04 -08:00
async function onTaskFinished ( error , appId , installationState , taskId , auditSource ) {
assert ( ! error || typeof error === 'object' ) ;
assert . strictEqual ( typeof appId , 'string' ) ;
assert . strictEqual ( typeof installationState , 'string' ) ;
assert . strictEqual ( typeof taskId , 'string' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
2021-09-30 10:31:50 -07:00
const success = ! error ;
const errorMessage = error ? . message || null ;
2021-09-30 10:45:25 -07:00
const app = await get ( appId ) ;
const task = await tasks . get ( taskId ) ;
if ( ! app || ! task ) return ;
2021-09-30 10:31:50 -07:00
switch ( installationState ) {
case exports . ISTATE _PENDING _DATA _DIR _MIGRATION :
2021-11-17 10:42:04 -08:00
if ( success ) await safe ( services . rebuildService ( 'sftp' , auditSource ) , { debug } ) ;
2021-09-30 10:31:50 -07:00
break ;
case exports . ISTATE _PENDING _UPDATE : {
const fromManifest = success ? task . args [ 1 ] . updateConfig . manifest : app . manifest ;
const toManifest = success ? app . manifest : task . args [ 1 ] . updateConfig . manifest ;
2021-11-17 10:42:04 -08:00
await eventlog . add ( eventlog . ACTION _APP _UPDATE _FINISH , auditSource , { app , toManifest , fromManifest , success , errorMessage } ) ;
2024-12-11 15:47:41 +01:00
await notifications . unpin ( notifications . TYPE _MANUAL _APP _UPDATE _NEEDED , { context : app . id } ) ;
2021-09-30 10:31:50 -07:00
break ;
}
2022-04-05 09:28:30 -07:00
}
2021-09-30 10:31:50 -07:00
}
2021-11-17 10:42:04 -08:00
async function scheduleTask ( appId , installationState , taskId , auditSource ) {
2019-09-24 00:07:20 -07:00
assert . strictEqual ( typeof appId , 'string' ) ;
assert . strictEqual ( typeof installationState , 'string' ) ;
2019-09-24 10:28:50 -07:00
assert . strictEqual ( typeof taskId , 'string' ) ;
2021-11-17 10:42:04 -08:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2019-09-24 00:07:20 -07:00
2025-07-25 12:55:14 +02:00
const backupTarget = await backupTargets . getPrimary ( ) ;
2021-08-19 13:24:38 -07:00
2021-09-07 09:57:49 -07:00
let memoryLimit = 400 ;
2025-07-18 10:56:52 +02:00
if ( installationState === exports . ISTATE _PENDING _CLONE || installationState === exports . ISTATE _PENDING _RESTORE
2021-09-07 09:57:49 -07:00
|| installationState === exports . ISTATE _PENDING _IMPORT || installationState === exports . ISTATE _PENDING _UPDATE ) {
2025-07-25 12:55:14 +02:00
memoryLimit = backupTarget . limits ? . memoryLimit ? Math . max ( backupTarget . limits . memoryLimit / 1024 / 1024 , 400 ) : 400 ;
2021-09-07 09:57:49 -07:00
} else if ( installationState === exports . ISTATE _PENDING _DATA _DIR _MIGRATION ) {
memoryLimit = 1024 ; // cp takes more memory than we think
}
2021-01-06 14:46:46 -08:00
2021-09-07 09:57:49 -07:00
const options = { timeout : 20 * 60 * 60 * 1000 /* 20 hours */ , nice : 15 , memoryLimit } ;
appTaskManager . scheduleTask ( appId , taskId , options , async function ( error ) {
2025-07-18 19:33:34 +02:00
debug ( ` scheduleTask: task ${ taskId } of ${ appId } completed. error: %o ` , error ) ;
if ( error ? . code === tasks . ECRASHED || error ? . code === tasks . ESTOPPED ) { // if task crashed, update the error
2021-09-07 09:57:49 -07:00
debug ( ` Apptask crashed/stopped: ${ error . message } ` ) ;
2024-05-13 08:43:28 +02:00
const boxError = new BoxError ( BoxError . TASK _ERROR , error . message ) ;
2021-09-07 09:57:49 -07:00
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-09-30 10:31:50 -07:00
await safe ( update ( appId , { installationState : exports . ISTATE _ERROR , error : boxError . toPlainObject ( ) , taskId : null } ) , { debug } ) ;
2021-09-07 09:57:49 -07:00
} else if ( ! ( installationState === exports . ISTATE _PENDING _UNINSTALL && ! error ) ) { // clear out taskId except for successful uninstall
2021-09-30 10:31:50 -07:00
await safe ( update ( appId , { taskId : null } ) , { debug } ) ;
2019-09-24 00:07:20 -07:00
}
2021-01-06 14:46:46 -08:00
2021-11-17 10:42:04 -08:00
await safe ( onTaskFinished ( error , appId , installationState , taskId , auditSource ) , { debug } ) ; // ignore error
2019-09-24 00:07:20 -07:00
} ) ;
}
2021-11-17 10:38:02 -08:00
async function addTask ( appId , installationState , task , auditSource ) {
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 }
2021-11-17 10:38:02 -08:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2019-08-27 16:12:24 -07:00
2019-09-23 14:17:12 -07:00
const { args , values } = task ;
2025-07-23 15:31:25 +02:00
// TODO: match the SQL logic to match checkAppState. this means checking the error.details.installationState and installationState. Unfortunately, former is JSON right now
2019-12-06 11:29:33 -08:00
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-08-20 09:19:44 -07:00
const taskId = await tasks . add ( tasks . TASK _APP , [ appId , args ] ) ;
2019-08-27 22:39:59 -07:00
2023-05-25 11:27:23 +02:00
const [ updateError ] = await safe ( setTask ( appId , Object . assign ( { installationState , taskId , error : null } , values ) , { requiredState , requireNullTaskId } ) ) ;
2021-08-20 09:19:44 -07:00
if ( updateError && updateError . reason === BoxError . NOT _FOUND ) throw new BoxError ( BoxError . BAD _STATE , 'Another task is scheduled for this app' ) ; // could be because app went away OR a taskId exists
if ( updateError ) throw updateError ;
2019-08-26 15:55:57 -07:00
2021-11-17 10:42:04 -08:00
if ( scheduleNow ) await safe ( scheduleTask ( appId , installationState , taskId , auditSource ) , { debug } ) ; // ignore error
2019-08-26 15:55:57 -07:00
2021-08-20 09:19:44 -07:00
return 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
2025-07-23 15:40:25 +02:00
if ( app . error . details . installationState === state ) return null ;
2019-11-23 18:06:31 -08:00
// 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
2025-07-18 10:56:52 +02:00
if ( state === exports . ISTATE _PENDING _UPDATE || 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 ;
}
2022-11-28 22:16:22 +01:00
async function validateLocations ( locations ) {
2019-09-27 10:25:26 -07:00
assert ( Array . isArray ( locations ) ) ;
2022-11-28 22:16:22 +01:00
const domainObjectMap = await domains . getDomainObjectMap ( ) ;
2019-09-27 10:25:26 -07:00
2023-08-01 19:33:59 +05:30
const RESERVED _SUBDOMAINS = [
constants . SMTP _SUBDOMAIN ,
constants . IMAP _SUBDOMAIN
] ;
2023-08-17 16:05:19 +05:30
const dashboardLocation = await dashboard . getLocation ( ) ;
2022-11-17 08:31:17 +01:00
for ( const location of locations ) {
2022-11-28 22:16:22 +01:00
if ( ! ( location . domain in domainObjectMap ) ) return new BoxError ( BoxError . BAD _FIELD , ` No such domain in ${ location . type } location ` ) ;
2021-01-18 22:47:53 -08:00
2021-08-20 09:19:44 -07:00
let subdomain = location . subdomain ;
2023-08-17 16:05:19 +05:30
if ( location . type === Location . TYPE _ALIAS && subdomain . startsWith ( '*' ) ) {
2021-08-20 09:19:44 -07:00
if ( subdomain === '*' ) continue ;
subdomain = subdomain . replace ( /^\*\./ , '' ) ; // remove *.
2019-09-27 10:25:26 -07:00
}
2023-08-01 19:33:59 +05:30
if ( RESERVED _SUBDOMAINS . indexOf ( subdomain ) !== - 1 ) return new BoxError ( BoxError . BAD _FIELD , ` subdomain ' ${ subdomain } ' is reserved ` ) ;
2023-08-17 16:05:19 +05:30
if ( location . fqdn === dashboardLocation . fqdn ) return new BoxError ( BoxError . BAD _FIELD , ` subdomain ' ${ subdomain } ' is reserved for dashboard ` ) ;
2023-08-01 19:33:59 +05:30
2022-11-28 21:23:06 +01:00
const error = dns . validateHostname ( subdomain , location . domain ) ;
2022-11-28 22:16:22 +01:00
if ( error ) return new BoxError ( BoxError . BAD _FIELD , ` Bad ${ location . type } location: ${ error . message } ` ) ;
2021-08-20 09:19:44 -07:00
}
2022-11-28 22:16:22 +01:00
return null ;
2019-09-27 10:25:26 -07:00
}
2021-11-15 13:55:29 -08:00
async function getCount ( ) {
const result = await database . query ( 'SELECT COUNT(*) AS total FROM apps' ) ;
return result [ 0 ] . total ;
}
2021-08-20 09:19:44 -07:00
async function install ( data , auditSource ) {
2016-06-03 23:22:38 -07:00
assert ( data && typeof data === 'object' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
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
2022-01-16 12:32:12 -08:00
const subdomain = data . subdomain . toLowerCase ( ) ,
2017-11-02 22:17:44 +01:00
domain = data . domain . toLowerCase ( ) ,
2016-06-03 23:22:38 -07:00
accessRestriction = data . accessRestriction || null ,
2024-12-10 17:30:23 +01:00
operators = data . operators || null ,
2016-06-03 23:22:38 -07:00
memoryLimit = data . memoryLimit || 0 ,
2024-12-10 17:30:23 +01:00
cpuQuota = data . cpuQuota || 100 ,
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 ,
2022-01-14 22:29:47 -08:00
redirectDomains = data . redirectDomains || [ ] ,
2021-01-18 17:26:26 -08:00
aliasDomains = data . aliasDomains || [ ] ,
2024-12-05 13:47:59 +01:00
devices = data . devices || { } ,
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 ,
2023-07-13 15:06:07 +05:30
enableTurn = 'enableTurn' in data ? data . enableTurn : true ,
2020-03-29 16:24:04 -07:00
appStoreId = data . appStoreId ,
2022-06-10 11:23:58 -07:00
upstreamUri = data . upstreamUri || '' ,
2024-12-10 17:30:23 +01:00
manifest = data . manifest ,
notes = data . notes || null ,
crontab = data . crontab || null ;
2016-06-03 23:22:38 -07:00
2025-06-14 10:28:20 +02:00
// when redis is optional, do not enable it by default. it's mostly used for caching in those setups
const enableRedis = 'enableRedis' in data ? data . enableRedis : ! manifest . addons ? . redis ? . optional ;
2020-03-29 16:24:04 -07:00
let error = manifestFormat . parse ( manifest ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw new BoxError ( BoxError . BAD _FIELD , ` Manifest error: ${ error . message } ` ) ;
2015-07-20 00:09:47 -07:00
2024-03-30 18:25:33 +01:00
error = await checkManifest ( manifest ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2015-07-20 00:09:47 -07:00
2024-07-16 22:21:36 +02:00
error = validatePorts ( data . ports || null , manifest ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2024-07-16 22:21:36 +02:00
const portBindings = translateToPortBindings ( data . ports || null , manifest ) ;
2015-07-20 00:09:47 -07:00
2020-03-29 16:24:04 -07:00
error = validateAccessRestriction ( accessRestriction ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2015-07-20 00:09:47 -07:00
2024-12-10 17:30:23 +01:00
error = validateAccessRestriction ( operators ) ; // not a typo. same structure for operators and accessRestriction
if ( error ) throw error ;
2020-03-29 16:24:04 -07:00
error = validateMemoryLimit ( manifest , memoryLimit ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2015-07-20 00:09:47 -07:00
2020-03-29 16:24:04 -07:00
error = validateDebugMode ( debugMode ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2016-02-05 15:07:27 +01:00
2020-03-29 16:24:04 -07:00
error = validateLabel ( label ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2016-02-11 18:14:16 +01:00
2024-12-10 17:30:23 +01:00
error = validateCpuQuota ( cpuQuota ) ;
if ( error ) throw error ;
parseCrontab ( crontab ) ;
2022-11-23 12:53:21 +01:00
if ( 'upstreamUri' in data ) error = validateUpstreamUri ( upstreamUri ) ;
2022-06-09 14:21:09 +02:00
if ( error ) throw error ;
2020-03-29 16:24:04 -07:00
error = validateTags ( tags ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2019-10-11 20:21:32 -07:00
2022-01-20 16:57:30 -08:00
error = validateSecondaryDomains ( data . secondaryDomains || { } , manifest ) ;
if ( error ) throw error ;
const secondaryDomains = translateSecondaryDomains ( data . secondaryDomains || { } ) ;
2022-01-14 22:40:51 -08:00
2021-10-01 09:37:33 -07:00
let sso = 'sso' in data ? data . sso : null ;
2021-08-20 09:19:44 -07:00
if ( 'sso' in data && ! ( 'optionalSso' in manifest ) ) throw new BoxError ( BoxError . BAD _FIELD , 'sso can only be specified for apps with optionalSso' ) ;
2020-03-29 16:24:04 -07:00
// if sso was unspecified, enable it by default if possible
2023-04-14 21:18:44 +02:00
if ( sso === null ) sso = ! ! manifest . addons ? . ldap || ! ! manifest . addons ? . proxyAuth || ! ! manifest . addons ? . oidc ;
2019-03-22 07:48:31 -07:00
2024-12-05 13:47:59 +01:00
error = validateDevices ( devices ) ;
if ( error ) throw error ;
2020-03-29 16:24:04 -07:00
error = validateEnv ( env ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2019-03-22 07:48:31 -07:00
2024-05-23 09:52:05 +02:00
if ( constants . DEMO && constants . DEMO _BLOCKED _APPS . includes ( appStoreId ) ) throw new BoxError ( BoxError . BAD _FIELD , 'This app is blocked in the demo' ) ;
2020-05-04 14:56:10 -07:00
2021-10-01 09:37:33 -07:00
// sendmail is enabled by default
2021-10-01 12:09:13 -07:00
const enableMailbox = 'enableMailbox' in data ? data . enableMailbox : true ;
2022-01-16 12:32:12 -08:00
const mailboxName = manifest . addons ? . sendmail ? mailboxNameForSubdomain ( subdomain , manifest ) : null ;
2021-10-01 09:37:33 -07:00
const mailboxDomain = manifest . addons ? . sendmail ? domain : null ;
2016-11-11 10:55:44 +05:30
2021-10-01 09:37:33 -07:00
let icon = data . icon || null ;
2020-03-29 16:24:04 -07:00
if ( icon ) {
2021-04-30 13:18:15 -07:00
icon = Buffer . from ( icon , 'base64' ) ;
2025-02-10 14:58:36 +01:00
if ( icon . length === 0 ) throw new BoxError ( BoxError . BAD _FIELD , 'icon is not base64' ) ;
2020-03-29 16:24:04 -07:00
}
2016-07-09 12:25:00 -07:00
2023-08-17 16:05:19 +05:30
const locations = [ new Location ( subdomain , domain , Location . TYPE _PRIMARY ) ]
. concat ( secondaryDomains . map ( sd => new Location ( sd . subdomain , sd . domain , Location . TYPE _SECONDARY ) ) )
. concat ( redirectDomains . map ( rd => new Location ( rd . subdomain , rd . domain , Location . TYPE _REDIRECT ) ) )
. concat ( aliasDomains . map ( ad => new Location ( ad . subdomain , ad . domain , Location . TYPE _ALIAS ) ) ) ;
2021-01-18 22:47:53 -08:00
2022-11-28 22:16:22 +01:00
error = await validateLocations ( locations ) ;
if ( error ) throw error ;
2021-08-20 09:19:44 -07:00
2023-08-04 14:13:30 +05:30
if ( constants . DEMO && ( await getCount ( ) >= constants . DEMO _APP _LIMIT ) ) throw new BoxError ( BoxError . BAD _STATE , 'Too many installed apps, please uninstall a few and try again' ) ;
2021-11-15 13:55:29 -08:00
2025-07-28 12:53:27 +02:00
const appId = crypto . randomUUID ( ) ;
2024-12-09 21:33:26 +01:00
debug ( ` Installing app ${ appId } ` ) ;
2021-08-20 09:19:44 -07:00
const app = {
accessRestriction ,
2024-12-10 17:30:23 +01:00
operators ,
2021-08-20 09:19:44 -07:00
memoryLimit ,
2024-12-10 17:30:23 +01:00
cpuQuota ,
2021-08-20 09:19:44 -07:00
sso ,
debugMode ,
mailboxName ,
mailboxDomain ,
enableBackup ,
enableAutomaticUpdate ,
2022-01-14 22:40:51 -08:00
secondaryDomains ,
2022-01-14 22:29:47 -08:00
redirectDomains ,
2021-08-20 09:19:44 -07:00
aliasDomains ,
env ,
2024-12-05 13:47:59 +01:00
devices ,
2021-08-20 09:19:44 -07:00
label ,
tags ,
icon ,
enableMailbox ,
2022-06-09 14:21:09 +02:00
upstreamUri ,
2023-07-13 15:06:07 +05:30
enableTurn ,
2023-07-13 16:37:33 +05:30
enableRedis ,
2024-12-10 17:30:23 +01:00
notes ,
crontab ,
2021-08-20 09:19:44 -07:00
runState : exports . RSTATE _RUNNING ,
installationState : exports . ISTATE _PENDING _INSTALL
} ;
2018-01-09 21:03:59 -08:00
2024-02-25 16:28:57 +01:00
const [ addError ] = await safe ( add ( appId , appStoreId , manifest , subdomain , domain , portBindings , app ) ) ;
2022-11-28 22:14:10 +01:00
if ( addError && addError . reason === BoxError . ALREADY _EXISTS ) throw getDuplicateErrorDetails ( addError . message , locations , portBindings ) ;
2021-08-20 09:19:44 -07:00
if ( addError ) throw addError ;
2018-05-03 13:20:34 +02:00
2021-08-20 09:19:44 -07:00
const task = {
2024-12-10 19:05:31 +01:00
args : { restoreConfig : null , skipDnsSetup , overwriteDns } ,
2021-08-20 09:19:44 -07:00
values : { } ,
requiredState : app . installationState
} ;
2019-08-27 15:18:16 -07:00
2021-11-17 10:38:02 -08:00
const taskId = await addTask ( appId , app . installationState , task , auditSource ) ;
2018-02-20 10:34:09 -08:00
2025-02-13 14:03:25 +01:00
const newApp = Object . assign ( { } , _ . omit ( app , [ 'icon' ] ) , { appStoreId , manifest , subdomain , domain , portBindings } ) ;
2022-11-28 21:23:06 +01:00
newApp . fqdn = dns . fqdn ( newApp . subdomain , newApp . domain ) ;
newApp . secondaryDomains . forEach ( function ( ad ) { ad . fqdn = dns . fqdn ( ad . subdomain , ad . domain ) ; } ) ;
newApp . redirectDomains . forEach ( function ( ad ) { ad . fqdn = dns . fqdn ( ad . subdomain , ad . domain ) ; } ) ;
newApp . aliasDomains . forEach ( function ( ad ) { ad . fqdn = dns . fqdn ( ad . subdomain , ad . domain ) ; } ) ;
2019-09-27 11:24:21 -07:00
2021-08-20 09:19:44 -07:00
await eventlog . add ( eventlog . ACTION _APP _INSTALL , auditSource , { appId , app : newApp , taskId } ) ;
2018-01-09 21:03:59 -08:00
2021-08-20 09:19:44 -07:00
return { id : appId , taskId } ;
2015-07-20 00:09:47 -07:00
}
2021-08-20 09:19:44 -07:00
async function setAccessRestriction ( app , accessRestriction , auditSource ) {
2020-03-29 17:11:10 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2019-09-08 16:57:08 -07:00
assert . strictEqual ( typeof accessRestriction , 'object' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
2024-05-13 08:43:28 +02:00
const error = validateAccessRestriction ( accessRestriction ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
await update ( appId , { accessRestriction } ) ;
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , accessRestriction } ) ;
2019-09-08 16:57:08 -07:00
}
2021-09-21 10:11:27 -07:00
async function setOperators ( app , operators , auditSource ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof operators , 'object' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
const appId = app . id ;
2021-09-27 14:21:42 -07:00
const error = validateAccessRestriction ( operators ) ; // not a typo. same structure for operators and accessRestriction
2021-09-21 10:11:27 -07:00
if ( error ) throw error ;
await update ( appId , { operators } ) ;
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , operators } ) ;
}
2021-09-27 14:21:42 -07:00
async function setCrontab ( app , crontab , auditSource ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert ( crontab === null || typeof crontab === 'string' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
const appId = app . id ;
parseCrontab ( crontab ) ;
await update ( appId , { crontab } ) ;
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , crontab } ) ;
}
2022-06-08 11:21:09 +02:00
async function setUpstreamUri ( app , upstreamUri , auditSource ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof upstreamUri , 'string' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
2024-06-10 10:53:00 +02:00
if ( app . manifest . id !== constants . PROXY _APP _APPSTORE _ID ) throw new BoxError ( BoxError . BAD _FIELD , 'upstreamUri can only be set for proxy app' ) ;
2022-06-08 11:21:09 +02:00
const appId = app . id ;
2022-06-10 11:23:58 -07:00
const error = validateUpstreamUri ( upstreamUri ) ;
2022-06-08 11:21:09 +02:00
if ( error ) throw error ;
2023-05-25 11:27:23 +02:00
await reverseProxy . writeAppConfigs ( Object . assign ( { } , app , { upstreamUri } ) ) ;
2022-06-08 11:50:27 +02:00
2022-06-08 11:21:09 +02:00
await update ( appId , { upstreamUri } ) ;
2022-06-08 11:50:27 +02:00
2022-06-08 11:21:09 +02:00
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , upstreamUri } ) ;
}
2021-08-20 09:19:44 -07:00
async function setLabel ( app , label , auditSource ) {
2020-03-29 17:11:10 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2019-09-08 16:57:08 -07:00
assert . strictEqual ( typeof label , 'string' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
2024-05-13 08:43:28 +02:00
const error = validateLabel ( label ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
await update ( appId , { label } ) ;
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , label } ) ;
2019-09-08 16:57:08 -07:00
}
2021-08-20 09:19:44 -07:00
async function setTags ( app , tags , auditSource ) {
2020-03-29 17:11:10 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2019-09-08 16:57:08 -07:00
assert ( Array . isArray ( tags ) ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
2024-05-13 08:43:28 +02:00
const error = validateTags ( tags ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
await update ( appId , { tags } ) ;
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , tags } ) ;
2019-09-08 16:57:08 -07:00
}
2024-04-10 17:02:32 +02:00
async function setNotes ( app , notes , auditSource ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof notes , 'string' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
await update ( app . id , { notes } ) ;
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId : app . id , app , notes } ) ;
}
2024-06-24 18:39:37 +02:00
async function setChecklistItem ( app , checklistItemKey , acknowledged , auditSource ) {
2024-04-19 12:40:35 +02:00
assert . strictEqual ( typeof app , 'object' ) ;
2024-06-24 18:39:37 +02:00
assert . strictEqual ( typeof checklistItemKey , 'string' ) ;
assert . strictEqual ( typeof acknowledged , 'boolean' ) ;
2024-04-19 12:40:35 +02:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2024-06-24 18:39:37 +02:00
if ( ! app . checklist [ checklistItemKey ] ) throw new BoxError ( BoxError . NOT _FOUND , 'no such checklist item' ) ;
// nothing changed
if ( app . checklist [ checklistItemKey ] . acknowledged === acknowledged ) return ;
const checklist = app . checklist ;
checklist [ checklistItemKey ] . acknowledged = acknowledged ;
checklist [ checklistItemKey ] . changedAt = Date . now ( ) ;
checklist [ checklistItemKey ] . changedBy = auditSource . username ;
2024-04-19 12:40:35 +02:00
await update ( app . id , { checklist } ) ;
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId : app . id , app , checklist } ) ;
}
2021-08-20 09:19:44 -07:00
async function setIcon ( app , icon , auditSource ) {
2020-03-29 17:11:10 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2019-09-08 16:57:08 -07:00
assert ( icon === null || typeof icon === 'string' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
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 ) {
2021-04-30 13:18:15 -07:00
icon = Buffer . from ( icon , 'base64' ) ;
2025-02-10 14:58:36 +01:00
if ( icon . length === 0 ) throw new BoxError ( BoxError . BAD _FIELD , 'icon is not base64' ) ;
2020-03-29 17:11:10 -07:00
}
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
await update ( appId , { icon } ) ;
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , iconChanged : true } ) ;
2019-09-08 16:57:08 -07:00
}
2021-08-20 09:19:44 -07:00
async function setMemoryLimit ( app , memoryLimit , auditSource ) {
2020-03-29 17:11:10 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2019-09-08 16:57:08 -07:00
assert . strictEqual ( typeof memoryLimit , 'number' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
let error = checkAppState ( app , exports . ISTATE _PENDING _RESIZE ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2019-09-08 16:57:08 -07:00
2020-03-29 17:11:10 -07:00
error = validateMemoryLimit ( app . manifest , memoryLimit ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2019-09-21 19:45:55 -07:00
2020-03-29 17:11:10 -07:00
const task = {
args : { } ,
values : { memoryLimit }
} ;
2021-11-17 10:38:02 -08:00
const taskId = await addTask ( appId , exports . ISTATE _PENDING _RESIZE , task , auditSource ) ;
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , memoryLimit , taskId } ) ;
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
return { taskId } ;
2019-09-08 16:57:08 -07:00
}
2024-04-10 17:38:49 +02:00
async function setCpuQuota ( app , cpuQuota , auditSource ) {
2020-03-29 17:11:10 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2024-04-10 17:38:49 +02:00
assert . strictEqual ( typeof cpuQuota , 'number' ) ;
2020-01-28 21:30:35 -08:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
let error = checkAppState ( app , exports . ISTATE _PENDING _RESIZE ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2020-01-28 21:30:35 -08:00
2024-04-10 17:38:49 +02:00
error = validateCpuQuota ( cpuQuota ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2020-01-28 21:30:35 -08:00
2020-03-29 17:11:10 -07:00
const task = {
args : { } ,
2024-04-10 17:38:49 +02:00
values : { cpuQuota }
2020-03-29 17:11:10 -07:00
} ;
2021-11-17 10:38:02 -08:00
const taskId = await safe ( addTask ( appId , exports . ISTATE _PENDING _RESIZE , task , auditSource ) ) ;
2020-01-28 21:30:35 -08:00
2024-04-10 17:38:49 +02:00
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , cpuQuota , taskId } ) ;
2020-01-28 21:30:35 -08:00
2021-08-20 09:19:44 -07:00
return { taskId } ;
2020-01-28 21:30:35 -08:00
}
2021-08-20 09:19:44 -07:00
async function setMounts ( app , mounts , auditSource ) {
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' ) ;
const appId = app . id ;
2024-05-13 08:43:28 +02:00
const error = checkAppState ( app , exports . ISTATE _PENDING _RECREATE _CONTAINER ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2020-04-29 21:55:21 -07:00
const task = {
args : { } ,
2020-10-28 19:42:48 -07:00
values : { mounts }
2020-04-29 21:55:21 -07:00
} ;
2021-11-17 10:38:02 -08:00
const [ taskError , taskId ] = await safe ( addTask ( appId , exports . ISTATE _PENDING _RECREATE _CONTAINER , task , auditSource ) ) ;
2021-08-20 09:19:44 -07:00
if ( taskError && taskError . reason === BoxError . ALREADY _EXISTS ) throw new BoxError ( BoxError . CONFLICT , 'Duplicate mount points' ) ;
if ( taskError ) throw taskError ;
2020-04-29 21:55:21 -07:00
2021-08-20 09:19:44 -07:00
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , mounts , taskId } ) ;
2020-04-29 21:55:21 -07:00
2021-08-20 09:19:44 -07:00
return { taskId } ;
2020-04-29 21:55:21 -07:00
}
2024-12-05 13:47:59 +01:00
async function setDevices ( app , devices , auditSource ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof devices , 'object' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
const appId = app . id ;
2024-12-05 15:16:06 +01:00
let error = checkAppState ( app , exports . ISTATE _PENDING _RECREATE _CONTAINER ) ;
if ( error ) throw error ;
error = validateDevices ( devices ) ;
2024-12-05 13:47:59 +01:00
if ( error ) throw error ;
const task = {
args : { } ,
values : { devices }
} ;
const [ taskError , taskId ] = await safe ( addTask ( appId , exports . ISTATE _PENDING _RECREATE _CONTAINER , task , auditSource ) ) ;
if ( taskError ) throw taskError ;
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , devices , taskId } ) ;
return { taskId } ;
}
2021-08-20 09:19:44 -07:00
async function setEnvironment ( app , env , auditSource ) {
2020-03-29 17:11:10 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2019-09-08 16:57:08 -07:00
assert . strictEqual ( typeof env , 'object' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
let error = checkAppState ( app , exports . ISTATE _PENDING _RECREATE _CONTAINER ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2019-09-08 16:57:08 -07:00
2020-03-29 17:11:10 -07:00
error = validateEnv ( env ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2019-09-21 19:45:55 -07:00
2020-03-29 17:11:10 -07:00
const task = {
args : { } ,
values : { env }
} ;
2021-11-17 10:38:02 -08:00
const taskId = await addTask ( appId , exports . ISTATE _PENDING _RECREATE _CONTAINER , task , auditSource ) ;
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , env , taskId } ) ;
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
return { taskId } ;
2019-09-08 16:57:08 -07:00
}
2021-08-20 09:19:44 -07:00
async function setDebugMode ( app , debugMode , auditSource ) {
2020-03-29 17:11:10 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2019-09-08 16:57:08 -07:00
assert . strictEqual ( typeof debugMode , 'object' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
let error = checkAppState ( app , exports . ISTATE _PENDING _DEBUG ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2019-09-08 16:57:08 -07:00
2020-03-29 17:11:10 -07:00
error = validateDebugMode ( debugMode ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2019-09-21 19:45:55 -07:00
2020-03-29 17:11:10 -07:00
const task = {
args : { } ,
values : { debugMode }
} ;
2021-11-17 10:38:02 -08:00
const taskId = await addTask ( appId , exports . ISTATE _PENDING _DEBUG , task , auditSource ) ;
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , debugMode , taskId } ) ;
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
return { taskId } ;
2019-09-08 16:57:08 -07:00
}
2021-08-20 09:19:44 -07:00
async function setMailbox ( app , data , auditSource ) {
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' ) ;
2021-10-01 12:09:13 -07:00
assert . strictEqual ( typeof data . enable , 'boolean' ) ;
const enableMailbox = data . enable ;
2021-03-16 22:38:59 -07:00
2020-03-29 17:11:10 -07:00
const appId = app . id ;
2023-07-13 16:25:01 +05:30
let error = checkAppState ( app , exports . ISTATE _PENDING _SERVICES _CHANGE ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2020-03-30 22:18:39 -07:00
2021-10-01 11:19:26 -07:00
if ( ! app . manifest . addons ? . sendmail ) throw new BoxError ( BoxError . BAD _FIELD , 'App does not use sendmail' ) ;
const optional = 'optional' in app . manifest . addons . sendmail ? app . manifest . addons . sendmail . optional : false ;
2021-10-01 12:09:13 -07:00
if ( ! optional && ! enableMailbox ) throw new BoxError ( BoxError . BAD _FIELD , 'App requires sendmail to be enabled' ) ;
2021-06-29 14:26:34 -07:00
2022-05-31 17:53:09 -07:00
const mailboxDisplayName = data . mailboxDisplayName || '' ;
2021-10-01 12:09:13 -07:00
let mailboxName = data . mailboxName || null ;
const mailboxDomain = data . mailboxDomain || null ;
2019-09-21 19:45:55 -07:00
2021-10-01 12:09:13 -07:00
if ( enableMailbox ) {
await mail . getDomain ( mailboxDomain ) ; // check if domain exists
if ( mailboxName ) {
error = mail . validateName ( mailboxName ) ;
2022-02-07 13:19:59 -08:00
if ( error ) throw new BoxError ( BoxError . BAD _FIELD , error . message ) ;
2021-10-01 12:09:13 -07:00
} else {
2022-01-16 12:32:12 -08:00
mailboxName = mailboxNameForSubdomain ( app . subdomain , app . domain , app . manifest ) ;
2021-10-01 12:09:13 -07:00
}
2022-05-31 17:53:09 -07:00
if ( mailboxDisplayName ) {
error = mail . validateDisplayName ( mailboxDisplayName ) ;
if ( error ) throw new BoxError ( BoxError . BAD _FIELD , error . message ) ;
}
2021-10-01 12:09:13 -07:00
}
const task = {
args : { } ,
2022-05-31 17:53:09 -07:00
values : { enableMailbox , mailboxName , mailboxDomain , mailboxDisplayName }
2021-10-01 12:09:13 -07:00
} ;
2023-07-13 16:25:01 +05:30
const taskId = await addTask ( appId , exports . ISTATE _PENDING _SERVICES _CHANGE , task , auditSource ) ;
2021-10-01 12:09:13 -07:00
2022-05-31 17:53:09 -07:00
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , mailboxName , mailboxDomain , mailboxDisplayName , taskId } ) ;
2021-10-01 12:09:13 -07:00
return { taskId } ;
}
async function setInbox ( app , data , auditSource ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof data , 'object' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
assert . strictEqual ( typeof data . enable , 'boolean' ) ;
const enableInbox = data . enable ;
const appId = app . id ;
2023-07-13 16:25:01 +05:30
let error = checkAppState ( app , exports . ISTATE _PENDING _SERVICES _CHANGE ) ;
2021-10-01 12:09:13 -07:00
if ( error ) throw error ;
if ( ! app . manifest . addons ? . recvmail ) throw new BoxError ( BoxError . BAD _FIELD , 'App does not use recvmail addon' ) ;
const inboxName = data . inboxName || null ;
const inboxDomain = data . inboxDomain || null ;
if ( enableInbox ) {
const domain = await mail . getDomain ( data . inboxDomain ) ; // check if domain exists
if ( ! domain . enabled ) throw new BoxError ( BoxError . BAD _FIELD , 'Domain does not have incoming email enabled' ) ;
error = mail . validateName ( data . inboxName ) ;
2022-02-07 13:19:59 -08:00
if ( error ) throw new BoxError ( BoxError . BAD _FIELD , error . message ) ;
2021-08-20 09:19:44 -07:00
}
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
const task = {
args : { } ,
2021-10-01 12:09:13 -07:00
values : { enableInbox , inboxName , inboxDomain }
2021-08-20 09:19:44 -07:00
} ;
2023-07-13 16:25:01 +05:30
const taskId = await addTask ( appId , exports . ISTATE _PENDING _SERVICES _CHANGE , task , auditSource ) ;
2019-11-14 21:43:14 -08:00
2021-10-01 12:09:13 -07:00
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , enableInbox , inboxName , inboxDomain , taskId } ) ;
2019-11-14 21:43:14 -08:00
2021-08-20 09:19:44 -07:00
return { taskId } ;
2019-09-08 16:57:08 -07:00
}
2023-07-13 15:06:07 +05:30
async function setTurn ( app , enableTurn , auditSource ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof enableTurn , 'boolean' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
const appId = app . id ;
2024-05-13 08:43:28 +02:00
const error = checkAppState ( app , exports . ISTATE _PENDING _SERVICES _CHANGE ) ;
2023-07-13 15:06:07 +05:30
if ( error ) throw error ;
if ( ! app . manifest . addons ? . turn ) throw new BoxError ( BoxError . BAD _FIELD , 'App does not use turn addon' ) ;
if ( ! app . manifest . addons . turn . optional ) throw new BoxError ( BoxError . BAD _FIELD , 'turn service is not optional' ) ;
const task = {
args : { } ,
values : { enableTurn }
} ;
const taskId = await addTask ( appId , exports . ISTATE _PENDING _SERVICES _CHANGE , task , auditSource ) ;
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , enableTurn , taskId } ) ;
return { taskId } ;
}
2023-07-13 16:37:33 +05:30
async function setRedis ( app , enableRedis , auditSource ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof enableRedis , 'boolean' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
const appId = app . id ;
2024-05-13 08:43:28 +02:00
const error = checkAppState ( app , exports . ISTATE _PENDING _SERVICES _CHANGE ) ;
2023-07-13 16:37:33 +05:30
if ( error ) throw error ;
if ( ! app . manifest . addons ? . redis ) throw new BoxError ( BoxError . BAD _FIELD , 'App does not use redis addon' ) ;
if ( ! app . manifest . addons . redis . optional ) throw new BoxError ( BoxError . BAD _FIELD , 'redis service is not optional' ) ;
const task = {
args : { } ,
values : { enableRedis }
} ;
const taskId = await addTask ( appId , exports . ISTATE _PENDING _SERVICES _CHANGE , task , auditSource ) ;
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , enableRedis , taskId } ) ;
return { taskId } ;
}
2021-08-20 09:19:44 -07:00
async function setAutomaticBackup ( app , enable , auditSource ) {
2020-03-29 17:11:10 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2019-09-08 16:57:08 -07:00
assert . strictEqual ( typeof enable , 'boolean' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
2021-08-20 09:19:44 -07:00
await update ( appId , { enableBackup : enable } ) ;
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , enableBackup : enable } ) ;
2019-09-08 16:57:08 -07:00
}
2021-08-20 09:19:44 -07:00
async function setAutomaticUpdate ( app , enable , auditSource ) {
2020-03-29 17:11:10 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2019-09-08 16:57:08 -07:00
assert . strictEqual ( typeof enable , 'boolean' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
2021-08-20 09:19:44 -07:00
await update ( appId , { enableAutomaticUpdate : enable } ) ;
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , enableAutomaticUpdate : enable } ) ;
2019-09-08 16:57:08 -07:00
}
2021-08-17 14:04:29 -07:00
async function setReverseProxyConfig ( app , reverseProxyConfig , auditSource ) {
2020-03-29 17:11:10 -07:00
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' ) ;
2023-05-25 11:27:23 +02:00
reverseProxyConfig = Object . assign ( { robotsTxt : null , csp : null , hstsPreload : false } , 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 ) ;
2021-08-17 14:04:29 -07:00
if ( error ) throw error ;
2019-09-08 16:57:08 -07:00
2020-03-29 17:11:10 -07:00
error = validateRobotsTxt ( reverseProxyConfig . robotsTxt ) ;
2021-08-17 14:04:29 -07:00
if ( error ) throw error ;
2019-10-13 18:22:03 -07:00
2023-05-25 11:27:23 +02:00
await reverseProxy . writeAppConfigs ( Object . assign ( { } , app , { reverseProxyConfig } ) ) ;
2019-10-13 18:22:03 -07:00
2021-08-20 09:19:44 -07:00
await update ( appId , { reverseProxyConfig } ) ;
2019-09-09 21:41:55 -07:00
2021-08-20 09:19:44 -07:00
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , reverseProxyConfig } ) ;
2019-09-08 16:57:08 -07:00
}
2023-08-17 16:05:19 +05:30
async function getLocation ( subdomain , domain ) {
assert . strictEqual ( typeof subdomain , 'string' ) ;
assert . strictEqual ( typeof domain , 'string' ) ;
const result = await database . query ( ` SELECT ${ LOCATION _FIELDS } FROM locations WHERE subdomain=? AND domain=? ` , [ subdomain , domain ] ) ;
if ( result . length === 0 ) return null ;
return new Location ( subdomain , domain , result [ 0 ] . type , safe . JSON . parse ( result [ 0 ] . certificateJson ) ) ;
}
2021-08-17 14:04:29 -07:00
async function setCertificate ( app , data , auditSource ) {
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' ) ;
2022-07-14 11:57:04 +05:30
const { subdomain , domain , cert , key } = data ;
2019-09-08 16:57:08 -07:00
2021-08-17 14:04:29 -07:00
const domainObject = await domains . get ( domain ) ;
2022-07-14 11:57:04 +05:30
if ( domainObject === null ) throw new BoxError ( BoxError . NOT _FOUND , 'Domain not found' ) ;
2019-09-08 16:57:08 -07:00
2024-02-21 12:33:04 +01:00
if ( cert && key ) await reverseProxy . validateCertificate ( subdomain , domain , { cert , key } ) ;
2019-09-08 16:57:08 -07:00
2022-07-14 13:25:41 +05:30
const certificate = cert && key ? { cert , key } : null ;
const result = await database . query ( 'UPDATE locations SET certificateJson=? WHERE location=? AND domain=?' , [ certificate ? JSON . stringify ( certificate ) : null , subdomain , domain ] ) ;
2022-07-14 11:57:04 +05:30
if ( result . affectedRows === 0 ) throw new BoxError ( BoxError . NOT _FOUND , 'Location not found' ) ;
2023-08-17 16:05:19 +05:30
const location = await getLocation ( subdomain , domain ) ; // fresh location object with type
2022-11-28 22:32:34 +01:00
await reverseProxy . setUserCertificate ( app , location ) ;
2022-07-14 11:57:04 +05:30
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId : app . id , app , subdomain , domain , cert } ) ;
2019-09-08 16:57:08 -07:00
}
2021-08-20 09:19:44 -07:00
async function setLocation ( app , data , auditSource ) {
2020-03-29 17:11:10 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2019-09-08 16:57:08 -07:00
assert . strictEqual ( typeof data , 'object' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
let error = checkAppState ( app , exports . ISTATE _PENDING _LOCATION _CHANGE ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2019-09-08 16:57:08 -07:00
2022-01-16 18:43:19 -08:00
const values = {
subdomain : data . subdomain . toLowerCase ( ) ,
2020-03-29 17:11:10 -07:00
domain : data . domain . toLowerCase ( ) ,
// these are intentionally reset, if not set
2024-07-16 22:21:36 +02:00
portBindings : { } ,
2022-01-14 22:40:51 -08:00
secondaryDomains : [ ] ,
2022-01-14 22:29:47 -08:00
redirectDomains : [ ] ,
2021-01-18 17:26:26 -08:00
aliasDomains : [ ]
2020-03-29 17:11:10 -07:00
} ;
2019-09-21 19:45:55 -07:00
2024-07-16 22:21:36 +02:00
if ( 'ports' in data ) {
error = validatePorts ( data . ports || null , app . manifest ) ;
2024-02-25 16:28:57 +01:00
if ( error ) throw error ;
2024-07-16 22:21:36 +02:00
values . portBindings = translateToPortBindings ( data . ports || null , app . manifest ) ;
2020-03-29 17:11:10 -07:00
}
2019-09-08 16:57:08 -07:00
2021-10-01 09:37:33 -07:00
// rename the auto-created mailbox to match the new location
2021-12-06 13:59:00 -08:00
if ( app . manifest . addons ? . sendmail && app . mailboxName ? . endsWith ( '.app' ) ) {
2022-01-16 12:32:12 -08:00
values . mailboxName = mailboxNameForSubdomain ( values . subdomain , app . manifest ) ;
2020-03-30 22:18:39 -07:00
values . mailboxDomain = values . domain ;
}
2019-09-08 16:57:08 -07:00
2022-01-20 16:57:30 -08:00
error = validateSecondaryDomains ( data . secondaryDomains || { } , app . manifest ) ;
if ( error ) throw error ;
values . secondaryDomains = translateSecondaryDomains ( data . secondaryDomains || { } ) ;
2022-01-14 22:40:51 -08:00
2022-01-14 22:29:47 -08:00
if ( 'redirectDomains' in data ) {
values . redirectDomains = data . redirectDomains ;
2020-03-29 17:11:10 -07:00
}
2019-09-27 10:25:26 -07:00
2021-01-18 17:26:26 -08:00
if ( 'aliasDomains' in data ) {
values . aliasDomains = data . aliasDomains ;
}
2023-08-17 16:05:19 +05:30
const locations = [ new Location ( values . subdomain , values . domain , Location . TYPE _PRIMARY ) ]
. concat ( values . secondaryDomains . map ( sd => new Location ( sd . subdomain , sd . domain , Location . TYPE _SECONDARY ) ) )
. concat ( values . redirectDomains . map ( rd => new Location ( rd . subdomain , rd . domain , Location . TYPE _REDIRECT ) ) )
. concat ( values . aliasDomains . map ( ad => new Location ( ad . subdomain , ad . domain , Location . TYPE _ALIAS ) ) ) ;
2019-09-08 16:57:08 -07:00
2022-11-28 22:16:22 +01:00
error = await validateLocations ( locations ) ;
if ( error ) throw error ;
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
const task = {
args : {
2025-02-13 14:03:25 +01:00
oldConfig : _ . pick ( app , [ 'subdomain' , 'domain' , 'fqdn' , 'secondaryDomains' , 'redirectDomains' , 'aliasDomains' , 'portBindings' ] ) ,
2021-08-20 09:19:44 -07:00
skipDnsSetup : ! ! data . skipDnsSetup ,
overwriteDns : ! ! data . overwriteDns
} ,
values
} ;
2024-05-13 08:43:28 +02:00
const [ taskError , taskId ] = await safe ( addTask ( appId , exports . ISTATE _PENDING _LOCATION _CHANGE , task , auditSource ) ) ;
if ( taskError && taskError . reason !== BoxError . ALREADY _EXISTS ) throw taskError ;
2024-07-16 22:21:36 +02:00
if ( taskError && taskError . reason === BoxError . ALREADY _EXISTS ) throw getDuplicateErrorDetails ( taskError . message , locations , values . portBindings ) ;
2019-09-08 16:57:08 -07:00
2022-11-28 21:23:06 +01:00
values . fqdn = dns . fqdn ( values . subdomain , values . domain ) ;
values . secondaryDomains . forEach ( function ( ad ) { ad . fqdn = dns . fqdn ( ad . subdomain , ad . domain ) ; } ) ;
values . redirectDomains . forEach ( function ( ad ) { ad . fqdn = dns . fqdn ( ad . subdomain , ad . domain ) ; } ) ;
values . aliasDomains . forEach ( function ( ad ) { ad . fqdn = dns . fqdn ( ad . subdomain , ad . domain ) ; } ) ;
2019-09-27 11:24:21 -07:00
2023-05-25 11:27:23 +02:00
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , Object . assign ( { appId , app , taskId } , values ) ) ;
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
return { taskId } ;
2019-09-08 16:57:08 -07:00
}
2022-06-01 22:44:52 -07:00
async function setStorage ( app , volumeId , volumePrefix , auditSource ) {
2020-03-29 17:11:10 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2022-06-01 22:44:52 -07:00
assert ( volumeId === null || typeof volumeId === 'string' ) ;
assert ( volumePrefix === null || typeof volumePrefix === 'string' ) ;
2019-09-08 16:57:08 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
2024-05-13 08:43:28 +02:00
const error = checkAppState ( app , exports . ISTATE _PENDING _DATA _DIR _MIGRATION ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2019-09-08 16:57:08 -07:00
2022-06-01 22:44:52 -07:00
if ( volumeId ) {
2022-06-08 12:24:11 -07:00
await checkStorage ( app , volumeId , volumePrefix ) ;
2022-06-03 09:10:37 -07:00
} else {
volumeId = volumePrefix = null ;
2022-06-01 22:44:52 -07:00
}
2019-09-21 19:45:55 -07:00
2020-03-29 17:11:10 -07:00
const task = {
2022-06-01 22:44:52 -07:00
args : { newStorageVolumeId : volumeId , newStorageVolumePrefix : volumePrefix } ,
2021-09-30 10:31:50 -07:00
values : { }
2020-03-29 17:11:10 -07:00
} ;
2021-11-17 10:38:02 -08:00
const taskId = await addTask ( appId , exports . ISTATE _PENDING _DATA _DIR _MIGRATION , task , auditSource ) ;
2019-09-08 16:57:08 -07:00
2022-06-01 22:44:52 -07:00
await eventlog . add ( eventlog . ACTION _APP _CONFIGURE , auditSource , { appId , app , volumeId , volumePrefix , taskId } ) ;
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
return { taskId } ;
2019-09-08 16:57:08 -07:00
}
2021-08-20 09:19:44 -07:00
async function updateApp ( app , data , auditSource ) {
2020-03-29 17:11:10 -07:00
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
2024-07-17 08:58:43 +02:00
const skipBackup = ! ! data . skipBackup , appId = app . id , manifest = data . manifest ;
2025-06-26 17:08:14 +02:00
const values = { updateInfo : null } ; // clear update indicator immediately
2020-03-31 15:44:46 -07:00
2021-08-20 09:19:44 -07:00
if ( app . runState === exports . RSTATE _STOPPED ) throw 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 ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2016-06-04 19:06:16 -07:00
2020-03-29 17:11:10 -07:00
error = manifestFormat . parse ( manifest ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw new BoxError ( BoxError . BAD _FIELD , 'Manifest error:' + error . message ) ;
2015-07-20 00:09:47 -07:00
2024-03-30 18:25:33 +01:00
error = await checkManifest ( manifest ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2015-07-20 00:09:47 -07:00
2024-07-17 08:58:43 +02:00
const updateConfig = { skipBackup , manifest } ; // this will clear appStoreId when updating from a repo and set it if passed in for update route
if ( 'appStoreId' in data ) updateConfig . appStoreId = data . appStoreId ;
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 ) {
2021-08-20 09:19:44 -07:00
if ( ! data . force ) throw new BoxError ( BoxError . BAD _FIELD , 'manifest id does not match. force to override' ) ;
2020-03-29 17:11:10 -07:00
}
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 ) ) {
2021-08-20 09:19:44 -07:00
if ( ! data . force ) throw new BoxError ( BoxError . BAD _FIELD , 'Downgrades are not permitted for apps installed from AppStore. force to override' ) ;
2020-03-29 17:11:10 -07:00
}
2019-01-11 14:19:32 -08:00
2020-03-29 17:11:10 -07:00
if ( 'icon' in data ) {
if ( data . icon ) {
2021-04-30 13:18:15 -07:00
data . icon = Buffer . from ( data . icon , 'base64' ) ;
2025-02-10 14:58:36 +01:00
if ( data . icon . length === 0 ) throw new BoxError ( BoxError . BAD _FIELD , 'icon is not 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
2021-08-20 09:19:44 -07:00
if ( app . debugMode && ! data . force ) throw 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
2021-10-03 23:38:12 -07:00
if ( ! manifest . addons ? . sendmail ) { // clear if the update removed addon
2020-03-31 15:44:46 -07:00
values . mailboxName = values . mailboxDomain = null ;
2021-10-03 23:38:12 -07:00
} else if ( ! app . mailboxName || app . mailboxName . endsWith ( '.app' ) ) { // allocate since update added the addon
2022-01-16 12:32:12 -08:00
values . mailboxName = mailboxNameForSubdomain ( app . subdomain , manifest ) ;
2020-03-31 15:44:46 -07:00
values . mailboxDomain = app . domain ;
}
2024-02-27 13:45:08 +01:00
if ( ! manifest . addons ? . recvmail ) { // clear if the update removed addon. required for fk constraint
values . enableInbox = false ;
values . inboxName = values . inboxDomain = null ;
}
2023-04-21 15:36:05 +02:00
const hasSso = ! ! updateConfig . manifest . addons ? . proxyAuth || ! ! updateConfig . manifest . addons ? . ldap || ! ! manifest . addons ? . oidc ;
2022-04-25 23:11:18 -07:00
if ( ! hasSso && app . sso ) values . sso = false ; // turn off sso flag, if the update removes sso options
2020-03-29 17:11:10 -07:00
const task = {
args : { updateConfig } ,
2021-09-30 10:31:50 -07:00
values
2020-03-29 17:11:10 -07:00
} ;
2021-11-17 10:38:02 -08:00
const taskId = await addTask ( appId , exports . ISTATE _PENDING _UPDATE , task , auditSource ) ;
2015-07-20 00:09:47 -07:00
2021-08-20 09:19:44 -07:00
await eventlog . add ( eventlog . ACTION _APP _UPDATE , auditSource , { appId , app , skipBackup , toManifest : manifest , fromManifest : app . manifest , force : data . force , taskId } ) ;
2016-05-01 21:37:08 -07:00
2021-08-20 09:19:44 -07:00
return { taskId } ;
2015-07-20 00:09:47 -07:00
}
2022-01-04 09:12:45 -08:00
async function getLogPaths ( app ) {
2020-10-06 17:53:04 +02:00
assert . strictEqual ( typeof app , 'object' ) ;
const appId = app . id ;
2022-01-04 09:24:02 -08:00
const filePaths = [ ] ;
2020-10-06 17:53:04 +02:00
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 ` ) ) ;
2022-01-04 09:24:02 -08:00
if ( app . manifest . logPaths ) {
const [ error , result ] = await safe ( docker . inspect ( app . containerId ) ) ;
if ( ! error ) {
const runVolume = result . Mounts . find ( function ( mount ) { return mount . Destination === '/run' ; } ) ;
const tmpVolume = result . Mounts . find ( function ( mount ) { return mount . Destination === '/tmp' ; } ) ;
const dataVolume = result . Mounts . find ( function ( mount ) { return mount . Destination === '/app/data' ; } ) ;
// note: wild cards are not supported yet in logPaths since that will require shell expansion
for ( const logPath of app . manifest . logPaths ) {
if ( logPath . startsWith ( '/tmp/' ) ) filePaths . push ( ` ${ tmpVolume . Source } / ${ logPath . slice ( '/tmp/' . length ) } ` ) ;
else if ( logPath . startsWith ( '/run/' ) ) filePaths . push ( ` ${ runVolume . Source } / ${ logPath . slice ( '/run/' . length ) } ` ) ;
else if ( logPath . startsWith ( '/app/data/' ) ) filePaths . push ( ` ${ dataVolume . Source } / ${ logPath . slice ( '/app/data/' . length ) } ` ) ;
}
}
}
2020-10-06 17:53:04 +02:00
return filePaths ;
}
2021-10-01 09:23:20 -07:00
async function getLogs ( app , options ) {
2020-03-29 17:11:10 -07:00
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
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
2022-01-04 09:12:45 -08:00
const logPaths = await getLogPaths ( app ) ;
2024-07-25 17:32:14 +02:00
const cp = logs . tail ( logPaths , { lines : options . lines , follow : options . follow , sudo : true } ) ; // need sudo access for paths inside app container (manifest.logPaths)
2015-07-20 00:09:47 -07:00
2023-03-27 10:38:09 +02:00
const logStream = new logs . LogStream ( { format : options . format || 'json' , source : appId } ) ;
2024-02-24 17:18:38 +01:00
logStream . on ( 'close' , ( ) => cp . terminate ( ) ) ; // the caller has to call destroy() on logStream. destroy() of Transform emits 'close'
2018-06-04 21:24:02 +02:00
2022-11-06 13:44:47 +01:00
cp . stdout . pipe ( logStream ) ;
2018-06-14 12:21:43 +02:00
2022-11-06 13:44:47 +01:00
return logStream ;
2015-07-20 00:09:47 -07:00
}
2022-06-09 14:56:40 +02:00
// never fails just prints error
async function appendLogLine ( app , line ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof line , 'string' ) ;
const logFilePath = path . join ( paths . LOG _DIR , app . id , 'app.log' ) ;
2023-04-04 19:12:30 +02:00
const isoDate = new Date ( new Date ( ) . toUTCString ( ) ) . toISOString ( ) ;
2022-06-09 14:56:40 +02:00
2023-04-04 19:12:30 +02:00
if ( ! safe . fs . appendFileSync ( logFilePath , ` ${ isoDate } ${ line } \n ` ) ) console . error ( ` Could not append log line for app ${ app . id } at ${ logFilePath } : ${ safe . error . message } ` ) ;
2022-06-09 14:56:40 +02:00
}
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
2021-08-20 09:19:44 -07:00
async function repair ( app , data , auditSource ) {
2020-03-29 17:11:10 -07:00
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' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
2025-07-23 15:40:25 +02:00
let errorState = ( app . error && app . error . details . 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 ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw new BoxError ( BoxError . BAD _FIELD , ` manifest error: ${ error . message } ` ) ;
2019-09-19 17:04:11 -07:00
2024-03-30 18:25:33 +01:00
error = await checkManifest ( data . manifest ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2019-09-21 19:45:55 -07:00
2021-10-01 12:09:13 -07:00
if ( ! data . manifest . addons ? . sendmail ) { // clear if repair removed addon
2020-03-31 15:44:46 -07:00
task . values . mailboxName = task . values . mailboxDomain = null ;
} else if ( ! app . mailboxName || app . mailboxName . endsWith ( '.app' ) ) { // allocate since repair added the addon
2022-01-16 12:32:12 -08:00
task . values . mailboxName = mailboxNameForSubdomain ( app . subdomain , data . manifest ) ;
2020-03-31 15:44:46 -07:00
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 ) {
2024-05-13 08:43:28 +02:00
const newManifest = Object . assign ( { } , app . manifest , { dockerImage : data . dockerImage } ) ;
2020-03-29 17:11:10 -07:00
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
2021-11-17 10:38:02 -08:00
const taskId = await addTask ( appId , errorState , task , auditSource ) ;
2019-10-03 11:35:27 -07:00
2021-08-20 09:19:44 -07:00
await eventlog . add ( eventlog . ACTION _APP _REPAIR , auditSource , { app , taskId } ) ;
2019-10-03 11:35:27 -07:00
2021-08-20 09:19:44 -07:00
return { 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
2025-07-25 12:55:14 +02:00
const backup = backupId ? await backups . get ( backupId ) : { manifest : app . manifest } ;
if ( ! backup ) throw new BoxError ( BoxError . BAD _FIELD , 'No such backup' ) ;
const manifest = backup . manifest ;
2019-09-21 19:45:55 -07:00
2024-02-27 11:35:14 +01:00
if ( ! manifest ) throw new BoxError ( BoxError . EXTERNAL _ERROR , 'Could not get restore manifest' ) ;
2025-07-25 12:55:14 +02:00
if ( backup . 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
2024-03-30 18:25:33 +01:00
error = await checkManifest ( manifest ) ;
2021-07-14 11:07:19 -07:00
if ( error ) throw error ;
2016-06-13 13:44:49 -07:00
2024-02-27 11:35:14 +01:00
const values = { manifest } ;
if ( ! manifest . addons ? . sendmail ) { // clear if restore removed addon
2021-07-14 11:07:19 -07:00
values . mailboxName = values . mailboxDomain = null ;
} else if ( ! app . mailboxName || app . mailboxName . endsWith ( '.app' ) ) { // allocate since restore added the addon
2024-02-27 11:35:14 +01:00
values . mailboxName = mailboxNameForSubdomain ( app . subdomain , manifest ) ;
2021-07-14 11:07:19 -07:00
values . mailboxDomain = app . domain ;
}
2016-06-13 18:11:11 -07:00
2024-02-27 13:45:08 +01:00
if ( ! manifest . addons ? . recvmail ) { // recvmail is always optional. clear if restore removed addon
values . enableInbox = false ;
values . inboxName = values . inboxDomain = null ;
}
2025-07-25 12:55:14 +02:00
const restoreConfig = { backupId : backup . id } ;
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-11-17 10:38:02 -08:00
const taskId = await addTask ( appId , exports . ISTATE _PENDING _RESTORE , task , auditSource ) ;
2015-08-19 10:54:39 -07:00
2025-07-25 12:55:14 +02:00
await eventlog . add ( eventlog . ACTION _APP _RESTORE , auditSource , { app , backupId : backup . id , remotePath : backup . remotePath , fromManifest : app . manifest , toManifest : manifest , taskId } ) ;
2016-05-01 21:37:08 -07:00
2021-08-20 09:19:44 -07:00
return { taskId } ;
2015-07-20 00:09:47 -07:00
}
2021-08-20 09:19:44 -07:00
async function importApp ( app , data , auditSource ) {
2020-03-29 17:11:10 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2019-10-15 09:16:29 -07:00
assert . strictEqual ( typeof data , 'object' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
2020-03-29 17:11:10 -07:00
const appId = app . id ;
2019-10-15 09:16:29 -07:00
2025-08-01 23:20:51 +02:00
const error = checkAppState ( app , exports . ISTATE _PENDING _IMPORT ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2019-12-04 18:54:25 -08:00
2025-08-01 23:20:51 +02:00
let restoreConfig ;
2022-10-02 10:08:50 +02:00
if ( data . remotePath ) { // if not provided, we import in-place
2025-08-01 23:20:51 +02:00
const backupTarget = await backupTargets . createPseudo ( {
id : ` appimport- ${ app . id } ` ,
provider : data . provider ,
config : data . config ,
format : data . format ,
encryptionPassword : data . encryptionPassword ,
encryptedFilenames : data . encryptedFilenames
} ) ;
2024-04-09 13:24:33 +02:00
2025-08-01 23:20:51 +02:00
restoreConfig = { remotePath : data . remotePath , backupTarget } ;
2025-07-28 11:45:10 +02:00
} else { // inPlace
2025-08-01 13:16:57 +02:00
restoreConfig = { inPlace : true } ;
2022-10-02 10:08:50 +02:00
}
2019-10-15 09:16:29 -07:00
2021-08-20 09:19:44 -07:00
const task = {
args : {
restoreConfig ,
oldManifest : app . manifest ,
skipDnsSetup : false ,
overwriteDns : true
} ,
values : { }
} ;
2021-11-17 10:38:02 -08:00
const taskId = await addTask ( appId , exports . ISTATE _PENDING _IMPORT , task , auditSource ) ;
2019-10-15 09:16:29 -07:00
2025-08-01 23:20:51 +02:00
await eventlog . add ( eventlog . ACTION _APP _IMPORT , auditSource , { app , remotePath : data . remotePath , inPlace : data . inPlace , taskId } ) ;
2019-12-04 18:54:25 -08:00
2021-08-20 09:19:44 -07:00
return { taskId } ;
2019-10-15 09:16:29 -07:00
}
2021-08-20 09:19:44 -07:00
async function exportApp ( app , data , auditSource ) {
2020-12-06 19:38:50 -08:00
assert . strictEqual ( typeof app , 'object' ) ;
2021-08-20 09:19:44 -07:00
assert . strictEqual ( typeof data , 'object' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
2020-12-06 19:38:50 -08:00
const appId = app . id ;
2025-07-18 10:56:52 +02:00
if ( ! canBackupApp ( app ) ) throw new BoxError ( BoxError . BAD _STATE , 'App cannot be backed up in this state' ) ;
2020-12-06 19:38:50 -08:00
2025-07-25 12:55:14 +02:00
const backupTarget = await backupTargets . getPrimary ( ) ;
2025-07-24 19:02:02 +02:00
const taskId = await tasks . add ( ` ${ tasks . TASK _APP _BACKUP _PREFIX } ${ app . id } ` , [ appId , backupTarget . id , { snapshotOnly : true } ] ) ;
2025-07-18 10:56:52 +02:00
safe ( tasks . startTask ( taskId , { } ) , { debug } ) ; // background
2021-08-20 09:19:44 -07:00
return { taskId } ;
2020-12-06 19:38:50 -08:00
}
2021-08-20 09:19:44 -07:00
async function clone ( app , data , user , auditSource ) {
2020-03-29 17:11:10 -07:00
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' ) ;
2022-01-16 12:32:12 -08:00
const subdomain = data . subdomain . toLowerCase ( ) ,
2017-11-02 22:17:44 +01:00
domain = data . domain . toLowerCase ( ) ,
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 ,
2024-02-27 11:44:42 +01:00
skipDnsSetup = 'skipDnsSetup' in data ? data . skipDnsSetup : false ;
2016-06-17 17:12:55 -05:00
assert . strictEqual ( typeof backupId , 'string' ) ;
2022-01-16 12:32:12 -08:00
assert . strictEqual ( typeof subdomain , 'string' ) ;
2017-11-02 22:17:44 +01:00
assert . strictEqual ( typeof domain , 'string' ) ;
2016-06-17 17:12:55 -05:00
2025-07-25 12:55:14 +02:00
const backup = await backups . get ( backupId ) ;
2016-06-17 17:12:55 -05:00
2025-07-25 12:55:14 +02:00
if ( ! backup ) throw new BoxError ( BoxError . NOT _FOUND , 'Backup not found' ) ;
if ( ! backup . manifest ) throw new BoxError ( BoxError . EXTERNAL _ERROR , 'Could not detect restore manifest' ) ;
if ( backup . encryptionVersion === 1 ) throw new BoxError ( BoxError . BAD _FIELD , 'This encrypted backup was created with an older Cloudron version and cannot be cloned' ) ;
2021-07-14 11:07:19 -07:00
2025-07-25 12:55:14 +02:00
const manifest = backup . manifest , appStoreId = app . appStoreId ;
2016-06-17 17:12:55 -05:00
2022-02-01 23:36:41 -08:00
let error = validateSecondaryDomains ( data . secondaryDomains || { } , manifest ) ;
if ( error ) throw error ;
const secondaryDomains = translateSecondaryDomains ( data . secondaryDomains || { } ) ;
2023-08-17 16:05:19 +05:30
const locations = [ new Location ( subdomain , domain , Location . TYPE _PRIMARY ) ]
. concat ( secondaryDomains . map ( sd => new Location ( sd . subdomain , sd . domain , Location . TYPE _SECONDARY ) ) ) ;
2022-02-01 23:36:41 -08:00
2022-11-28 22:16:22 +01:00
error = await validateLocations ( locations ) ;
if ( error ) throw error ;
2022-02-01 23:36:41 -08:00
2021-08-20 09:19:44 -07:00
// re-validate because this new box version may not accept old configs
2024-03-30 18:25:33 +01:00
error = await checkManifest ( manifest ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2016-06-17 17:12:55 -05:00
2024-07-16 22:21:36 +02:00
error = validatePorts ( data . ports || null , manifest ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2024-07-16 22:21:36 +02:00
const portBindings = translateToPortBindings ( data . ports || null , manifest ) ;
2018-12-12 12:55:35 -08:00
2021-08-20 09:19:44 -07:00
// should we copy the original app's mailbox settings instead?
2022-01-16 12:32:12 -08:00
const mailboxName = manifest . addons ? . sendmail ? mailboxNameForSubdomain ( subdomain , manifest ) : null ;
2021-10-01 09:37:33 -07:00
const mailboxDomain = manifest . addons ? . sendmail ? domain : null ;
2021-08-20 09:19:44 -07:00
2025-07-28 12:53:27 +02:00
const newAppId = crypto . randomUUID ( ) ;
2021-08-20 09:19:44 -07:00
2024-12-10 22:43:06 +01:00
// label, checklist intentionally omitted . icon is loaded in apptask from the backup
2025-07-25 12:55:14 +02:00
const dolly = _ . pick ( backup . appConfig || app , [ 'memoryLimit' , 'cpuQuota' , 'crontab' , 'reverseProxyConfig' , 'env' , 'servicesConfig' , 'tags' , 'devices' ,
2024-12-10 16:48:17 +01:00
'enableMailbox' , 'mailboxDisplayName' , 'mailboxName' , 'mailboxDomain' , 'enableInbox' , 'inboxName' , 'inboxDomain' , 'debugMode' ,
2024-12-10 17:16:06 +01:00
'enableTurn' , 'enableRedis' , 'mounts' , 'enableBackup' , 'enableAutomaticUpdate' , 'accessRestriction' , 'operators' , 'sso' ,
2025-02-13 14:03:25 +01:00
'notes' , 'checklist' ] ) ;
2024-02-27 11:49:12 +01:00
2024-02-27 13:45:08 +01:00
if ( ! manifest . addons ? . recvmail ) dolly . inboxDomain = null ; // needed because we are cloning _current_ app settings with old manifest
2024-02-27 11:49:12 +01:00
const obj = Object . assign ( dolly , {
2021-08-20 09:19:44 -07:00
installationState : exports . ISTATE _PENDING _CLONE ,
runState : exports . RSTATE _RUNNING ,
2022-02-01 23:36:41 -08:00
mailboxName ,
mailboxDomain ,
secondaryDomains ,
2022-01-14 22:29:47 -08:00
redirectDomains : [ ] ,
2021-08-20 09:19:44 -07:00
aliasDomains : [ ] ,
2024-12-10 21:07:59 +01:00
label : dolly . label ? ` ${ dolly . label } -clone ` : '' ,
2024-02-27 11:49:12 +01:00
} ) ;
2016-06-17 17:12:55 -05:00
2024-02-27 13:45:08 +01:00
const [ addError ] = await safe ( add ( newAppId , appStoreId , manifest , subdomain , domain , portBindings , obj ) ) ;
if ( addError && addError . reason === BoxError . ALREADY _EXISTS ) throw getDuplicateErrorDetails ( addError . message , locations , portBindings ) ;
2021-08-20 09:19:44 -07:00
if ( addError ) throw addError ;
2018-05-03 13:20:34 +02:00
2025-07-25 12:55:14 +02:00
const restoreConfig = { backupId : backup . id } ;
2021-08-20 09:19:44 -07:00
const task = {
args : { restoreConfig , overwriteDns , skipDnsSetup , oldManifest : null } ,
values : { } ,
requiredState : exports . ISTATE _PENDING _CLONE
} ;
2021-11-17 10:38:02 -08:00
const taskId = await addTask ( newAppId , exports . ISTATE _PENDING _CLONE , task , auditSource ) ;
2018-01-11 10:59:30 -08:00
2025-02-13 14:43:44 +01:00
const newApp = Object . assign ( { } , _ . omit ( obj , [ 'icon' ] ) , { appStoreId , manifest , subdomain , domain , portBindings } ) ;
2022-11-28 21:23:06 +01:00
newApp . fqdn = dns . fqdn ( newApp . subdomain , newApp . domain ) ;
newApp . secondaryDomains . forEach ( function ( ad ) { ad . fqdn = dns . fqdn ( ad . subdomain , ad . domain ) ; } ) ;
newApp . redirectDomains . forEach ( function ( ad ) { ad . fqdn = dns . fqdn ( ad . subdomain , ad . domain ) ; } ) ;
newApp . aliasDomains . forEach ( function ( ad ) { ad . fqdn = dns . fqdn ( ad . subdomain , ad . domain ) ; } ) ;
2021-01-18 17:26:26 -08:00
2025-07-25 12:55:14 +02:00
await eventlog . add ( eventlog . ACTION _APP _CLONE , auditSource , { appId : newAppId , oldAppId : app . id , backupId , remotePath : backup . remotePath , oldApp : app , newApp , taskId } ) ;
2020-03-29 17:11:10 -07:00
2021-08-20 09:19:44 -07:00
return { id : newAppId , taskId } ;
2016-06-17 17:12:55 -05:00
}
2024-12-10 17:19:12 +01:00
async function unarchive ( archive , data , auditSource ) {
assert . strictEqual ( typeof archive , 'object' ) ;
assert . strictEqual ( typeof data , 'object' ) ;
assert ( auditSource && typeof auditSource === 'object' ) ;
2025-07-25 01:34:29 +02:00
const backup = await backups . get ( archive . backupId ) ;
2025-07-25 12:55:14 +02:00
const restoreConfig = { backupId : backup . id } ;
2024-12-10 19:05:31 +01:00
const subdomain = data . subdomain . toLowerCase ( ) ,
domain = data . domain . toLowerCase ( ) ,
overwriteDns = 'overwriteDns' in data ? data . overwriteDns : false ;
2024-12-19 14:21:39 +01:00
const manifest = backup . manifest , appStoreId = backup . manifest . id ;
2024-12-10 17:19:12 +01:00
2024-12-10 19:05:31 +01:00
let error = validateSecondaryDomains ( data . secondaryDomains || { } , manifest ) ;
if ( error ) throw error ;
const secondaryDomains = translateSecondaryDomains ( data . secondaryDomains || { } ) ;
const locations = [ new Location ( subdomain , domain , Location . TYPE _PRIMARY ) ]
. concat ( secondaryDomains . map ( sd => new Location ( sd . subdomain , sd . domain , Location . TYPE _SECONDARY ) ) ) ;
error = await validateLocations ( locations ) ;
if ( error ) throw error ;
// re-validate because this new box version may not accept old configs
error = await checkManifest ( manifest ) ;
if ( error ) throw error ;
error = validatePorts ( data . ports || null , manifest ) ;
if ( error ) throw error ;
const portBindings = translateToPortBindings ( data . ports || null , manifest ) ;
2025-07-28 12:53:27 +02:00
const appId = crypto . randomUUID ( ) ;
2024-12-10 19:05:31 +01:00
2024-12-19 14:21:39 +01:00
// appConfig is null for pre-8.2 backups
2025-02-13 14:03:25 +01:00
const dolly = _ . pick ( backup . appConfig || { } , [ 'memoryLimit' , 'cpuQuota' , 'crontab' , 'reverseProxyConfig' , 'env' , 'servicesConfig' ,
2024-12-10 17:19:12 +01:00
'tags' , 'label' , 'enableMailbox' , 'mailboxDisplayName' , 'mailboxName' , 'mailboxDomain' , 'enableInbox' , 'inboxName' , 'inboxDomain' , 'devices' ,
'enableTurn' , 'enableRedis' , 'mounts' , 'enableBackup' , 'enableAutomaticUpdate' , 'accessRestriction' , 'operators' , 'sso' ,
2025-02-13 14:03:25 +01:00
'notes' , 'checklist' ] ) ;
2024-12-10 17:19:12 +01:00
// intentionally not filled up: redirectDomain, aliasDomains, mailboxDomain
2024-12-10 19:05:31 +01:00
const obj = Object . assign ( dolly , {
secondaryDomains ,
redirectDomains : [ ] ,
aliasDomains : [ ] ,
2024-12-10 17:19:12 +01:00
mailboxDomain : data . domain , // archive's mailboxDomain may not exist
2024-12-10 19:05:31 +01:00
runState : exports . RSTATE _RUNNING ,
2024-12-19 14:21:39 +01:00
installationState : exports . ISTATE _PENDING _INSTALL ,
sso : backup . appConfig ? backup . appConfig . sso : true // when no appConfig take a blind guess
2024-12-10 17:19:12 +01:00
} ) ;
2024-12-10 19:05:31 +01:00
obj . icon = ( await archives . getIcons ( archive . id ) ) ? . icon ;
const [ addError ] = await safe ( add ( appId , appStoreId , manifest , subdomain , domain , portBindings , obj ) ) ;
if ( addError && addError . reason === BoxError . ALREADY _EXISTS ) throw getDuplicateErrorDetails ( addError . message , locations , portBindings ) ;
if ( addError ) throw addError ;
const task = {
args : { restoreConfig , overwriteDns } ,
values : { } ,
requiredState : obj . installationState
} ;
const taskId = await addTask ( appId , obj . installationState , task , auditSource ) ;
2024-12-10 17:19:12 +01:00
2025-02-13 14:43:44 +01:00
const newApp = Object . assign ( { } , _ . omit ( obj , [ 'icon' ] ) , { appStoreId , manifest , subdomain , domain , portBindings } ) ;
2024-12-10 19:05:31 +01:00
newApp . fqdn = dns . fqdn ( newApp . subdomain , newApp . domain ) ;
newApp . secondaryDomains . forEach ( function ( ad ) { ad . fqdn = dns . fqdn ( ad . subdomain , ad . domain ) ; } ) ;
newApp . redirectDomains . forEach ( function ( ad ) { ad . fqdn = dns . fqdn ( ad . subdomain , ad . domain ) ; } ) ;
newApp . aliasDomains . forEach ( function ( ad ) { ad . fqdn = dns . fqdn ( ad . subdomain , ad . domain ) ; } ) ;
await eventlog . add ( eventlog . ACTION _APP _INSTALL , auditSource , { appId , app : newApp , taskId } ) ;
return { id : appId , taskId } ;
2024-12-10 17:19:12 +01:00
}
2021-08-20 09:19:44 -07:00
async function uninstall ( app , auditSource ) {
2020-03-29 17:11:10 -07:00
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
2020-03-29 17:11:10 -07:00
const appId = app . id ;
2024-05-13 08:43:28 +02:00
const error = checkAppState ( app , exports . ISTATE _PENDING _UNINSTALL ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw error ;
2015-07-20 00:09:47 -07:00
2021-08-20 09:19:44 -07:00
const task = {
args : { } ,
values : { } ,
requiredState : null // can run in any state, as long as no task is active
} ;
2021-11-17 10:38:02 -08:00
const taskId = await addTask ( appId , exports . ISTATE _PENDING _UNINSTALL , task , auditSource ) ;
2021-08-20 09:19:44 -07:00
await eventlog . add ( eventlog . ACTION _APP _UNINSTALL , auditSource , { appId , app , taskId } ) ;
2016-05-01 21:37:08 -07:00
2021-08-20 09:19:44 -07:00
return { taskId } ;
2015-07-20 00:09:47 -07:00
}
2024-12-09 18:28:35 +01:00
async function archive ( app , backupId , auditSource ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof backupId , 'string' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
2024-12-10 16:29:34 +01:00
if ( app . manifest . id === constants . PROXY _APP _APPSTORE _ID ) throw new BoxError ( BoxError . BAD _FIELD , 'cannot archive proxy app' ) ;
2025-07-25 01:34:29 +02:00
const result = await backups . getByIdentifierAndStatePaged ( app . id , backups . BACKUP _STATE _NORMAL , 1 , 1 ) ;
2024-12-09 18:28:35 +01:00
if ( result . length === 0 ) throw new BoxError ( BoxError . BAD _STATE , 'No recent backup to archive' ) ;
if ( result [ 0 ] . id !== backupId ) throw new BoxError ( BoxError . BAD _STATE , 'Latest backup id has changed' ) ;
2024-12-09 23:20:44 +01:00
const icons = await getIcons ( app . id ) ;
2024-12-17 14:33:36 +01:00
const archiveId = await archives . add ( backupId , { icon : icons . icon , appStoreIcon : icons . appStoreIcon , appConfig : app } , auditSource ) ;
2024-12-09 18:28:35 +01:00
const { taskId } = await uninstall ( app , auditSource ) ;
2024-12-17 14:33:36 +01:00
return { taskId , id : archiveId } ;
2024-12-09 18:28:35 +01:00
}
2021-08-20 09:19:44 -07:00
async function start ( app , auditSource ) {
2020-03-29 17:11:10 -07:00
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
2020-03-29 17:11:10 -07:00
const appId = app . id ;
2024-05-13 08:43:28 +02:00
const error = checkAppState ( app , exports . ISTATE _PENDING _START ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw 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 }
} ;
2021-11-17 10:38:02 -08:00
const taskId = await addTask ( appId , exports . ISTATE _PENDING _START , task , auditSource ) ;
2021-08-20 09:19:44 -07:00
await eventlog . add ( eventlog . ACTION _APP _START , auditSource , { appId , app , taskId } ) ;
return { taskId } ;
2015-07-20 00:09:47 -07:00
}
2021-08-20 09:19:44 -07:00
async function stop ( app , auditSource ) {
2020-03-29 17:11:10 -07:00
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
2020-03-29 17:11:10 -07:00
const appId = app . id ;
2024-05-13 08:43:28 +02:00
const error = checkAppState ( app , exports . ISTATE _PENDING _STOP ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw 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 }
} ;
2021-11-17 10:38:02 -08:00
const taskId = await addTask ( appId , exports . ISTATE _PENDING _STOP , task , auditSource ) ;
2019-08-29 09:10:39 -07:00
2021-08-20 09:19:44 -07:00
await eventlog . add ( eventlog . ACTION _APP _STOP , auditSource , { appId , app , taskId } ) ;
2020-03-19 17:02:42 -07:00
2021-08-20 09:19:44 -07:00
return { taskId } ;
2015-07-20 00:09:47 -07:00
}
2021-08-20 09:19:44 -07:00
async function restart ( app , auditSource ) {
2020-03-29 17:11:10 -07:00
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
2020-03-29 17:11:10 -07:00
const appId = app . id ;
2024-05-13 08:43:28 +02:00
const error = checkAppState ( app , exports . ISTATE _PENDING _RESTART ) ;
2021-08-20 09:19:44 -07:00
if ( error ) throw 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 }
} ;
2021-11-17 10:38:02 -08:00
const taskId = await addTask ( appId , exports . ISTATE _PENDING _RESTART , task , auditSource ) ;
2019-12-20 10:29:29 -08:00
2021-08-20 09:19:44 -07:00
await eventlog . add ( eventlog . ACTION _APP _RESTART , auditSource , { appId , app , taskId } ) ;
2020-03-19 17:02:42 -07:00
2021-08-20 09:19:44 -07:00
return { taskId } ;
2019-12-20 10:29:29 -08:00
}
2024-03-30 18:25:33 +01:00
async function checkManifest ( manifest ) {
2016-06-13 18:02:57 -07:00
assert ( manifest && typeof manifest === 'object' ) ;
2021-12-06 17:43:50 -08: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
}
2024-03-30 18:31:57 +01:00
const error = await services . checkAddonsSupport ( manifest . addons || { } ) ;
return error ;
2015-07-20 00:09:47 -07:00
}
2022-05-16 10:26:30 -07:00
async function createExec ( app , options ) {
2020-03-29 17:11:10 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2015-07-20 00:09:47 -07:00
assert ( options && typeof options === 'object' ) ;
2024-06-11 14:01:13 +02:00
if ( app . manifest . id === constants . PROXY _APP _APPSTORE _ID ) throw new BoxError ( BoxError . BAD _FIELD , 'cannot exec on proxy app' ) ;
2024-06-10 10:53:00 +02:00
2021-08-25 19:41:46 -07:00
const 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 ) {
2021-08-25 19:41:46 -07:00
throw new BoxError ( BoxError . BAD _STATE , 'App not installed or running' ) ;
2020-03-29 17:11:10 -07:00
}
2018-10-27 14:15:52 -07:00
2022-05-16 10:26:30 -07:00
const createOptions = {
2020-03-29 17:11:10 -07:00
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 ,
2022-11-16 16:08:54 +01:00
Cmd : cmd
2020-03-29 17:11:10 -07:00
} ;
2022-11-16 16:08:54 +01:00
// currently the webterminal and cli sets C.UTF-8
if ( options . lang ) createOptions . Env = [ 'LANG=' + options . lang ] ;
2022-05-16 10:26:30 -07:00
return await docker . createExec ( app . containerId , createOptions ) ;
}
async function startExec ( app , execId , options ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof execId , 'string' ) ;
assert ( options && typeof options === 'object' ) ;
if ( app . installationState !== exports . ISTATE _INSTALLED || app . runState !== exports . RSTATE _RUNNING ) {
throw new BoxError ( BoxError . BAD _STATE , 'App not installed or running' ) ;
}
2021-08-25 19:41:46 -07:00
const startOptions = {
2020-03-29 17:11:10 -07:00
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
} ;
2022-05-16 10:26:30 -07:00
const stream = await docker . startExec ( execId , startOptions ) ;
if ( options . rows && options . columns ) {
// there is a race where resizing too early results in a 404 "no such exec"
// https://git.cloudron.io/cloudron/box/issues/549
setTimeout ( async function ( ) {
await safe ( docker . resizeExec ( execId , { h : options . rows , w : options . columns } , { debug } ) ) ;
} , 2000 ) ;
}
2021-08-25 19:41:46 -07:00
return stream ;
2015-07-20 00:09:47 -07:00
}
2022-05-16 10:26:30 -07:00
async function getExec ( app , execId ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof execId , 'string' ) ;
return await docker . getExec ( execId ) ;
}
2025-07-18 10:56:52 +02:00
function canBackupApp ( app ) {
// only backup apps that are installed or specific pending states
// stopped apps cannot be backed up because addons might be down (redis)
if ( app . runState === exports . RSTATE _STOPPED ) return false ;
// we used to check the health here but that doesn't work for stopped apps. it's better to just fail
// and inform the user if the backup fails and the app addons have not been setup yet.
return app . installationState === exports . ISTATE _INSTALLED ||
app . installationState === exports . ISTATE _PENDING _CONFIGURE ||
app . installationState === exports . ISTATE _PENDING _UPDATE ; // called from apptask
}
2021-09-30 10:45:25 -07:00
async function backup ( app , auditSource ) {
2020-03-29 17:11:10 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2021-09-30 10:45:25 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2015-07-20 00:09:47 -07:00
2025-07-18 10:56:52 +02:00
if ( ! canBackupApp ( app ) ) throw new BoxError ( BoxError . BAD _STATE , 'App cannot be backed up in this state' ) ;
2019-09-21 19:45:55 -07:00
2025-07-25 12:55:14 +02:00
const backupTarget = await backupTargets . getPrimary ( ) ;
2015-07-20 00:09:47 -07:00
2025-07-24 19:02:02 +02:00
const taskId = await tasks . add ( ` ${ tasks . TASK _APP _BACKUP _PREFIX } ${ app . id } ` , [ app . id , backupTarget . id , { snapshotOnly : false } ] ) ;
const memoryLimit = backupTarget . limits ? . memoryLimit ? Math . max ( backupTarget . limits . memoryLimit / 1024 / 1024 , 1024 ) : 1024 ;
2025-07-18 10:56:52 +02:00
// background
tasks . startTask ( taskId , { timeout : 24 * 60 * 60 * 1000 /* 24 hours */ , nice : 15 , memoryLimit , oomScoreAdjust : - 999 } )
. then ( async ( backupId ) => {
2025-07-25 01:34:29 +02:00
const backup = await backups . get ( backupId ) ; // if task crashed, no result
2025-07-18 10:56:52 +02:00
await eventlog . add ( eventlog . ACTION _APP _BACKUP _FINISH , auditSource , { app , success : ! ! backup , errorMessage : '' , remotePath : backup ? . remotePath , backupId : backupId } ) ;
} )
. catch ( async ( error ) => {
await eventlog . add ( eventlog . ACTION _APP _BACKUP _FINISH , auditSource , { app , success : false , errorMessage : error . message } ) ;
2025-07-18 18:11:56 +02:00
} )
. finally ( async ( ) => {
await locks . releaseByTaskId ( taskId ) ;
2025-07-18 10:56:52 +02:00
} ) ;
await eventlog . add ( eventlog . ACTION _APP _BACKUP , auditSource , { app , appId : app . id , taskId } ) ;
2019-08-27 20:55:49 -07:00
2021-08-20 09:19:44 -07:00
return { 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
2025-07-25 01:34:29 +02: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
2022-04-02 17:09:08 -07:00
async function updateBackup ( app , backupId , data ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof backupId , 'string' ) ;
assert . strictEqual ( typeof data , 'object' ) ;
2025-07-25 01:34:29 +02:00
const backup = await backups . get ( backupId ) ;
2022-04-02 17:09:08 -07:00
if ( ! backup ) throw new BoxError ( BoxError . NOT _FOUND , 'Backup not found' ) ;
if ( backup . identifier !== app . id ) throw new BoxError ( BoxError . NOT _FOUND , 'Backup not found' ) ; // some other app's backup
2025-07-25 11:49:13 +02:00
await backups . update ( backup , data ) ;
2022-04-02 17:09:08 -07:00
}
2022-11-03 22:13:57 +01:00
async function getBackupDownloadStream ( app , backupId ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof backupId , 'string' ) ;
2025-07-25 01:34:29 +02:00
const backup = await backups . get ( backupId ) ;
2022-11-03 22:13:57 +01:00
if ( ! backup ) throw new BoxError ( BoxError . NOT _FOUND , 'Backup not found' ) ;
if ( backup . identifier !== app . id ) throw new BoxError ( BoxError . NOT _FOUND , 'Backup not found' ) ; // some other app's backup
if ( backup . format !== 'tgz' ) throw new BoxError ( BoxError . BAD _STATE , 'only tgz backups can be downloaded' ) ;
2025-07-25 12:55:14 +02:00
const backupTarget = await backupTargets . get ( backup . targetId ) ;
if ( ! backupTarget ) throw new BoxError ( BoxError . NOT _FOUND , 'Backup target not found' ) ; // not possible
2022-11-03 22:13:57 +01:00
2023-07-24 22:25:06 +05:30
const ps = new PassThrough ( ) ;
2025-08-01 20:49:11 +02:00
const stream = await backupTargets . storageApi ( backupTarget ) . download ( backupTarget . config , backupTargets . getBackupFilePath ( backupTarget , backup . remotePath ) ) ;
2023-07-24 22:25:06 +05:30
stream . on ( 'error' , function ( error ) {
debug ( ` getBackupDownloadStream: read stream error: ${ error . message } ` ) ;
ps . emit ( 'error' , new BoxError ( BoxError . EXTERNAL _ERROR , error ) ) ;
2022-11-03 22:13:57 +01:00
} ) ;
2023-07-24 22:25:06 +05:30
stream . pipe ( ps ) ;
const now = ( new Date ( ) ) . toISOString ( ) . replace ( /:|T/g , '-' ) . replace ( /\..*/ , '' ) ;
const encryptionSuffix = backup . encryptionVersion ? '.enc' : '' ;
const filename = ` app-backup- ${ now } ( ${ app . fqdn } ).tar.gz ${ encryptionSuffix } ` ;
return { stream : ps , filename } ;
2022-11-03 22:13:57 +01:00
}
2023-08-21 18:18:03 +05:30
async function restoreApps ( apps , options , auditSource ) {
assert ( Array . isArray ( apps ) ) ;
2021-02-24 16:29:43 -08:00
assert . strictEqual ( typeof options , 'object' ) ;
2021-11-17 10:33:28 -08:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2016-05-24 10:33:10 -07:00
2021-08-20 09:19:44 -07:00
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 _RESTORE ) ; // safeguard against tasks being created non-stop if we crash on startup
for ( const app of apps ) {
2025-07-25 01:34:29 +02:00
const [ error , result ] = await safe ( backups . getByIdentifierAndStatePaged ( app . id , backups . BACKUP _STATE _NORMAL , 1 , 1 ) ) ;
2021-08-20 09:19:44 -07:00
let installationState , restoreConfig , oldManifest ;
2022-04-08 16:27:11 -07:00
if ( ! error && result . length ) {
2021-08-20 09:19:44 -07:00
installationState = exports . ISTATE _PENDING _RESTORE ;
2022-04-08 16:27:11 -07:00
restoreConfig = { remotePath : result [ 0 ] . remotePath , backupFormat : result [ 0 ] . format } ;
2021-08-20 09:19:44 -07:00
oldManifest = app . manifest ;
} else {
installationState = exports . ISTATE _PENDING _INSTALL ;
restoreConfig = null ;
oldManifest = null ;
2021-07-14 11:07:19 -07:00
}
2017-11-17 22:29:13 -08:00
2021-08-20 09:19:44 -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-05-24 10:33:10 -07:00
2023-08-21 18:18:03 +05:30
debug ( ` restoreApps: marking ${ app . fqdn } for restore using restore config ${ JSON . stringify ( restoreConfig ) } ` ) ;
2019-10-24 10:39:47 -07:00
2021-11-17 10:38:02 -08:00
const [ addTaskError , taskId ] = await safe ( addTask ( app . id , installationState , task , auditSource ) ) ;
2023-08-21 18:18:03 +05:30
if ( addTaskError ) debug ( ` restoreApps: error marking ${ app . fqdn } for restore: ${ JSON . stringify ( addTaskError ) } ` ) ;
else debug ( ` restoreApps: marked ${ app . id } for restore with taskId ${ taskId } ` ) ;
2021-08-20 09:19:44 -07:00
}
}
2016-05-24 10:33:10 -07:00
2023-08-21 18:18:03 +05:30
async function configureApps ( apps , options , auditSource ) {
2023-07-21 17:10:25 +02:00
assert ( Array . isArray ( apps ) ) ;
2023-08-21 18:18:03 +05:30
assert . strictEqual ( typeof options , 'object' ) ;
2021-11-17 10:33:28 -08:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2021-08-20 09:19:44 -07:00
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 _CONFIGURE ) ; // safeguard against tasks being created non-stop if we crash on startup
2016-05-24 10:33:10 -07:00
2023-08-21 18:18:03 +05:30
const scheduleNow = ! ! options . scheduleNow ;
2021-08-20 09:19:44 -07:00
for ( const app of apps ) {
2023-08-21 18:18:03 +05:30
debug ( ` configureApps: marking ${ app . fqdn } for reconfigure (scheduleNow: ${ scheduleNow } ) ` ) ;
2016-06-16 06:38:47 -07:00
2021-08-20 09:19:44 -07:00
const task = {
args : { } ,
values : { } ,
2023-08-21 18:18:03 +05:30
scheduleNow ,
2021-08-20 09:19:44 -07:00
requireNullTaskId : false // ignore existing stale taskId
} ;
2019-09-24 20:29:01 -07:00
2021-11-17 10:38:02 -08:00
const [ addTaskError , taskId ] = await safe ( addTask ( app . id , exports . ISTATE _PENDING _CONFIGURE , task , auditSource ) ) ;
2023-08-21 18:18:03 +05:30
if ( addTaskError ) debug ( ` configureApps: error marking ${ app . fqdn } for configure: ${ JSON . stringify ( addTaskError ) } ` ) ;
else debug ( ` configureApps: marked ${ app . id } for re-configure with taskId ${ taskId } ` ) ;
2021-08-20 09:19:44 -07:00
}
2016-05-24 10:33:10 -07:00
}
2017-08-18 20:45:52 -07:00
2021-11-17 10:33:28 -08:00
async function restartAppsUsingAddons ( changedAddons , auditSource ) {
2020-05-22 16:43:16 -07:00
assert ( Array . isArray ( changedAddons ) ) ;
2021-11-17 10:33:28 -08:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2020-05-22 16:43:16 -07:00
2021-08-20 09:19:44 -07:00
let apps = await list ( ) ;
2023-07-14 08:34:02 +05:30
// TODO: This ends up restarting apps that have optional redis
2021-08-20 09:19:44 -07:00
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
apps = apps . filter ( app => app . runState !== exports . RSTATE _STOPPED ) ; // don't start stopped apps
2020-05-22 16:43:16 -07:00
2021-08-20 09:19:44 -07:00
for ( const app of apps ) {
debug ( ` restartAppsUsingAddons: marking ${ app . fqdn } for restart ` ) ;
2020-05-22 16:43:16 -07:00
2021-08-20 09:19:44 -07:00
const task = {
args : { } ,
values : { runState : exports . RSTATE _RUNNING }
} ;
2020-08-31 17:53:29 -07:00
2021-08-20 09:19:44 -07:00
// stop apps before updating the databases because postgres will "lock" them preventing import
2021-08-25 19:41:46 -07:00
const [ stopError ] = await safe ( docker . stopContainers ( app . id ) ) ;
2021-08-20 09:19:44 -07:00
if ( stopError ) debug ( ` restartAppsUsingAddons: error stopping ${ app . fqdn } ` , stopError ) ;
2020-08-31 17:53:29 -07:00
2021-11-17 10:38:02 -08:00
const [ addTaskError , taskId ] = await safe ( addTask ( app . id , exports . ISTATE _PENDING _RESTART , task , auditSource ) ) ;
2021-08-20 09:19:44 -07:00
if ( addTaskError ) debug ( ` restartAppsUsingAddons: error marking ${ app . fqdn } for restart: ${ JSON . stringify ( addTaskError ) } ` ) ;
else debug ( ` restartAppsUsingAddons: marked ${ app . id } for restart with taskId ${ taskId } ` ) ;
}
2020-05-22 16:43:16 -07:00
}
2019-09-24 20:29:01 -07:00
// auto-restart app tasks after a crash
2021-11-17 10:33:28 -08:00
async function schedulePendingTasks ( auditSource ) {
assert . strictEqual ( typeof auditSource , 'object' ) ;
2019-09-24 20:29:01 -07:00
debug ( 'schedulePendingTasks: scheduling app tasks' ) ;
2019-09-24 10:28:50 -07:00
2021-08-25 19:41:46 -07:00
const result = await list ( ) ;
2019-09-24 10:28:50 -07:00
2021-09-07 09:57:49 -07:00
for ( const app of result ) {
if ( ! app . taskId ) continue ; // if not in any pending state, do nothing
2019-09-24 10:28:50 -07:00
2021-08-20 09:19:44 -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
2021-11-17 10:42:04 -08:00
await safe ( scheduleTask ( app . id , app . installationState , app . taskId , auditSource ) , { debug } ) ; // ignore error
2021-09-07 09:57:49 -07:00
}
2019-09-24 10:28:50 -07:00
}
2021-09-21 19:45:29 -07:00
async function listEventlog ( app , page , perPage ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof page , 'number' ) ;
assert . strictEqual ( typeof perPage , 'number' ) ;
const actions = [ ] ;
const search = app . id ;
return await eventlog . listPaged ( actions , search , page , perPage ) ;
}
2021-10-21 15:25:15 -07:00
async function drainStream ( stream ) {
return new Promise ( ( resolve , reject ) => {
let data = '' ;
stream . setEncoding ( 'utf8' ) ;
stream . on ( 'error' , ( error ) => reject ( new BoxError . FS _ERROR , error . message ) ) ;
stream . on ( 'data' , function ( d ) { data += d ; } ) ;
stream . on ( 'end' , function ( ) {
resolve ( data ) ;
} ) ;
} ) ;
}
async function downloadFile ( app , filePath ) {
2020-03-29 17:11:10 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2017-08-18 20:45:52 -07:00
assert . strictEqual ( typeof filePath , 'string' ) ;
2022-05-16 11:37:25 -07:00
const statExecId = await createExec ( app , { cmd : [ 'stat' , '--printf=%F-%s' , filePath ] , tty : true } ) ;
const statStream = await startExec ( app , statExecId , { tty : true } ) ;
2021-10-21 15:25:15 -07:00
const data = await drainStream ( statStream ) ;
2017-08-18 20:45:52 -07:00
2021-10-21 15:25:15 -07:00
const parts = data . split ( '-' ) ;
if ( parts . length !== 2 ) throw new BoxError ( BoxError . NOT _FOUND , 'file does not exist' ) ;
2017-08-19 10:48:12 +02:00
2024-05-13 08:43:28 +02:00
const type = parts [ 0 ] ;
let filename , cmd , size ;
2017-08-19 10:48:12 +02:00
2021-10-21 15:25:15 -07:00
if ( type === 'regular file' ) {
cmd = [ 'cat' , filePath ] ;
size = parseInt ( parts [ 1 ] , 10 ) ;
filename = path . basename ( filePath ) ;
if ( isNaN ( size ) ) throw new BoxError ( BoxError . NOT _FOUND , 'file does not exist' ) ;
} else if ( type === 'directory' ) {
cmd = [ 'tar' , 'zcf' , '-' , '-C' , filePath , '.' ] ;
filename = path . basename ( filePath ) + '.tar.gz' ;
size = 0 ; // unknown
} else {
throw new BoxError ( BoxError . NOT _FOUND , 'only files or dirs can be downloaded' ) ;
}
2017-08-20 23:39:49 -07:00
2022-05-16 11:37:25 -07:00
const execId = await createExec ( app , { cmd , tty : false } ) ;
const inputStream = await startExec ( app , execId , { tty : false } ) ;
2017-08-20 23:39:49 -07:00
2021-10-21 15:25:15 -07:00
// transforms the docker stream into a normal stream
const stdoutStream = new TransformStream ( {
transform : function ( chunk , ignoredEncoding , callback ) {
this . _buffer = this . _buffer ? Buffer . concat ( [ this . _buffer , chunk ] ) : chunk ;
2017-08-20 23:39:49 -07:00
2021-10-21 15:25:15 -07:00
for ( ; ; ) {
if ( this . _buffer . length < 8 ) break ; // header is 8 bytes
2017-08-20 23:39:49 -07:00
2022-02-07 16:09:43 -08:00
const type = this . _buffer . readUInt8 ( 0 ) ;
const len = this . _buffer . readUInt32BE ( 4 ) ;
2017-08-20 23:39:49 -07:00
2021-10-21 15:25:15 -07:00
if ( this . _buffer . length < ( 8 + len ) ) break ; // not enough
2017-08-20 23:39:49 -07:00
2022-02-07 16:09:43 -08:00
const payload = this . _buffer . slice ( 8 , 8 + len ) ;
2017-08-20 23:39:49 -07:00
2021-10-21 15:25:15 -07:00
this . _buffer = this . _buffer . slice ( 8 + len ) ; // consumed
2017-08-20 23:39:49 -07:00
2021-10-21 15:25:15 -07:00
if ( type === 1 ) this . push ( payload ) ;
}
2017-08-20 23:39:49 -07:00
2021-10-21 15:25:15 -07:00
callback ( ) ;
}
2017-08-18 20:45:52 -07:00
} ) ;
2021-10-21 15:25:15 -07:00
inputStream . pipe ( stdoutStream ) ;
return { stream : stdoutStream , filename , size } ;
2017-08-18 20:45:52 -07:00
}
2024-07-22 22:27:41 +02:00
async function uploadFile ( app , sourceFilePath , destFilePath ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof sourceFilePath , 'string' ) ;
assert . strictEqual ( typeof destFilePath , 'string' ) ;
// 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
2024-10-16 10:25:07 +02:00
const escapedDestFilePath = await shell . bash ( ` printf %q ' ${ destFilePath . replace ( /'/g , '\'\\\'\'' ) } ' ` , { encoding : 'utf8' } ) ;
2024-07-22 22:27:41 +02:00
debug ( ` uploadFile: ${ sourceFilePath } -> ${ escapedDestFilePath } ` ) ;
const execId = await createExec ( app , { cmd : [ 'bash' , '-c' , ` cat - > ${ escapedDestFilePath } ` ] , tty : false } ) ;
const destStream = await startExec ( app , execId , { tty : false } ) ;
return new Promise ( ( resolve , reject ) => {
const done = once ( error => reject ( new BoxError ( BoxError . FS _ERROR , error . message ) ) ) ;
const sourceStream = fs . createReadStream ( sourceFilePath ) ;
sourceStream . on ( 'error' , done ) ;
destStream . on ( 'error' , done ) ;
destStream . on ( 'finish' , resolve ) ;
sourceStream . pipe ( destStream ) ;
} ) ;
}
2024-02-10 11:53:25 +01:00
async function writeConfig ( app ) {
2021-05-25 21:31:48 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2024-02-10 10:40:56 +01:00
if ( ! safe . fs . writeFileSync ( path . join ( paths . APPS _DATA _DIR , app . id + '/config.json' ) , JSON . stringify ( app , null , 4 ) ) ) {
2021-08-20 09:19:44 -07:00
throw new BoxError ( BoxError . FS _ERROR , 'Error creating config.json: ' + safe . error . message ) ;
2021-05-25 21:31:48 -07:00
}
2021-08-20 09:19:44 -07:00
const [ error , icons ] = await safe ( getIcons ( app . id ) ) ;
if ( ! error && icons . icon ) safe . fs . writeFileSync ( path . join ( paths . APPS _DATA _DIR , app . id + '/icon.png' ) , icons . icon ) ;
2021-05-25 21:31:48 -07:00
}
2021-05-26 09:48:34 -07:00
2024-02-10 11:53:25 +01:00
async function loadConfig ( app ) {
2021-05-26 09:48:34 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
const appConfig = safe . JSON . parse ( safe . fs . readFileSync ( path . join ( paths . APPS _DATA _DIR , app . id + '/config.json' ) ) ) ;
let data = { } ;
if ( appConfig ) {
2025-02-13 14:03:25 +01:00
data = _ . pick ( appConfig , [ 'memoryLimit' , 'cpuQuota' , 'enableBackup' , 'reverseProxyConfig' , 'env' , 'servicesConfig' , 'label' , 'tags' , 'enableAutomaticUpdate' ] ) ;
2021-05-26 09:48:34 -07:00
}
const icon = safe . fs . readFileSync ( path . join ( paths . APPS _DATA _DIR , app . id + '/icon.png' ) ) ;
if ( icon ) data . icon = icon ;
2021-08-20 09:19:44 -07:00
await update ( app . id , data ) ;
2021-05-26 09:48:34 -07:00
}