Files
cloudron-box/src/apps.js
T

2923 lines
119 KiB
JavaScript
Raw Normal View History

'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,
2020-10-27 17:11:50 -07:00
removeInternalFields,
removeRestrictedFields,
2021-08-20 09:19:44 -07:00
// database crud
add,
update,
setHealth,
del,
2021-09-17 09:52:18 -07:00
delPortBinding,
2021-08-20 09:19:44 -07:00
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,
uninstall,
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,
setMemoryLimit,
setCpuShares,
2020-10-28 19:42:48 -07:00
setMounts,
2020-10-27 17:11:50 -07:00
setAutomaticBackup,
setAutomaticUpdate,
setReverseProxyConfig,
setCertificate,
setDebugMode,
setEnvironment,
setMailbox,
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,
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
canAutoupdateApp,
autoupdateApps,
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,
getLimits,
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,
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
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
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',
ISTATE_PENDING_RESIZE: 'pending_resize',
ISTATE_PENDING_DEBUG: 'pending_debug',
2019-08-30 13:12:49 -07:00
ISTATE_PENDING_UNINSTALL: 'pending_uninstall', // uninstallation
ISTATE_PENDING_RESTORE: 'pending_restore', // restore to previous backup or on upgrade
2021-05-26 09:27:15 -07:00
ISTATE_PENDING_IMPORT: 'pending_import', // import from external backup
2019-08-30 13:12:49 -07:00
ISTATE_PENDING_UPDATE: 'pending_update', // update from installed state preserving data
ISTATE_PENDING_BACKUP: 'pending_backup', // backup the app. this is state because it blocks other operations
2019-09-22 00:20:12 -07:00
ISTATE_PENDING_START: 'pending_start',
ISTATE_PENDING_STOP: 'pending_stop',
2019-12-20 10:29:29 -08:00
ISTATE_PENDING_RESTART: 'pending_restart',
2019-08-30 13:12:49 -07:00
ISTATE_ERROR: 'error', // error executing last pending_* command
ISTATE_INSTALLED: 'installed', // app is installed
// run states
RSTATE_RUNNING: 'running',
RSTATE_STOPPED: 'stopped', // app stopped by us
// health states (keep in sync in UI)
HEALTH_HEALTHY: 'healthy',
HEALTH_UNHEALTHY: 'unhealthy',
HEALTH_ERROR: 'error',
HEALTH_DEAD: 'dead',
// exported for testing
_checkForPortBindingConflict: checkForPortBindingConflict,
2015-10-15 12:26:48 +02:00
_validatePortBindings: validatePortBindings,
_validateAccessRestriction: validateAccessRestriction,
2022-11-23 14:36:57 +01:00
_validateUpstreamUri: validateUpstreamUri,
_validateLocations: validateLocations,
2019-08-20 11:45:00 -07:00
_translatePortBindings: translatePortBindings,
2021-09-27 14:21:42 -07:00
_parseCrontab: parseCrontab,
2021-08-20 09:19:44 -07:00
_clear: clear
};
2021-08-20 09:19:44 -07:00
const appstore = require('./appstore.js'),
2019-08-28 15:00:55 -07:00
appTaskManager = require('./apptaskmanager.js'),
assert = require('assert'),
backups = require('./backups.js'),
2019-09-19 23:13:04 -07:00
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
2021-09-27 14:21:42 -07:00
CronJob = require('cron').CronJob,
2023-08-11 19:41:05 +05:30
dashboard = require('./dashboard.js'),
2021-05-07 21:37:23 -07:00
database = require('./database.js'),
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'),
fs = require('fs'),
2023-08-17 16:05:19 +05:30
Location = require('./location.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'),
manifestFormat = require('cloudron-manifestformat'),
notifications = require('./notifications.js'),
2022-04-15 19:01:35 -05:00
once = require('./once.js'),
2017-11-07 09:09:30 -08:00
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
PassThrough = require('stream').PassThrough,
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
semver = require('semver'),
services = require('./services.js'),
2022-06-03 10:56:50 -07:00
shell = require('./shell.js'),
2022-11-03 22:13:57 +01:00
storage = require('./storage.js'),
system = require('./system.js'),
2019-08-26 15:55:57 -07:00
tasks = require('./tasks.js'),
2022-11-03 22:13:57 +01:00
tgz = require('./backupformat/tgz.js'),
2017-08-20 23:39:49 -07:00
TransformStream = require('stream').Transform,
users = require('./users.js'),
util = require('util'),
2017-08-13 17:44:31 -07:00
uuid = require('uuid'),
2018-04-29 10:47:34 -07:00
validator = require('validator'),
2022-06-01 22:44:52 -07:00
volumes = require('./volumes.js'),
2018-04-29 10:47:34 -07:00
_ = require('underscore');
2021-08-20 09:19:44 -07:00
const APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.errorJson', 'apps.runState',
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.accessRestrictionJson', 'apps.memoryLimit', 'apps.cpuShares',
2021-09-21 10:11:27 -07:00
'apps.label', 'apps.tagsJson', 'apps.taskId', 'apps.reverseProxyConfigJson', 'apps.servicesConfigJson', 'apps.operatorsJson',
2021-09-27 14:21:42 -07:00
'apps.sso', 'apps.debugModeJson', 'apps.enableBackup', 'apps.proxyAuth', 'apps.containerIp', 'apps.crontab',
2022-06-08 11:21:09 +02:00
'apps.creationTime', 'apps.updateTime', 'apps.enableAutomaticUpdate', 'apps.upstreamUri',
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
// const PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId' ].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');
2018-08-12 22:37:36 -07:00
function validatePortBindings(portBindings, manifest) {
2017-01-29 13:01:09 -08:00
assert.strictEqual(typeof portBindings, 'object');
2018-08-12 22:37:36 -07:00
assert.strictEqual(typeof manifest, 'object');
2017-01-29 13:01:09 -08:00
2018-06-04 21:24:14 +02:00
// keep the public ports in sync with firewall rules in setup/start/cloudron-firewall.sh
// 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 */
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 */
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 */
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) */
constants.LDAP_PORT,
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 */
];
2020-03-30 10:01:52 +02:00
const RESERVED_PORT_RANGES = [
[50000, 51000] /* turn udp ports */
];
2021-02-17 13:11:00 -08:00
const ALLOWED_PORTS = [
53, // dns 53 is special and adblocker apps can use them
853 // dns over tls
];
if (!portBindings) return null;
2024-02-27 13:19:19 +01:00
const tcpPorts = manifest.tcpPorts || {};
const udpPorts = manifest.udpPorts || {};
for (const portName in portBindings) {
if (!/^[A-Z0-9_]+$/.test(portName)) return new BoxError(BoxError.BAD_FIELD, `${portName} is not a valid environment variable in portBindings`);
2024-02-27 13:19:19 +01:00
const hostPort = portBindings[portName];
2022-02-07 13:19:59 -08:00
if (!Number.isInteger(hostPort)) return new BoxError(BoxError.BAD_FIELD, `${hostPort} is not an integer in ${portName} portBindings`);
if (RESERVED_PORTS.indexOf(hostPort) !== -1) return new BoxError(BoxError.BAD_FIELD, `Port ${hostPort} for ${portName} is reserved in portBindings`);
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 portBindings`);
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 portBindings`);
// it is OK if there is no 1-1 mapping between values in manifest.tcpPorts and portBindings. missing values implies the service is disabled
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`);
}
return null;
}
2018-08-13 08:33:09 -07:00
function translatePortBindings(portBindings, manifest) {
assert.strictEqual(typeof portBindings, 'object');
assert.strictEqual(typeof manifest, 'object');
if (!portBindings) return null;
let result = {};
2018-08-13 08:33:09 -07:00
const tcpPorts = manifest.tcpPorts || { };
for (let portName in portBindings) {
2018-08-13 08:33:09 -07:00
const portType = portName in tcpPorts ? exports.PORT_TYPE_TCP : exports.PORT_TYPE_UDP;
const portCount = portBindings[portName].portCount || (portName in tcpPorts ? manifest.tcpPorts[portName].portCount : manifest.udpPorts[portName].portCount);
result[portName] = { hostPort: portBindings[portName], type: portType, portCount: portCount || 1 };
}
2024-02-25 14:33:57 +01:00
return result;
}
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 = {
'@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 {
new CronJob('00 ' + schedule, function() {}); // second is disallowed
} catch (ex) {
throw new BoxError(BoxError.BAD_FIELD, `Invalid cron pattern at line ${i+1}`);
}
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) {
assert.strictEqual(typeof accessRestriction, 'object');
2015-10-15 12:26:48 +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');
2022-02-07 16:09:43 -08:00
const min = manifest.memoryLimit || constants.DEFAULT_MEMORY_LIMIT;
const max = os.totalmem() * 2; // this will overallocate since we don't allocate equal swap always (#466)
2016-02-11 17:39:15 +01:00
// allow 0, which indicates that it is not set, the one from the manifest will be choosen but we don't commit any user value
// this is needed so an app update can change the value in the manifest, and if not set by the user, the new value should be used
if (memoryLimit === 0) return null;
// a special value that indicates unlimited memory
if (memoryLimit === -1) return null;
2019-10-24 10:39:47 -07:00
if (memoryLimit < min) return new BoxError(BoxError.BAD_FIELD, 'memoryLimit too small');
if (memoryLimit > max) return new BoxError(BoxError.BAD_FIELD, 'memoryLimit too large');
2016-02-11 17:39:15 +01:00
return null;
}
2020-01-28 21:30:35 -08:00
function validateCpuShares(cpuShares) {
assert.strictEqual(typeof cpuShares, 'number');
2020-01-28 22:38:54 -08:00
if (cpuShares < 2 || cpuShares > 1024) return new BoxError(BoxError.BAD_FIELD, 'cpuShares has to be between 2 and 1024');
2020-01-28 21:30:35 -08:00
return null;
}
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' );
return null;
}
2017-07-14 12:19:27 -05:00
function validateRobotsTxt(robotsTxt) {
if (robotsTxt === null) return null;
// 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');
// 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;
}
2018-10-18 11:19:32 -07:00
function validateEnv(env) {
for (let 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);
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');
2022-06-09 17:49:33 -07:00
const [error] = await safe(shell.promises.sudo('checkStorage', [ 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;
}
function getDuplicateErrorDetails(errorMessage, locations, portBindings) {
assert.strictEqual(typeof errorMessage, 'string');
2019-09-27 10:25:26 -07:00
assert(Array.isArray(locations));
assert.strictEqual(typeof portBindings, 'object');
2022-02-07 16:09:43 -08:00
const match = errorMessage.match(/ER_DUP_ENTRY: Duplicate entry '(.*)' for key '(.*)'/);
if (!match) {
debug('Unexpected SQL error message.', errorMessage);
2019-10-24 18:32:33 -07:00
return new BoxError(BoxError.DATABASE_ERROR, errorMessage);
}
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];
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
}
// check if any of the port bindings conflict
2022-02-07 16:09:43 -08:00
for (const portName in portBindings) {
if (portBindings[portName] === parseInt(match[1])) return new BoxError(BoxError.ALREADY_EXISTS, `Port ${match[1]} is in use`);
}
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`);
}
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
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
}
2022-07-14 11:57:04 +05:30
function removeCertificateKeys(app) {
if (app.certificate) delete app.certificate.key;
app.secondaryDomains.forEach(sd => { if (sd.certificate) delete sd.certificate.key; });
app.aliasDomains.forEach(ad => { if (ad.certificate) delete ad.certificate.key; });
app.redirectDomains.forEach(rd => { if (rd.certificate) delete rd.certificate.key; });
}
2018-06-25 16:40:16 -07:00
function removeInternalFields(app) {
2022-07-14 11:57:04 +05:30
const result = _.pick(app,
2019-08-30 11:18:42 -07:00
'id', 'appStoreId', 'installationState', 'error', 'runState', 'health', 'taskId',
2022-07-14 11:57:04 +05:30
'subdomain', 'domain', 'fqdn', 'certificate', 'crontab', 'upstreamUri',
2021-09-21 10:11:27 -07:00
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'cpuShares', 'operators',
2019-10-14 15:57:41 -07:00
'sso', 'debugMode', 'reverseProxyConfig', 'enableBackup', 'creationTime', 'updateTime', 'ts', 'tags',
2022-07-14 11:57:04 +05:30
'label', 'secondaryDomains', 'redirectDomains', 'aliasDomains', 'env', 'enableAutomaticUpdate',
2023-07-13 16:37:33 +05:30
'storageVolumeId', 'storageVolumePrefix', 'mounts', 'enableTurn', 'enableRedis',
2022-05-31 17:53:09 -07:00
'enableMailbox', 'mailboxDisplayName', 'mailboxName', 'mailboxDomain', 'enableInbox', 'inboxName', 'inboxDomain');
2022-07-14 11:57:04 +05:30
removeCertificateKeys(result);
return result;
2018-04-29 10:47:34 -07:00
}
2019-02-11 14:37:49 -08:00
// non-admins can only see these
2018-06-25 16:45:15 -07:00
function removeRestrictedFields(app) {
2022-07-14 11:57:04 +05:30
const result = _.pick(app,
2023-03-11 16:16:07 +01:00
'id', 'appStoreId', 'installationState', 'error', 'runState', 'health', 'taskId', 'accessRestriction',
2022-07-14 11:57:04 +05:30
'secondaryDomains', 'redirectDomains', 'aliasDomains', 'sso', 'subdomain', 'domain', 'fqdn', 'certificate',
'manifest', 'portBindings', 'iconUrl', 'creationTime', 'ts', 'tags', 'label', 'enableBackup', 'upstreamUri');
removeCertificateKeys(result);
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
}
async function getLimits(app) {
assert.strictEqual(typeof app, 'object');
return {
memory: await system.getMemory()
};
}
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;
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');
result.portBindings = { };
let hostPorts = result.hostPorts === null ? [ ] : result.hostPorts.split(',');
let environmentVariables = result.environmentVariables === null ? [ ] : result.environmentVariables.split(',');
let portTypes = result.portTypes === null ? [ ] : result.portTypes.split(',');
delete result.hostPorts;
delete result.environmentVariables;
delete result.portTypes;
for (let i = 0; i < environmentVariables.length; i++) {
result.portBindings[environmentVariables[i]] = { hostPort: parseInt(hostPorts[i], 10), type: portTypes[i] };
}
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;
let volumeReadOnlys = JSON.parse(result.volumeReadOnlys);
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;
}
function attachProperties(app, domainObjectMap) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof domainObjectMap, 'object');
2019-03-06 11:12:39 -08:00
let result = {};
for (let portName in app.portBindings) {
result[portName] = app.portBindings[portName].hostPort;
}
app.portBindings = result;
2021-04-30 13:18:15 -07:00
app.iconUrl = app.hasIcon || app.hasAppStoreIcon ? `/api/v1/apps/${app.id}/icon` : null;
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); });
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
2021-09-21 10:11:27 -07:00
if (app.operators.users.some(function (e) { return e === user.id; })) return true;
if (!app.operators.groups) return isAdmin(user);
if (app.operators.groups.some(function (gid) { return Array.isArray(user.groupIds) && user.groupIds.indexOf(gid) !== -1; })) 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;
if (app.accessRestriction.users.some(function (e) { return e === user.id; })) return true;
if (!app.accessRestriction.groups) return isOperator(app, user);
2021-08-20 09:19:44 -07:00
if (app.accessRestriction.groups.some(function (gid) { return Array.isArray(user.groupIds) && user.groupIds.indexOf(gid) !== -1; })) 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) {
if (isAdmin(user)) return 'admin';
if (isOperator(app, user)) return 'operator';
return canAccess(app, user) ? 'user' : null;
2015-10-15 15:06:34 +02:00
}
async function checkForPortBindingConflict(portBindings, id = '') {
assert.strictEqual(typeof portBindings, 'object');
assert.strictEqual(typeof id, 'string');
let existingPortBindings;
if (id) existingPortBindings = await database.query('SELECT * FROM appPortBindings WHERE appId != ?', [ id ]);
else existingPortBindings = await database.query('SELECT * FROM appPortBindings', []);
if (existingPortBindings.length === 0) return;
const tcpPorts = existingPortBindings.filter((p) => p.type === 'tcp');
const udpPorts = existingPortBindings.filter((p) => p.type === 'udp');
for (let portName in portBindings) {
const p = portBindings[portName];
const testPorts = p.type === 'tcp' ? tcpPorts : udpPorts;
const found = testPorts.find((e) => {
// if one is true we dont have a conflict
// a1 <----> a2 b1 <-------> b2
// b1 <------> b2 a1 <-----> a2
const a2 = (e.hostPort + e.count - 1);
const b1 = p.hostPort;
const b2 = (p.hostPort + p.portCount -1);
const a1 = e.hostPort;
return !((a2 < b1) || (b2 < a1));
});
if (found) throw new BoxError(BoxError.CONFLICT, `Conflicting port ${p.hostPort}`);
}
}
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');
assert.strictEqual(typeof portBindings, 'object');
assert(data && typeof data === 'object');
2021-08-20 09:19:44 -07:00
portBindings = portBindings || { };
2021-10-01 12:09:13 -07:00
const manifestJson = JSON.stringify(manifest),
accessRestriction = data.accessRestriction || null,
accessRestrictionJson = JSON.stringify(accessRestriction),
memoryLimit = data.memoryLimit || 0,
cpuShares = data.cpuShares || 512,
installationState = data.installationState,
runState = data.runState,
sso = 'sso' in data ? data.sso : null,
debugModeJson = data.debugMode ? JSON.stringify(data.debugMode) : null,
env = data.env || {},
label = data.label || null,
tagsJson = data.tags ? JSON.stringify(data.tags) : null,
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,
upstreamUri = data.upstreamUri || '',
2023-07-13 15:06:07 +05:30
enableTurn = 'enableTurn' in data ? data.enableTurn : true,
2023-07-13 16:37:33 +05:30
enableRedis = 'enableRedis' in data ? data.enableRedis : true,
2021-10-01 12:09:13 -07:00
icon = data.icon || null;
2021-08-20 09:19:44 -07:00
await checkForPortBindingConflict(portBindings);
2021-10-01 12:09:13 -07:00
const queries = [];
2021-08-20 09:19:44 -07:00
queries.push({
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, cpuShares, '
2021-10-01 12:09:13 -07:00
+ 'sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson, servicesConfigJson, icon, '
2023-07-13 16:37:33 +05:30
+ 'enableMailbox, mailboxDisplayName, upstreamUri, enableTurn, enableRedis) '
+ ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
2021-08-20 09:19:44 -07:00
args: [ id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, cpuShares,
2022-05-31 17:53:09 -07:00
sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson, servicesConfigJson, icon,
2023-07-13 16:37:33 +05:30
enableMailbox, mailboxDisplayName, upstreamUri, enableTurn, enableRedis ]
2021-08-20 09:19:44 -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-02-25 14:33:57 +01:00
args: [ env, portBindings[env].hostPort, portBindings[env].type, id, portBindings[env].portCount ]
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));
assert(!('env' in app) || typeof app.env === 'object');
2021-08-20 09:19:44 -07:00
const queries = [ ];
if ('portBindings' in app) {
2022-02-07 16:09:43 -08:00
const portBindings = app.portBindings || { };
await checkForPortBindingConflict(portBindings, id);
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-02-25 14:33:57 +01:00
const values = [ portBindings[env].hostPort, portBindings[env].type, env, id, portBindings[env].portCount ];
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 ] });
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) {
if (p === 'manifest' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode' || p === 'error' || p === 'reverseProxyConfig' || p === 'servicesConfig' || p === 'operators') {
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');
}
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
}
async function delPortBinding(hostPort, type) {
assert.strictEqual(typeof hostPort, 'number');
assert.strictEqual(typeof type, 'string');
const result = await database.query('DELETE FROM appPortBindings WHERE hostPort=? AND type=?', [ hostPort, type ]);
if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'App not found');
2021-08-20 09:19:44 -07:00
}
2019-03-19 16:23:03 -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
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 FROM apps LEFT JOIN appPortBindings ON apps.id = appPortBindings.appId GROUP BY apps.id';
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';
2022-07-14 11:57:04 +05:30
const APPS_QUERY = `SELECT ${APPS_FIELDS_PREFIXED}, hostPorts, environmentVariables, portTypes, 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`;
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();
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];
}
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';
}
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');
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;
switch (installationState) {
case exports.ISTATE_PENDING_DATA_DIR_MIGRATION:
if (success) await safe(services.rebuildService('sftp', auditSource), { debug });
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;
await eventlog.add(eventlog.ACTION_APP_UPDATE_FINISH, auditSource, { app, toManifest, fromManifest, success, errorMessage });
break;
}
case exports.ISTATE_PENDING_BACKUP: {
const backup = await backups.get(task.result);
await eventlog.add(eventlog.ACTION_APP_BACKUP_FINISH, auditSource, { app, success, errorMessage, remotePath: backup?.remotePath, backupId: task.result });
2021-09-30 10:45:25 -07:00
break;
}
}
}
async function scheduleTask(appId, installationState, taskId, auditSource) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof installationState, 'string');
2019-09-24 10:28:50 -07:00
assert.strictEqual(typeof taskId, 'string');
assert.strictEqual(typeof auditSource, 'object');
2023-08-04 11:24:28 +05:30
const backupConfig = await backups.getConfig();
2021-08-19 13:24:38 -07:00
2021-09-07 09:57:49 -07:00
let memoryLimit = 400;
if (installationState === exports.ISTATE_PENDING_BACKUP || installationState === exports.ISTATE_PENDING_CLONE || installationState === exports.ISTATE_PENDING_RESTORE
|| installationState === exports.ISTATE_PENDING_IMPORT || installationState === exports.ISTATE_PENDING_UPDATE) {
2023-07-13 11:50:57 +05:30
memoryLimit = backupConfig.limits?.memoryLimit ? Math.max(backupConfig.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-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) {
debug(`scheduleTask: task ${taskId} of ${appId} completed`);
if (error && (error.code === tasks.ECRASHED || error.code === tasks.ESTOPPED)) { // if task crashed, update the error
debug(`Apptask crashed/stopped: ${error.message}`);
let boxError = new BoxError(BoxError.TASK_ERROR, error.message);
boxError.details.crashed = error.code === tasks.ECRASHED;
boxError.details.stopped = error.code === tasks.ESTOPPED;
// see also apptask makeTaskError
boxError.details.taskId = taskId;
boxError.details.installationState = installationState;
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
await safe(update(appId, { taskId: null }), { debug });
}
await safe(onTaskFinished(error, appId, installationState, taskId, auditSource), { debug }); // ignore error
});
}
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;
2019-12-06 11:29:33 -08:00
// TODO: match the SQL logic to match checkAppState. this means checking the error.installationState and installationState. Unfortunately, former is JSON right now
const requiredState = null; // 'requiredState' in task ? task.requiredState : exports.ISTATE_INSTALLED;
2019-09-24 20:29:01 -07:00
const scheduleNow = 'scheduleNow' in task ? task.scheduleNow : true;
const requireNullTaskId = 'requireNullTaskId' in task ? task.requireNullTaskId : true;
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
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
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
2019-11-23 18:06:31 -08:00
if (app.error.installationState === state) return null;
// allow uninstall from any state
2021-05-26 09:27:15 -07:00
if (state !== exports.ISTATE_PENDING_UNINSTALL && state !== exports.ISTATE_PENDING_RESTORE && state !== exports.ISTATE_PENDING_IMPORT) return new BoxError(BoxError.BAD_STATE, 'Not allowed in error state');
2019-09-23 10:35:42 -07:00
}
2019-09-21 19:45:55 -07:00
if (app.runState === exports.RSTATE_STOPPED) {
// can't backup or restore since app addons are down. can't update because migration scripts won't run
2021-05-26 09:27:15 -07:00
if (state === exports.ISTATE_PENDING_UPDATE || state === exports.ISTATE_PENDING_BACKUP || state === exports.ISTATE_PENDING_RESTORE || state === exports.ISTATE_PENDING_IMPORT) return new BoxError(BoxError.BAD_STATE, 'Not allowed in stopped state');
}
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
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
}
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`);
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
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,
memoryLimit = data.memoryLimit || 0,
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 || [],
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 || [],
overwriteDns = 'overwriteDns' in data ? data.overwriteDns : false,
skipDnsSetup = 'skipDnsSetup' in data ? data.skipDnsSetup : false,
2023-07-13 15:06:07 +05:30
enableTurn = 'enableTurn' in data ? data.enableTurn : true,
2023-07-13 16:37:33 +05:30
enableRedis = 'enableRedis' in data ? data.enableRedis : true,
appStoreId = data.appStoreId,
2022-06-10 11:23:58 -07:00
upstreamUri = data.upstreamUri || '',
manifest = data.manifest;
2016-06-03 23:22:38 -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}`);
2024-03-30 18:25:33 +01:00
error = await checkManifest(manifest);
2021-08-20 09:19:44 -07:00
if (error) throw error;
2024-02-27 13:19:19 +01:00
error = validatePortBindings(data.portBindings || null, manifest);
2021-08-20 09:19:44 -07:00
if (error) throw error;
2024-02-27 13:19:19 +01:00
const portBindings = translatePortBindings(data.portBindings || null, manifest);
error = validateAccessRestriction(accessRestriction);
2021-08-20 09:19:44 -07:00
if (error) throw error;
error = validateMemoryLimit(manifest, memoryLimit);
2021-08-20 09:19:44 -07:00
if (error) throw error;
error = validateDebugMode(debugMode);
2021-08-20 09:19:44 -07:00
if (error) throw error;
error = validateLabel(label);
2021-08-20 09:19:44 -07:00
if (error) throw error;
2022-11-23 12:53:21 +01:00
if ('upstreamUri' in data) error = validateUpstreamUri(upstreamUri);
if (error) throw error;
error = validateTags(tags);
2021-08-20 09:19:44 -07:00
if (error) throw error;
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');
// if sso was unspecified, enable it by default if possible
if (sso === null) sso = !!manifest.addons?.ldap || !!manifest.addons?.proxyAuth || !!manifest.addons?.oidc;
2019-03-22 07:48:31 -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
2023-08-04 14:13:30 +05:30
if (constants.DEMO && constants.DEMO_BLACKLISTED_APPS.includes(appStoreId)) throw new BoxError(BoxError.BAD_FIELD, 'This app is blacklisted 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;
if (icon) {
2022-02-07 13:19:59 -08:00
if (!validator.isBase64(icon)) throw new BoxError(BoxError.BAD_FIELD, 'icon is not base64');
2021-04-30 13:18:15 -07:00
icon = Buffer.from(icon, 'base64');
}
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
2021-10-01 09:37:33 -07:00
const appId = uuid.v4();
2021-08-20 09:19:44 -07:00
debug('Will install app with id : ' + appId);
const app = {
accessRestriction,
memoryLimit,
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,
label,
tags,
icon,
enableMailbox,
upstreamUri,
2023-07-13 15:06:07 +05:30
enableTurn,
2023-07-13 16:37:33 +05:30
enableRedis,
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
const [addError] = await safe(add(appId, appStoreId, manifest, subdomain, domain, portBindings, app));
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;
2021-08-20 09:19:44 -07:00
await purchaseApp({ appId, appstoreId: appStoreId, manifestId: manifest.id || 'customapp' });
2021-08-20 09:19:44 -07:00
const task = {
args: { restoreConfig: null, skipDnsSetup, overwriteDns },
values: { },
requiredState: app.installationState
};
2021-11-17 10:38:02 -08:00
const taskId = await addTask(appId, app.installationState, task, auditSource);
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); });
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 };
}
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;
let 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');
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;
await reverseProxy.writeAppConfigs(Object.assign({}, app, { upstreamUri }));
2022-06-08 11:21:09 +02:00
await update(appId, { upstreamUri });
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;
let 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;
let 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
}
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) {
2022-02-07 13:19:59 -08:00
if (!validator.isBase64(icon)) throw new BoxError(BoxError.BAD_FIELD, 'icon is not base64');
2021-04-30 13:18:15 -07:00
icon = Buffer.from(icon, 'base64');
2020-03-29 17:11:10 -07:00
}
2019-09-08 16:57:08 -07:00
2021-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
}
2021-08-20 09:19:44 -07:00
async function setCpuShares(app, cpuShares, auditSource) {
2020-03-29 17:11:10 -07:00
assert.strictEqual(typeof app, 'object');
2020-01-28 21:30:35 -08:00
assert.strictEqual(typeof cpuShares, '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;
2020-01-28 21:30:35 -08:00
2020-03-29 17:11:10 -07:00
error = validateCpuShares(cpuShares);
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: {},
values: { cpuShares }
};
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
2021-08-20 09:19:44 -07:00
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, cpuShares, 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;
let 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
}
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;
let error = checkAppState(app, exports.ISTATE_PENDING_SERVICES_CHANGE);
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;
let error = checkAppState(app, exports.ISTATE_PENDING_SERVICES_CHANGE);
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');
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
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');
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
portBindings: null,
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
2020-03-29 17:11:10 -07:00
if ('portBindings' in data) {
2024-02-27 13:19:19 +01:00
error = validatePortBindings(data.portBindings || null, app.manifest);
if (error) throw error;
2024-02-27 13:19:19 +01:00
values.portBindings = translatePortBindings(data.portBindings || 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
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: {
2022-01-16 18:43:19 -08: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
};
2021-11-17 10:38:02 -08:00
let [taskError, taskId] = await safe(addTask(appId, exports.ISTATE_PENDING_LOCATION_CHANGE, task, auditSource));
if (taskError && taskError.reason === BoxError.ALREADY_EXISTS) taskError = getDuplicateErrorDetails(taskError.message, locations, data.portBindings);
2021-08-20 09:19:44 -07:00
if (taskError) throw taskError;
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); });
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;
let 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 },
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');
const skipBackup = !!data.skipBackup,
appId = app.id,
manifest = data.manifest,
appStoreId = data.appStoreId;
2020-03-31 15:44:46 -07:00
let values = {};
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);
2024-03-30 18:25:33 +01:00
error = await checkManifest(manifest);
2021-08-20 09:19:44 -07:00
if (error) throw error;
2021-08-20 09:19:44 -07:00
const updateConfig = { skipBackup, manifest, appStoreId }; // this will clear appStoreId when updating from a repo and set it if passed in for update route
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) {
2022-02-07 13:19:59 -08:00
if (!validator.isBase64(data.icon)) throw new BoxError(BoxError.BAD_FIELD, 'icon is not base64');
2021-04-30 13:18:15 -07:00
data.icon = Buffer.from(data.icon, 'base64');
}
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
if (!manifest.addons?.sendmail) { // clear if the update removed addon
2020-03-31 15:44:46 -07:00
values.mailboxName = values.mailboxDomain = null;
} 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;
}
const hasSso = !!updateConfig.manifest.addons?.proxyAuth || !!updateConfig.manifest.addons?.ldap || !!manifest.addons?.oidc;
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 },
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);
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 };
}
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');
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);
2023-03-27 10:38:09 +02:00
const cp = logs.tail(logPaths, { lines: options.lines, follow: options.follow });
2023-03-27 10:38:09 +02:00
const logStream = new logs.LogStream({ format: options.format || 'json', source: appId });
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;
}
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
// 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;
let errorState = (app.error && app.error.installationState) || exports.ISTATE_PENDING_CONFIGURE;
2019-09-19 17:04:11 -07:00
2020-03-29 17:11:10 -07:00
const task = {
args: {},
values: {},
requiredState: null
};
2019-09-21 19:45:55 -07:00
2020-03-29 17:11:10 -07:00
// maybe split this into a separate route like reinstall?
if (errorState === exports.ISTATE_PENDING_INSTALL || errorState === exports.ISTATE_PENDING_CLONE) {
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) {
let 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);
2021-08-20 09:19:44 -07:00
await eventlog.add(eventlog.ACTION_APP_REPAIR, auditSource, { app, taskId });
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');
2020-03-29 17:11:10 -07:00
const appId = app.id;
2020-03-29 17:11:10 -07:00
let error = checkAppState(app, exports.ISTATE_PENDING_RESTORE);
2021-07-14 11:07:19 -07:00
if (error) throw error;
2020-03-29 17:11:10 -07:00
// for empty or null backupId, use existing manifest to mimic a reinstall
2021-07-14 11:07:19 -07:00
const backupInfo = backupId ? await backups.get(backupId) : { manifest: app.manifest };
2024-02-27 11:35:14 +01:00
const manifest = backupInfo.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');
2021-07-14 11:07:19 -07:00
if (backupInfo.encryptionVersion === 1) throw new BoxError(BoxError.BAD_FIELD, 'This encrypted backup was created with an older Cloudron version and has to be restored using the CLI tool');
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;
}
const restoreConfig = { remotePath: backupInfo.remotePath, backupFormat: backupInfo.format };
2020-03-31 15:44:46 -07:00
2021-07-14 11:07:19 -07:00
const task = {
args: {
restoreConfig,
oldManifest: app.manifest,
skipDnsSetup: !!backupId, // if this is a restore, just skip dns setup. only re-installs should setup dns
overwriteDns: true
},
values
};
2021-11-17 10:38:02 -08:00
const taskId = await addTask(appId, exports.ISTATE_PENDING_RESTORE, task, auditSource);
2024-02-27 11:35:14 +01:00
await eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { app, backupId: backupInfo.id, remotePath: backupInfo.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 };
}
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');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof auditSource, 'object');
2020-03-29 17:11:10 -07:00
const appId = app.id;
const { remotePath, backupFormat, backupConfig } = data;
2022-10-02 10:08:50 +02:00
let error = checkAppState(app, exports.ISTATE_PENDING_IMPORT);
2021-08-20 09:19:44 -07:00
if (error) throw error;
2022-10-02 10:08:50 +02:00
let restoreConfig;
if (data.remotePath) { // if not provided, we import in-place
2023-08-15 20:24:54 +05:30
error = backups.validateFormat(backupFormat);
2022-10-02 10:08:50 +02:00
if (error) throw error;
2020-03-29 17:11:10 -07:00
2021-08-20 09:19:44 -07:00
if ('password' in backupConfig) {
backupConfig.encryption = backups.generateEncryptionKeysSync(backupConfig.password);
delete backupConfig.password;
} else {
backupConfig.encryption = null;
}
await backups.setupStorage(backupConfig, `/mnt/appimport-${app.id}`); // this validates mountOptions
backupConfig.rootPath = backups.getRootPath(backupConfig, `/mnt/appimport-${app.id}`);
error = await backups.testStorage(backupConfig); // this validates provider and it's api options. requires rootPath
if (error) throw error;
2022-10-02 10:08:50 +02:00
restoreConfig = { remotePath, backupFormat, backupConfig };
} else {
restoreConfig = { remotePath: null };
}
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);
await eventlog.add(eventlog.ACTION_APP_IMPORT, auditSource, { app: app, remotePath, fromManifest: app.manifest, toManifest: app.manifest, taskId });
2021-08-20 09:19:44 -07:00
return { taskId };
}
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;
let error = checkAppState(app, exports.ISTATE_PENDING_BACKUP);
2021-08-20 09:19:44 -07:00
if (error) throw error;
2020-12-06 19:38:50 -08:00
const task = {
args: { snapshotOnly: true },
values: {}
};
2021-11-17 10:38:02 -08:00
const taskId = await addTask(appId, exports.ISTATE_PENDING_BACKUP, task, auditSource);
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 purchaseApp(data) {
2019-05-05 10:31:42 -07:00
assert.strictEqual(typeof data, 'object');
2021-08-20 09:19:44 -07:00
const [purchaseError] = await safe(appstore.purchaseApp(data));
if (!purchaseError) return;
2021-08-18 15:54:53 -07:00
2021-08-20 09:19:44 -07:00
await del(data.appId);
2021-09-16 17:19:38 +02:00
throw purchaseError;
2019-05-05 10:31:42 -07: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
2021-08-20 09:19:44 -07:00
const backupInfo = await backups.get(backupId);
2016-06-17 17:12:55 -05:00
if (!backupInfo) throw new BoxError(BoxError.NOT_FOUND, 'Backup not found');
if (!backupInfo.manifest) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Could not detect restore manifest');
2021-08-20 09:19:44 -07:00
if (backupInfo.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
2021-08-20 09:19:44 -07:00
const manifest = backupInfo.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-02-27 13:19:19 +01:00
error = validatePortBindings(data.portBindings || null, manifest);
2021-08-20 09:19:44 -07:00
if (error) throw error;
2024-02-27 13:45:08 +01:00
const portBindings = translatePortBindings(data.portBindings || null, manifest);
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
const newAppId = uuid.v4();
const icons = await getIcons(app.id);
const dolly = _.pick(app, 'memoryLimit', 'cpuShares', 'crontab', 'reverseProxyConfig', 'env', 'servicesConfig', 'tags',
'enableMailbox', 'mailboxDisplayName', 'mailboxName', 'mailboxDomain', 'enableInbox', 'inboxName', 'inboxDomain',
'enableTurn', 'enableRedis', 'mounts', 'enableBackup', 'enableAutomaticUpdate', 'accessRestriction', 'operators', 'sso');
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
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: [],
label: app.label ? `${app.label}-clone` : '',
icon: icons.icon,
});
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;
2021-08-20 09:19:44 -07:00
await purchaseApp({ appId: newAppId, appstoreId: app.appStoreId, manifestId: manifest.id || 'customapp' });
2016-06-17 17:12:55 -05:00
2022-04-05 10:26:05 -07:00
const restoreConfig = { remotePath: backupInfo.remotePath, backupFormat: backupInfo.format };
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
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
2024-02-27 11:44:42 +01:00
await eventlog.add(eventlog.ACTION_APP_CLONE, auditSource, { appId: newAppId, oldAppId: app.id, backupId, remotePath: backupInfo.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
}
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');
2020-03-29 17:11:10 -07:00
const appId = app.id;
let error = checkAppState(app, exports.ISTATE_PENDING_UNINSTALL);
2021-08-20 09:19:44 -07:00
if (error) throw error;
2021-08-20 09:19:44 -07:00
await appstore.unpurchaseApp(appId, { appstoreId: app.appStoreId, manifestId: app.manifest.id || 'customapp' });
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 };
}
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');
2020-03-29 17:11:10 -07:00
const appId = app.id;
let 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 };
}
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');
2020-03-29 17:11:10 -07:00
const appId = app.id;
let 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 };
}
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;
let 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-10-24 10:39:47 -07:00
if (!manifest.dockerImage) return new BoxError(BoxError.BAD_FIELD, 'Missing dockerImage'); // dockerImage is optional in manifest
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');
}
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');
}
2024-03-30 18:31:57 +01:00
const error = await services.checkAddonsSupport(manifest.addons || {});
return error;
}
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');
assert(options && typeof options === 'object');
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);
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;
}
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);
}
function canAutoupdateApp(app, updateInfo) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof updateInfo, 'object');
const manifest = updateInfo.manifest;
2019-12-04 11:18:39 -08:00
if (!app.enableAutomaticUpdate) return false;
// for invalid subscriptions the appstore does not return a dockerImage
if (!manifest.dockerImage) return false;
if (updateInfo.unstable) return false; // only manual update allowed for unstable updates
if ((semver.major(app.manifest.version) !== 0) && (semver.major(app.manifest.version) !== semver.major(manifest.version))) return false; // major changes are blocking
if (app.runState === exports.RSTATE_STOPPED) return false; // stopped apps won't run migration scripts and shouldn't be updated
const newTcpPorts = manifest.tcpPorts || { };
const newUdpPorts = manifest.udpPorts || { };
const portBindings = app.portBindings; // this is never null
for (let portName in portBindings) {
2019-12-04 10:29:06 -08:00
if (!(portName in newTcpPorts) && !(portName in newUdpPorts)) return false; // portName was in use but new update removes it
}
// it's fine if one or more (unused) keys got removed
2019-12-04 10:29:06 -08:00
return true;
}
2015-09-10 11:39:03 -07:00
2021-08-20 09:19:44 -07:00
async function autoupdateApps(updateInfo, auditSource) { // updateInfo is { appId -> { manifest } }
assert.strictEqual(typeof updateInfo, 'object');
assert.strictEqual(typeof auditSource, 'object');
2015-09-10 11:39:03 -07:00
2021-08-20 09:19:44 -07:00
for (const appId of Object.keys(updateInfo)) {
const [getError, app] = await safe(get(appId));
if (getError) {
debug(`Cannot autoupdate app ${appId}: ${getError.message}`);
continue;
}
2021-08-20 09:19:44 -07:00
if (!canAutoupdateApp(app, updateInfo[appId])) {
debug(`app ${app.fqdn} requires manual update`);
notifications.alert(notifications.ALERT_MANUAL_APP_UPDATE, `${app.manifest.title} at ${app.fqdn} requires manual update to version ${updateInfo[appId].manifest.version}`, `Changelog:\n${updateInfo[appId].manifest.changelog}\n`, { persist: false });
2021-08-20 09:19:44 -07:00
continue;
}
2016-06-04 19:06:16 -07:00
2021-08-20 09:19:44 -07:00
const data = {
manifest: updateInfo[appId].manifest,
force: false
};
2021-08-20 09:19:44 -07:00
const [updateError] = await safe(updateApp(app, data, auditSource));
if (updateError) debug(`Error initiating autoupdate of ${appId}. ${updateError.message}`);
}
}
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');
2020-03-29 17:11:10 -07:00
const appId = app.id;
2019-09-21 19:45:55 -07:00
2020-03-29 17:11:10 -07:00
let error = checkAppState(app, exports.ISTATE_PENDING_BACKUP);
2021-08-20 09:19:44 -07:00
if (error) throw error;
2020-03-29 17:11:10 -07:00
const task = {
args: {},
values: {}
};
2021-11-17 10:38:02 -08:00
const taskId = await addTask(appId, exports.ISTATE_PENDING_BACKUP, task, auditSource);
2021-09-30 10:45:25 -07:00
await eventlog.add(eventlog.ACTION_APP_BACKUP, auditSource, { app, appId, taskId });
2019-08-27 20:55:49 -07:00
2021-08-20 09:19:44 -07:00
return { taskId };
}
2021-07-14 11:07:19 -07:00
async function listBackups(app, page, perPage) {
2020-03-29 17:11:10 -07:00
assert.strictEqual(typeof app, 'object');
2016-03-08 08:57:28 -08:00
assert(typeof page === 'number' && page > 0);
assert(typeof perPage === 'number' && perPage > 0);
2016-01-19 13:35:18 +01:00
2021-07-14 11:07:19 -07:00
return await backups.getByIdentifierAndStatePaged(app.id, backups.BACKUP_STATE_NORMAL, page, perPage);
2016-01-19 13:35:18 +01:00
}
2016-05-24 10:33:10 -07:00
async function updateBackup(app, backupId, data) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof data, 'object');
const backup = await backups.get(backupId);
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
await backups.update(backupId, data);
}
2022-11-03 22:13:57 +01:00
async function getBackupDownloadStream(app, backupId) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof backupId, 'string');
const backup = await backups.get(backupId);
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');
2023-08-04 11:24:28 +05:30
const backupConfig = await backups.getConfig();
2022-11-03 22:13:57 +01:00
const ps = new PassThrough();
const stream = await storage.api(backupConfig.provider).download(backupConfig, tgz.getBackupFilePath(backupConfig, backup.remotePath));
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
});
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
}
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) {
2022-04-08 16:27:11 -07: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
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));
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
async function configureApps(apps, options, auditSource) {
assert(Array.isArray(apps));
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
const scheduleNow = !!options.scheduleNow;
2021-08-20 09:19:44 -07:00
for (const app of apps) {
debug(`configureApps: marking ${app.fqdn} for reconfigure (scheduleNow: ${scheduleNow})`);
2021-08-20 09:19:44 -07:00
const task = {
args: {},
values: {},
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));
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 }
};
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);
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
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');
2021-10-21 15:25:15 -07:00
let type = parts[0], filename, cmd, size;
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
}
2021-10-21 15:15:39 -07:00
async function uploadFile(app, sourceFilePath, destFilePath) {
2020-03-29 17:11:10 -07:00
assert.strictEqual(typeof app, 'object');
2017-08-18 20:45:52 -07:00
assert.strictEqual(typeof sourceFilePath, 'string');
assert.strictEqual(typeof destFilePath, 'string');
2019-03-04 12:28:27 -08:00
// the built-in bash printf understands "%q" but not /usr/bin/printf.
// ' gets replaced with '\'' . the first closes the quote and last one starts a new one
2024-02-21 19:40:27 +01:00
const escapedDestFilePath = await shell.exec('uploadFile', `printf %q '${destFilePath.replace(/'/g, '\'\\\'\'')}'`, { shell: '/bin/bash' });
debug(`uploadFile: ${sourceFilePath} -> ${escapedDestFilePath}`);
2022-05-16 11:37:25 -07:00
const execId = await createExec(app, { cmd: [ 'bash', '-c', `cat - > ${escapedDestFilePath}` ], tty: false });
const destStream = await startExec(app, execId, { tty: false });
2021-10-21 15:15:39 -07:00
return new Promise((resolve, reject) => {
const done = once(error => reject(new BoxError(BoxError.FS_ERROR, error.message)));
2017-08-18 20:45:52 -07:00
2021-10-21 15:15:39 -07:00
const sourceStream = fs.createReadStream(sourceFilePath);
sourceStream.on('error', done);
destStream.on('error', done);
2017-08-18 20:45:52 -07:00
2021-10-21 15:15:39 -07:00
destStream.on('finish', resolve);
2017-08-18 20:45:52 -07:00
2021-10-21 15:15:39 -07:00
sourceStream.pipe(destStream);
2017-08-18 20:45:52 -07:00
});
}
2021-05-25 21:31:48 -07:00
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
}
2024-02-10 11:53:25 +01:00
async function loadConfig(app) {
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) {
data = _.pick(appConfig, 'memoryLimit', 'cpuShares', 'enableBackup', 'reverseProxyConfig', 'env', 'servicesConfig', 'label', 'tags', 'enableAutomaticUpdate');
}
const icon = safe.fs.readFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/icon.png'));
if (icon) data.icon = icon;
2021-08-20 09:19:44 -07:00
await update(app.id, data);
}