Files
cloudron-box/src/apps.js
T

2531 lines
101 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,
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,
setLocation,
setDataDir,
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,
2021-09-21 22:19:20 -07:00
getTask,
2020-10-27 17:11:50 -07:00
getLocalLogfilePaths,
getLogs,
2021-05-07 21:37:23 -07:00
getCertificate,
2020-10-27 17:11:50 -07:00
start,
stop,
restart,
exec,
checkManifestConstraints,
downloadManifest,
canAutoupdateApp,
autoupdateApps,
restoreInstalledApps,
configureInstalledApps,
schedulePendingTasks,
restartAppsUsingAddons,
getDataDir,
2021-04-30 13:18:15 -07:00
getIcon,
2021-01-20 12:12:14 -08:00
getMemoryLimit,
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
2021-05-25 21:31:48 -07:00
backupConfig,
restoreConfig,
2021-05-25 21:31:48 -07:00
2018-08-12 22:08:19 -07:00
PORT_TYPE_TCP: 'tcp',
2018-08-13 08:33:09 -07:00
PORT_TYPE_UDP: 'udp',
2018-08-12 22:08:19 -07:00
2019-09-22 00:20:12 -07:00
// task codes - the installation state is now a misnomer (keep in sync in UI)
2019-08-30 13:12:49 -07:00
ISTATE_PENDING_INSTALL: 'pending_install', // installs and fresh reinstalls
ISTATE_PENDING_CLONE: 'pending_clone', // clone
2019-09-19 17:04:11 -07:00
ISTATE_PENDING_CONFIGURE: 'pending_configure', // infra update
ISTATE_PENDING_RECREATE_CONTAINER: 'pending_recreate_container', // env change or addon change
2019-09-08 16:57:08 -07:00
ISTATE_PENDING_LOCATION_CHANGE: 'pending_location_change',
ISTATE_PENDING_DATA_DIR_MIGRATION: 'pending_data_dir_migration',
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',
2021-08-20 09:19:44 -07:00
// subdomain table types
SUBDOMAIN_TYPE_PRIMARY: 'primary',
SUBDOMAIN_TYPE_REDIRECT: 'redirect',
SUBDOMAIN_TYPE_ALIAS: 'alias',
// exported for testing
2015-10-15 12:26:48 +02:00
_validatePortBindings: validatePortBindings,
_validateAccessRestriction: validateAccessRestriction,
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'),
AuditSource = require('./auditsource.js'),
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,
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'),
2018-05-24 16:25:32 -07:00
mail = require('./mail.js'),
manifestFormat = require('cloudron-manifestformat'),
mounts = require('./mounts.js'),
2019-03-04 12:28:27 -08:00
once = require('once'),
2017-11-07 09:09:30 -08:00
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
semver = require('semver'),
services = require('./services.js'),
2019-07-26 10:49:29 -07:00
settings = require('./settings.js'),
2015-11-02 11:20:50 -08:00
spawn = require('child_process').spawn,
split = require('split'),
superagent = require('superagent'),
system = require('./system.js'),
2019-08-26 15:55:57 -07:00
tasks = require('./tasks.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'),
_ = 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',
2021-08-20 09:19:44 -07:00
'apps.creationTime', 'apps.updateTime', 'apps.enableMailbox', 'apps.mailboxName', 'apps.mailboxDomain', 'apps.enableAutomaticUpdate',
'apps.dataDir', 'apps.ts', 'apps.healthTime', '(apps.icon IS NOT NULL) AS hasIcon', '(apps.appStoreIcon IS NOT NULL) AS hasAppStoreIcon' ].join(',');
// const PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId' ].join(',');
2019-08-27 22:39:59 -07:00
// validate the port bindings
2018-08-12 22:37:36 -07:00
function validatePortBindings(portBindings, manifest) {
2017-01-29 13:01:09 -08:00
assert.strictEqual(typeof portBindings, 'object');
2018-08-12 22:37:36 -07:00
assert.strictEqual(typeof manifest, 'object');
2017-01-29 13:01:09 -08:00
2018-06-04 21:24:14 +02:00
// keep the public ports in sync with firewall rules in setup/start/cloudron-firewall.sh
// 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 */
2003, /* graphite (lo) */
2004, /* graphite (lo) */
2018-06-04 21:12:55 +02:00
2514, /* cloudron-syslog (lo) */
2019-07-25 15:43:51 -07:00
constants.PORT, /* app server (lo) */
2020-11-09 20:34:48 -08:00
constants.AUTHWALL_PORT, /* protected sites */
2019-07-25 15:27:28 -07:00
constants.INTERNAL_SMTP_PORT, /* internal smtp port (lo) */
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 */
8417, /* graphite (lo) */
];
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;
for (let portName in portBindings) {
2019-10-24 10:39:47 -07:00
if (!/^[a-zA-Z0-9_]+$/.test(portName)) return new BoxError(BoxError.BAD_FIELD, `${portName} is not a valid environment variable`, { field: 'portBindings', portName: portName });
const hostPort = portBindings[portName];
2019-10-24 10:39:47 -07:00
if (!Number.isInteger(hostPort)) return new BoxError(BoxError.BAD_FIELD, `${hostPort} is not an integer`, { field: 'portBindings', portName: portName });
if (RESERVED_PORTS.indexOf(hostPort) !== -1) return new BoxError(BoxError.BAD_FIELD, `Port ${hostPort} is reserved.`, { field: 'portBindings', portName: portName });
2020-03-30 10:01:52 +02:00
if (RESERVED_PORT_RANGES.find(range => (hostPort >= range[0] && hostPort <= range[1]))) return new BoxError(BoxError.BAD_FIELD, `Port ${hostPort} is reserved.`, { field: 'portBindings', portName: portName });
2021-02-17 13:11:00 -08:00
if (ALLOWED_PORTS.indexOf(hostPort) === -1 && (hostPort <= 1023 || hostPort > 65535)) return new BoxError(BoxError.BAD_FIELD, `${hostPort} is not in permitted range`, { field: 'portBindings', portName: portName });
}
// it is OK if there is no 1-1 mapping between values in manifest.tcpPorts and portBindings. missing values implies
// that the user wants the service disabled
2018-08-12 22:37:36 -07:00
const tcpPorts = manifest.tcpPorts || { };
2018-08-12 22:47:59 -07:00
const udpPorts = manifest.udpPorts || { };
for (let portName in portBindings) {
2019-10-24 10:39:47 -07:00
if (!(portName in tcpPorts) && !(portName in udpPorts)) return new BoxError(BoxError.BAD_FIELD, `Invalid portBindings ${portName}`, { field: 'portBindings', portName: portName });
}
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;
result[portName] = { hostPort: portBindings[portName], type: portType };
}
return result;
}
2021-09-27 14:21:42 -07:00
function parseCrontab(crontab) {
assert(crontab === null || typeof crontab === 'string');
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;
2021-09-28 11:08:10 -07:00
const parts = /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.+)$/.exec(line);
2021-09-27 14:21:42 -07:00
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}`);
}
2021-09-28 11:08:10 -07:00
if (command.length === 0) throw new BoxError(BoxError.BAD_FIELD, `Invalid cron pattern. Command must not be empty at line ${i+1}`); // not possible with the regexp we have
2021-09-27 14:21:42 -07:00
result.push({ schedule, command });
}
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');
2016-02-14 12:13:49 +01:00
var min = manifest.memoryLimit || constants.DEFAULT_MEMORY_LIMIT;
2017-11-07 09:09:30 -08:00
var max = os.totalmem() * 2; // this will overallocate since we don't allocate equal swap always (#466)
2016-02-11 17:39:15 +01:00
// allow 0, which indicates that it is not set, the one from the manifest will be choosen but we don't commit any user value
// this is needed so an app update can change the value in the manifest, and if not set by the user, the new value should be used
if (memoryLimit === 0) return null;
// 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
2019-10-24 10:39:47 -07:00
if (robotsTxt.length > 4096) return new BoxError(BoxError.BAD_FIELD, 'robotsTxt must be less than 4096', { field: 'robotsTxt' });
// TODO: validate the robots file? we escape the string when templating the nginx config right now
2017-07-14 12:19:27 -05:00
return null;
}
2019-10-14 16:59:22 -07:00
function validateCsp(csp) {
if (csp === null) return null;
2019-10-13 18:22:03 -07:00
2019-10-24 10:39:47 -07:00
if (csp.length > 4096) return new BoxError(BoxError.BAD_FIELD, 'CSP must be less than 4096', { field: 'csp' });
2019-10-13 18:22:03 -07:00
2019-10-24 10:39:47 -07:00
if (csp.includes('"')) return new BoxError(BoxError.BAD_FIELD, 'CSP cannot contains double quotes', { field: 'csp' });
2019-10-13 18:22:03 -07:00
return null;
}
2019-10-11 20:30:30 -07:00
function validateBackupFormat(format) {
assert.strictEqual(typeof format, 'string');
if (format === 'tgz' || format == 'rsync') return null;
2019-10-24 10:39:47 -07:00
return new BoxError(BoxError.BAD_FIELD, 'Invalid backup format');
2019-10-11 20:30:30 -07:00
}
2019-03-22 07:48:31 -07:00
function validateLabel(label) {
if (label === null) return null;
2019-10-24 10:39:47 -07:00
if (label.length > 128) return new BoxError(BoxError.BAD_FIELD, 'label must be less than 128', { field: 'label' });
2019-03-22 07:48:31 -07:00
return null;
}
function validateTags(tags) {
2019-10-24 10:39:47 -07:00
if (tags.length > 64) return new BoxError(BoxError.BAD_FIELD, 'Can only set up to 64 tags', { field: 'tags' });
2019-03-22 07:48:31 -07:00
2019-10-24 10:39:47 -07:00
if (tags.some(tag => tag.length == 0)) return new BoxError(BoxError.BAD_FIELD, 'tag cannot be empty', { field: 'tags' });
if (tags.some(tag => tag.length > 128)) return new BoxError(BoxError.BAD_FIELD, 'tag must be less than 128', { field: 'tags' });
2019-03-22 07:48:31 -07:00
return null;
}
2018-10-18 11:19:32 -07:00
function validateEnv(env) {
for (let key in env) {
2019-10-24 10:39:47 -07:00
if (key.length > 512) return new BoxError(BoxError.BAD_FIELD, 'Max env var key length is 512', { field: 'env', env: env });
2018-10-18 11:19:32 -07:00
// http://pubs.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap08.html
2019-10-24 10:39:47 -07:00
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) return new BoxError(BoxError.BAD_FIELD, `"${key}" is not a valid environment variable`, { field: 'env', env: env });
2018-10-18 11:19:32 -07:00
}
return null;
}
2018-12-20 14:33:29 -08:00
function validateDataDir(dataDir) {
2019-09-09 16:37:59 -07:00
if (dataDir === null) return null;
2018-12-20 14:33:29 -08:00
if (!path.isAbsolute(dataDir)) return new BoxError(BoxError.BAD_FIELD, `${dataDir} is not an absolute path`, { field: 'dataDir' });
if (dataDir.endsWith('/')) return new BoxError(BoxError.BAD_FIELD, `${dataDir} contains trailing slash`, { field: 'dataDir' });
if (path.normalize(dataDir) !== dataDir) return new BoxError(BoxError.BAD_FIELD, `${dataDir} is not a normalized path`, { field: 'dataDir' });
2018-12-20 14:33:29 -08:00
// nfs shares will have the directory mounted already
let stat = safe.fs.lstatSync(dataDir);
if (stat) {
if (!stat.isDirectory()) return new BoxError(BoxError.BAD_FIELD, `${dataDir} is not a directory`, { field: 'dataDir' });
2018-12-20 14:33:29 -08:00
let entries = safe.fs.readdirSync(dataDir);
if (!entries) return new BoxError(BoxError.BAD_FIELD, `${dataDir} could not be listed`, { field: 'dataDir' });
if (entries.length !== 0) return new BoxError(BoxError.BAD_FIELD, `${dataDir} is not empty. If this is the root of a mounted volume, provide a subdirectory.`, { field: 'dataDir' });
2018-12-20 14:33:29 -08:00
}
2019-01-19 22:19:43 -08:00
// backup logic relies on paths not overlapping (because it recurses)
if (dataDir.startsWith(paths.APPS_DATA_DIR)) return new BoxError(BoxError.BAD_FIELD, `${dataDir} cannot be inside apps data`, { field: 'dataDir' });
2018-12-20 14:33:29 -08:00
2019-01-19 22:19:43 -08:00
// if we made it this far, it cannot start with any of these realistically
const fhs = [ '/bin', '/boot', '/etc', '/lib', '/lib32', '/lib64', '/proc', '/run', '/sbin', '/tmp', '/usr' ];
if (fhs.some((p) => dataDir.startsWith(p))) return new BoxError(BoxError.BAD_FIELD, `${dataDir} cannot be placed inside this location`, { field: 'dataDir' });
2019-01-19 22:19:43 -08:00
2018-12-20 14:33:29 -08:00
return null;
}
2019-09-27 10:25:26 -07:00
function getDuplicateErrorDetails(errorMessage, locations, domainObjectMap, portBindings) {
assert.strictEqual(typeof errorMessage, 'string');
2019-09-27 10:25:26 -07:00
assert(Array.isArray(locations));
assert.strictEqual(typeof domainObjectMap, 'object');
assert.strictEqual(typeof portBindings, 'object');
var 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
2019-03-19 20:43:42 -07:00
if (match[2] === 'subdomain') {
2019-09-27 10:25:26 -07:00
for (let i = 0; i < locations.length; i++) {
const { subdomain, domain } = locations[i];
if (match[1] !== `${subdomain}-${domain}`) continue;
2019-06-05 16:01:44 +02:00
2021-08-13 17:22:28 -07:00
return new BoxError(BoxError.ALREADY_EXISTS, `Domain '${dns.fqdn(subdomain, domainObjectMap[domain])}' is in use`, { subdomain, domain });
2019-09-01 21:34:27 -07:00
}
2019-03-19 20:43:42 -07:00
}
// check if any of the port bindings conflict
for (let portName in portBindings) {
2020-12-03 22:27:59 -08:00
if (portBindings[portName] === parseInt(match[1])) return new BoxError(BoxError.ALREADY_EXISTS, `Port ${match[1]} is in use`, { portName });
}
2019-09-03 15:17:48 -07:00
if (match[2] === 'dataDir') {
2019-10-24 10:39:47 -07:00
return new BoxError(BoxError.BAD_FIELD, `Data directory ${match[1]} is in use`, { field: 'dataDir' });
2019-09-03 15:17:48 -07:00
}
2019-10-24 10:39:47 -07:00
return new BoxError(BoxError.ALREADY_EXISTS, `${match[2]} '${match[1]}' is in use`);
}
2018-12-20 14:33:29 -08:00
function getDataDir(app, dataDir) {
2019-09-15 21:51:38 -07:00
assert(dataDir === null || typeof dataDir === 'string');
2018-12-20 14:33:29 -08:00
return dataDir || path.join(paths.APPS_DATA_DIR, app.id, 'data');
}
2018-06-25 16:40:16 -07:00
function removeInternalFields(app) {
2018-05-24 15:27:55 -07:00
return _.pick(app,
2019-08-30 11:18:42 -07:00
'id', 'appStoreId', 'installationState', 'error', 'runState', 'health', 'taskId',
2021-09-27 14:21:42 -07:00
'location', 'domain', 'fqdn', 'mailboxName', 'mailboxDomain', 'crontab',
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',
2021-03-16 22:38:59 -07:00
'label', 'alternateDomains', 'aliasDomains', 'env', 'enableAutomaticUpdate', 'dataDir', 'mounts', 'enableMailbox');
2018-04-29 10:47:34 -07:00
}
2019-02-11 14:37:49 -08:00
// non-admins can only see these
2018-06-25 16:45:15 -07:00
function removeRestrictedFields(app) {
return _.pick(app,
'id', 'appStoreId', 'installationState', 'error', 'runState', 'health', 'taskId', 'accessRestriction', 'alternateDomains', 'aliasDomains', 'sso',
2021-09-21 17:28:58 -07:00
'location', 'domain', 'fqdn', 'manifest', 'portBindings', 'iconUrl', 'creationTime', 'ts', 'tags', 'label', 'enableBackup');
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;
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;
let subdomains = JSON.parse(result.subdomains), domains = JSON.parse(result.domains), subdomainTypes = JSON.parse(result.subdomainTypes);
delete result.subdomains;
delete result.domains;
delete result.subdomainTypes;
result.alternateDomains = [];
result.aliasDomains = [];
for (let i = 0; i < subdomainTypes.length; i++) {
if (subdomainTypes[i] === exports.SUBDOMAIN_TYPE_PRIMARY) {
result.location = subdomains[i];
result.domain = domains[i];
} else if (subdomainTypes[i] === exports.SUBDOMAIN_TYPE_REDIRECT) {
result.alternateDomains.push({ domain: domains[i], subdomain: subdomains[i] });
} else if (subdomainTypes[i] === exports.SUBDOMAIN_TYPE_ALIAS) {
result.aliasDomains.push({ domain: domains[i], subdomain: subdomains[i] });
}
}
let envNames = JSON.parse(result.envNames), envValues = JSON.parse(result.envValues);
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];
}
let volumeIds = JSON.parse(result.volumeIds);
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;
2021-08-13 17:22:28 -07:00
app.fqdn = dns.fqdn(app.location, domainObjectMap[app.domain]);
app.alternateDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
app.aliasDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[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) {
2015-10-15 15:06:34 +02:00
assert.strictEqual(typeof app, 'object');
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) {
assert.strictEqual(typeof app, 'object');
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
}
2021-08-20 09:19:44 -07:00
async function add(id, appStoreId, manifest, location, domain, portBindings, data) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof appStoreId, 'string');
assert(manifest && typeof manifest === 'object');
assert.strictEqual(typeof manifest.version, 'string');
assert.strictEqual(typeof location, 'string');
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 || { };
const manifestJson = JSON.stringify(manifest);
const accessRestriction = data.accessRestriction || null;
const accessRestrictionJson = JSON.stringify(accessRestriction);
const memoryLimit = data.memoryLimit || 0;
const cpuShares = data.cpuShares || 512;
const installationState = data.installationState;
const runState = data.runState;
const sso = 'sso' in data ? data.sso : null;
const debugModeJson = data.debugMode ? JSON.stringify(data.debugMode) : null;
const env = data.env || {};
const label = data.label || null;
const tagsJson = data.tags ? JSON.stringify(data.tags) : null;
const mailboxName = data.mailboxName || null;
const mailboxDomain = data.mailboxDomain || null;
const reverseProxyConfigJson = data.reverseProxyConfig ? JSON.stringify(data.reverseProxyConfig) : null;
const servicesConfigJson = data.servicesConfig ? JSON.stringify(data.servicesConfig) : null;
const enableMailbox = data.enableMailbox || false;
const icon = data.icon || null;
let queries = [];
queries.push({
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, cpuShares, '
+ 'sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson, servicesConfigJson, icon, enableMailbox) '
+ ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, cpuShares,
sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson, servicesConfigJson, icon, enableMailbox ]
});
2021-08-20 09:19:44 -07:00
queries.push({
query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)',
args: [ id, domain, location, exports.SUBDOMAIN_TYPE_PRIMARY ]
});
2019-03-06 11:12:39 -08:00
2021-08-20 09:19:44 -07:00
Object.keys(portBindings).forEach(function (env) {
queries.push({
query: 'INSERT INTO appPortBindings (environmentVariable, hostPort, type, appId) VALUES (?, ?, ?, ?)',
args: [ env, portBindings[env].hostPort, portBindings[env].type, id ]
});
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] ]
});
});
if (data.alternateDomains) {
data.alternateDomains.forEach(function (d) {
queries.push({
query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)',
args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_REDIRECT ]
});
});
}
if (data.aliasDomains) {
data.aliasDomains.forEach(function (d) {
queries.push({
query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)',
args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_ALIAS ]
});
});
}
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 === '');
assert(!('alternateDomains' in app) || Array.isArray(app.alternateDomains));
assert(!('aliasDomains' in app) || Array.isArray(app.aliasDomains));
assert(!('tags' in app) || Array.isArray(app.tags));
assert(!('env' in app) || typeof app.env === 'object');
const queries = [ ];
if ('portBindings' in app) {
var portBindings = app.portBindings || { };
// replace entries by app id
queries.push({ query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] });
Object.keys(portBindings).forEach(function (env) {
var values = [ portBindings[env].hostPort, portBindings[env].type, env, id ];
queries.push({ query: 'INSERT INTO appPortBindings (hostPort, type, environmentVariable, appId) VALUES(?, ?, ?, ?)', args: values });
});
}
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
}
if ('location' in app && 'domain' in app) { // must be updated together as they are unique together
queries.push({ query: 'DELETE FROM subdomains WHERE appId = ?', args: [ id ]}); // all locations of an app must be updated together
queries.push({ query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, app.domain, app.location, exports.SUBDOMAIN_TYPE_PRIMARY ]});
if ('alternateDomains' in app) {
app.alternateDomains.forEach(function (d) {
queries.push({ query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_REDIRECT ]});
});
}
if ('aliasDomains' in app) {
app.aliasDomains.forEach(function (d) {
queries.push({ query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_ALIAS ]});
});
}
}
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]));
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains' && p !== 'aliasDomains' && p !== 'env' && p !== 'mounts') {
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 = [
{ query: 'DELETE FROM subdomains WHERE appId = ?', args: [ id ] },
{ 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() {
await database.query('DELETE FROM subdomains');
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
async function getDomainObjectMap() {
const domainObjects = await domains.list();
let domainObjectMap = {};
for (let d of domainObjects) { domainObjectMap[d.domain] = d; }
return domainObjectMap;
}
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';
const SUBDOMAIN_QUERY = 'SELECT id, JSON_ARRAYAGG(subdomains.subdomain) AS subdomains, JSON_ARRAYAGG(subdomains.domain) AS domains, JSON_ARRAYAGG(subdomains.type) AS subdomainTypes FROM apps LEFT JOIN subdomains ON apps.id = subdomains.appId GROUP BY apps.id';
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';
const APPS_QUERY = `SELECT ${APPS_FIELDS_PREFIXED}, hostPorts, environmentVariables, portTypes, envNames, envValues, subdomains, domains, subdomainTypes, volumeIds, volumeReadOnlys FROM apps`
+ ` 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
2021-08-20 09:19:44 -07:00
const domainObjectMap = await 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
2021-08-20 09:19:44 -07:00
const domainObjectMap = await 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() {
const domainObjectMap = await getDomainObjectMap();
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);
}
2021-08-20 09:19:44 -07:00
async function downloadManifest(appStoreId, manifest) {
if (!appStoreId && !manifest) throw new BoxError(BoxError.BAD_FIELD, 'Neither manifest nor appStoreId provided');
2016-06-07 15:36:45 -07:00
2021-08-20 09:19:44 -07:00
if (!appStoreId) return { appStoreId: '', manifest };
2021-08-20 09:19:44 -07:00
const parts = appStoreId.split('@');
2021-08-20 09:19:44 -07:00
const url = settings.apiServerOrigin() + '/api/v1/apps/' + parts[0] + (parts[1] ? '/versions/' + parts[1] : '');
debug('downloading manifest from %s', url);
2021-08-20 09:19:44 -07:00
const [error, response] = await safe(superagent.get(url).timeout(30 * 1000).ok(() => true));
2021-08-20 09:19:44 -07:00
if (error) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Network error downloading manifest:' + error.message);
2021-08-20 09:19:44 -07:00
if (response.status !== 200) throw new BoxError(BoxError.NOT_FOUND, `Failed to get app info from store. status: ${response.status} text: ${response.text}`);
2021-08-20 09:19:44 -07:00
if (!response.body.manifest || typeof response.body.manifest !== 'object') throw new BoxError(BoxError.NOT_FOUND, `Missing manifest. Failed to get app info from store. status: ${response.status} text: ${response.text}`);
return { appStoreId: parts[0], manifest: response.body.manifest };
}
function mailboxNameForLocation(location, manifest) {
2019-11-14 21:43:14 -08:00
if (location) return `${location}.app`;
if (manifest.title) return manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '') + '.app';
return 'noreply.app';
}
async function onTaskFinished(appId, installationState, taskId, error) {
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.APPTASK), { 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.APPTASK, { app, toManifest, fromManifest, success, errorMessage });
break;
}
2021-09-30 10:45:25 -07:00
case exports.ISTATE_PENDING_BACKUP:
await eventlog.add(eventlog.ACTION_APP_BACKUP_FINISH, AuditSource.APPTASK, { app, success, errorMessage, backupId: task.result });
break;
}
}
async function scheduleTask(appId, installationState, taskId) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof installationState, 'string');
2019-09-24 10:28:50 -07:00
assert.strictEqual(typeof taskId, 'string');
2021-09-07 09:57:49 -07:00
const backupConfig = await settings.getBackupConfig();
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) {
memoryLimit = 'memoryLimit' in backupConfig ? Math.max(backupConfig.memoryLimit/1024/1024, 400) : 400;
} 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(appId, installationState, taskId, error), { debug }); // ignore error
});
}
2021-08-20 09:19:44 -07:00
async function addTask(appId, installationState, task) {
2019-08-28 15:00:55 -07:00
assert.strictEqual(typeof appId, 'string');
2019-09-23 14:17:12 -07:00
assert.strictEqual(typeof installationState, 'string');
assert.strictEqual(typeof task, 'object'); // { args, values }
2019-08-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
2021-08-20 09:19:44 -07:00
const [updateError] = await safe(setTask(appId, _.extend({ installationState, taskId, error: null }, values), { requiredState, requireNullTaskId }));
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), { 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;
}
2021-08-20 09:19:44 -07:00
async function validateLocations(locations) {
2019-09-27 10:25:26 -07:00
assert(Array.isArray(locations));
2021-08-20 09:19:44 -07:00
const domainObjectMap = await getDomainObjectMap();
2019-09-27 10:25:26 -07:00
2021-08-20 09:19:44 -07:00
for (let location of locations) {
if (!(location.domain in domainObjectMap)) throw new BoxError(BoxError.BAD_FIELD, 'No such domain', { field: 'location', domain: location.domain, subdomain: location.subdomain });
2021-01-18 22:47:53 -08:00
2021-08-20 09:19:44 -07:00
let subdomain = location.subdomain;
if (location.type === 'alias' && subdomain.startsWith('*')) {
if (subdomain === '*') continue;
subdomain = subdomain.replace(/^\*\./, ''); // remove *.
2019-09-27 10:25:26 -07:00
}
2021-08-20 09:19:44 -07:00
const error = dns.validateHostname(subdomain, domainObjectMap[location.domain]);
if (error) throw new BoxError(BoxError.BAD_FIELD, 'Bad location: ' + error.message, { field: 'location', domain: location.domain, subdomain: location.subdomain });
}
return domainObjectMap;
2019-09-27 10:25:26 -07:00
}
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
2021-10-01 09:37:33 -07:00
const location = data.location.toLowerCase(),
2017-11-02 22:17:44 +01:00
domain = data.domain.toLowerCase(),
2016-06-03 23:22:38 -07:00
portBindings = data.portBindings || null,
accessRestriction = data.accessRestriction || null,
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,
2018-10-11 14:07:43 -07:00
alternateDomains = data.alternateDomains || [],
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,
appStoreId = data.appStoreId,
2021-06-16 23:09:30 -07:00
enableMailbox = 'enabledMailbox' in data ? data.enableMailbox : true,
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}`);
error = checkManifestConstraints(manifest);
2021-08-20 09:19:44 -07:00
if (error) throw error;
error = validatePortBindings(portBindings, manifest);
2021-08-20 09:19:44 -07:00
if (error) throw error;
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;
error = validateTags(tags);
2021-08-20 09:19:44 -07:00
if (error) throw error;
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
2020-11-10 09:59:28 -08:00
if (sso === null) sso = !!manifest.addons['ldap'] || !!manifest.addons['proxyAuth'];
2019-03-22 07:48:31 -07:00
error = validateEnv(env);
2021-08-20 09:19:44 -07:00
if (error) throw error;
2019-03-22 07:48:31 -07:00
2021-08-20 09:19:44 -07:00
if (settings.isDemo() && 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
const mailboxName = manifest.addons?.sendmail ? mailboxNameForLocation(location, manifest) : null;
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) {
2021-08-20 09:19:44 -07:00
if (!validator.isBase64(icon)) throw new BoxError(BoxError.BAD_FIELD, 'icon is not base64', { field: 'icon' });
2021-04-30 13:18:15 -07:00
icon = Buffer.from(icon, 'base64');
}
2016-07-09 12:25:00 -07:00
2021-01-18 22:47:53 -08:00
const locations = [{ subdomain: location, domain, type: 'primary' }]
.concat(alternateDomains.map(ad => _.extend(ad, { type: 'redirect' })))
.concat(aliasDomains.map(ad => _.extend(ad, { type: 'alias' })));
2021-08-20 09:19:44 -07:00
const domainObjectMap = await validateLocations(locations);
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,
alternateDomains,
aliasDomains,
env,
label,
tags,
icon,
enableMailbox,
runState: exports.RSTATE_RUNNING,
installationState: exports.ISTATE_PENDING_INSTALL
};
2018-01-09 21:03:59 -08:00
2021-08-20 09:19:44 -07:00
const [addError] = await safe(add(appId, appStoreId, manifest, location, domain, translatePortBindings(portBindings, manifest), app));
if (addError && addError.reason === BoxError.ALREADY_EXISTS) throw getDuplicateErrorDetails(addError.message, locations, domainObjectMap, portBindings);
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-08-20 09:19:44 -07:00
const taskId = await addTask(appId, app.installationState, task);
2021-08-20 09:19:44 -07:00
const newApp = _.extend({}, _.omit(app, 'icon'), { appStoreId, manifest, location, domain, portBindings });
newApp.fqdn = dns.fqdn(newApp.location, domainObjectMap[newApp.domain]);
newApp.alternateDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
newApp.aliasDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[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 });
}
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) {
2021-08-20 09:19:44 -07:00
if (!validator.isBase64(icon)) throw new BoxError(BoxError.BAD_FIELD, 'icon is not base64', { field: 'icon' });
2021-04-30 13:18:15 -07:00
icon = Buffer.from(icon, 'base64');
2020-03-29 17:11:10 -07:00
}
2019-09-08 16:57:08 -07:00
2021-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-08-20 09:19:44 -07:00
const taskId = await addTask(appId, exports.ISTATE_PENDING_RESIZE, task);
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-08-20 09:19:44 -07:00
const taskId = await safe(addTask(appId, exports.ISTATE_PENDING_RESIZE, task));
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-08-20 09:19:44 -07:00
const [taskError, taskId] = await safe(addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task));
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-08-20 09:19:44 -07:00
const taskId = await addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task);
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-08-20 09:19:44 -07:00
const taskId = await addTask(appId, exports.ISTATE_PENDING_DEBUG, task);
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-03-16 22:38:59 -07:00
const { enable, mailboxDomain } = data;
let mailboxName = data.mailboxName;
assert.strictEqual(typeof enable, 'boolean');
assert(mailboxName === null || typeof mailboxName === 'string');
assert.strictEqual(typeof mailboxDomain, 'string');
2020-03-29 17:11:10 -07:00
const appId = app.id;
let error = checkAppState(app, exports.ISTATE_PENDING_RECREATE_CONTAINER);
2021-08-20 09:19:44 -07:00
if (error) throw error;
2020-03-30 22:18:39 -07:00
2021-10-01 09:37:33 -07:00
if (!app.manifest.addons?.sendmail) throw new BoxError(BoxError.BAD_FIELD, 'App does not use mail addons');
2021-06-29 14:26:34 -07:00
2021-08-20 09:19:44 -07:00
await mail.getDomain(mailboxDomain); // check if domain exists
2019-09-21 19:45:55 -07:00
2021-08-20 09:19:44 -07:00
if (mailboxName) {
error = mail.validateName(mailboxName);
if (error) throw new BoxError(BoxError.BAD_FIELD, error.message, { field: 'mailboxName' });
} else {
mailboxName = mailboxNameForLocation(app.location, app.domain, app.manifest);
}
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
const task = {
args: {},
values: { enableMailbox: enable, mailboxName, mailboxDomain }
};
const taskId = await addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task);
2019-11-14 21:43:14 -08:00
2021-08-20 09:19:44 -07:00
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, mailboxName, 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
}
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');
2019-10-14 16:59:22 -07:00
reverseProxyConfig = _.extend({ robotsTxt: null, csp: null }, reverseProxyConfig);
2019-10-13 18:22:03 -07:00
2020-03-29 17:11:10 -07:00
const appId = app.id;
let error = validateCsp(reverseProxyConfig.csp);
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
2021-08-17 14:04:29 -07:00
await reverseProxy.writeAppConfig(_.extend({}, 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
}
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');
2020-03-29 17:11:10 -07:00
const appId = app.id;
const { location, 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);
2019-09-08 16:57:08 -07:00
2021-08-17 14:04:29 -07:00
if (cert && key) {
const error = reverseProxy.validateCertificate(location, domainObject, { cert, key });
if (error) throw error;
}
2019-09-08 16:57:08 -07:00
2021-09-21 18:58:18 +02:00
await reverseProxy.setAppCertificate(location, domainObject, { cert, key });
2021-08-20 09:19:44 -07:00
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, cert, key });
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
2020-03-29 17:11:10 -07:00
let values = {
location: data.location.toLowerCase(),
domain: data.domain.toLowerCase(),
// these are intentionally reset, if not set
portBindings: null,
2021-01-18 17:26:26 -08:00
alternateDomains: [],
aliasDomains: []
2020-03-29 17:11:10 -07:00
};
2019-09-21 19:45:55 -07:00
2020-03-29 17:11:10 -07:00
if ('portBindings' in data) {
error = validatePortBindings(data.portBindings, app.manifest);
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
values.portBindings = translatePortBindings(data.portBindings || null, app.manifest);
}
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')) {
2020-03-30 22:18:39 -07:00
values.mailboxName = mailboxNameForLocation(values.location, app.manifest);
values.mailboxDomain = values.domain;
}
2019-09-08 16:57:08 -07:00
2020-03-29 17:11:10 -07:00
if ('alternateDomains' in data) {
values.alternateDomains = data.alternateDomains;
}
2019-09-27 10:25:26 -07:00
2021-01-18 17:26:26 -08:00
if ('aliasDomains' in data) {
values.aliasDomains = data.aliasDomains;
}
2021-01-18 22:47:53 -08:00
const locations = [{ subdomain: values.location, domain: values.domain, type: 'primary' }]
.concat(values.alternateDomains.map(ad => _.extend(ad, { type: 'redirect' })))
.concat(values.aliasDomains.map(ad => _.extend(ad, { type: 'alias' })));
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
const domainObjectMap = await validateLocations(locations);
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
const task = {
args: {
oldConfig: _.pick(app, 'location', 'domain', 'fqdn', 'alternateDomains', 'aliasDomains', 'portBindings'),
skipDnsSetup: !!data.skipDnsSetup,
overwriteDns: !!data.overwriteDns
},
values
};
let [taskError, taskId] = await safe(addTask(appId, exports.ISTATE_PENDING_LOCATION_CHANGE, task));
if (taskError && taskError.reason === BoxError.ALREADY_EXISTS) taskError = getDuplicateErrorDetails(error.message, locations, domainObjectMap, data.portBindings);
if (taskError) throw taskError;
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
values.fqdn = dns.fqdn(values.location, domainObjectMap[values.domain]);
values.alternateDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
values.aliasDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
2021-08-20 09:19:44 -07:00
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, _.extend({ 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
}
2021-08-20 09:19:44 -07:00
async function setDataDir(app, dataDir, auditSource) {
2020-03-29 17:11:10 -07:00
assert.strictEqual(typeof app, 'object');
2019-09-09 16:37:59 -07:00
assert(dataDir === null || typeof dataDir === 'string');
2019-09-08 16:57:08 -07:00
assert.strictEqual(typeof auditSource, 'object');
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
2020-03-29 17:11:10 -07:00
error = validateDataDir(dataDir);
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: { newDataDir: dataDir },
values: {}
2020-03-29 17:11:10 -07:00
};
2021-08-20 09:19:44 -07:00
const taskId = await addTask(appId, exports.ISTATE_PENDING_DATA_DIR_MIGRATION, task);
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, dataDir, 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);
2020-03-29 17:11:10 -07:00
error = checkManifestConstraints(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) {
2021-08-20 09:19:44 -07:00
if (!validator.isBase64(data.icon)) throw new BoxError(BoxError.BAD_FIELD, 'icon is not base64', { field: 'icon' });
2021-04-30 13:18:15 -07:00
data.icon = Buffer.from(data.icon, 'base64');
}
2021-04-30 13:18:15 -07:00
values.icon = data.icon;
2020-03-29 17:11:10 -07:00
}
2016-06-04 19:19:00 -07:00
2020-03-29 17:11:10 -07:00
// do not update apps in debug mode
2021-08-20 09:19:44 -07:00
if (app.debugMode && !data.force) throw new BoxError(BoxError.BAD_STATE, 'debug mode enabled. force to override');
2017-01-19 11:20:24 -08:00
2020-03-29 17:11:10 -07:00
// Ensure we update the memory limit in case the new app requires more memory as a minimum
// 0 and -1 are special updateConfig for memory limit indicating unset and unlimited
if (app.memoryLimit > 0 && updateConfig.manifest.memoryLimit && app.memoryLimit < updateConfig.manifest.memoryLimit) {
updateConfig.memoryLimit = updateConfig.manifest.memoryLimit;
}
2016-02-14 12:10:22 +01:00
2021-10-01 09:37:33 -07:00
if (!app.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 restore added the addon
values.mailboxName = mailboxNameForLocation(app.location, manifest);
values.mailboxDomain = app.domain;
}
2020-03-29 17:11:10 -07:00
const task = {
args: { updateConfig },
values
2020-03-29 17:11:10 -07:00
};
2021-08-20 09:19:44 -07:00
const taskId = await addTask(appId, exports.ISTATE_PENDING_UPDATE, task);
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 };
}
2020-10-06 17:53:04 +02:00
function getLocalLogfilePaths(app) {
assert.strictEqual(typeof app, 'object');
const appId = app.id;
var filePaths = [];
filePaths.push(path.join(paths.LOG_DIR, appId, 'apptask.log'));
filePaths.push(path.join(paths.LOG_DIR, appId, 'app.log'));
if (app.manifest.addons && app.manifest.addons.redis) filePaths.push(path.join(paths.LOG_DIR, `redis-${appId}/app.log`));
return filePaths;
}
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
2021-10-01 09:23:20 -07:00
const lines = options.lines === -1 ? '+1' : options.lines,
2020-03-29 17:11:10 -07:00
format = options.format || 'json',
follow = options.follow;
2017-04-18 20:32:57 -07:00
2020-03-29 17:11:10 -07:00
assert.strictEqual(typeof format, 'string');
2018-06-11 20:09:38 +02:00
2021-10-01 09:23:20 -07:00
const args = [ '--lines=' + lines ];
2020-03-29 17:11:10 -07:00
if (follow) args.push('--follow', '--retry', '--quiet'); // same as -F. to make it work if file doesn't exist, --quiet to not output file headers, which are no logs
2021-10-01 09:23:20 -07:00
const cp = spawn('/usr/bin/tail', args.concat(getLocalLogfilePaths(app)));
2021-10-01 09:23:20 -07:00
const transformStream = split(function mapper(line) {
2020-03-29 17:11:10 -07:00
if (format !== 'json') return line + '\n';
2018-06-04 21:24:02 +02:00
2020-03-29 17:11:10 -07:00
var data = line.split(' '); // logs are <ISOtimestamp> <msg>
var timestamp = (new Date(data[0])).getTime();
if (isNaN(timestamp)) timestamp = 0;
var message = line.slice(data[0].length+1);
2018-06-14 12:21:43 +02:00
2020-03-29 17:11:10 -07:00
// ignore faulty empty logs
if (!timestamp && !message) return;
2020-03-29 17:11:10 -07:00
return JSON.stringify({
realtimeTimestamp: timestamp * 1000,
message: message,
source: appId
}) + '\n';
});
2020-03-29 17:11:10 -07:00
transformStream.close = cp.kill.bind(cp, 'SIGKILL'); // closing stream kills the child process
2020-03-29 17:11:10 -07:00
cp.stdout.pipe(transformStream);
2021-10-01 09:23:20 -07:00
return transformStream;
}
2021-05-07 21:37:23 -07:00
async function getCertificate(subdomain, domain) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domain, 'string');
const result = await database.query('SELECT certificateJson FROM subdomains WHERE subdomain=? AND domain=?', [ subdomain, domain ]);
if (result.length === 0) return null;
return JSON.parse(result[0].certificateJson);
}
2019-11-23 18:35:51 -08:00
// does a re-configure when called from most states. for install/clone errors, it re-installs with an optional manifest
// 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
2020-03-29 17:11:10 -07:00
error = checkManifestConstraints(data.manifest);
2021-08-20 09:19:44 -07:00
if (error) throw error;
2019-09-21 19:45:55 -07:00
2020-03-31 15:44:46 -07:00
if (!hasMailAddon(data.manifest)) { // clear if repair removed addon
task.values.mailboxName = task.values.mailboxDomain = null;
} else if (!app.mailboxName || app.mailboxName.endsWith('.app')) { // allocate since repair added the addon
task.values.mailboxName = mailboxNameForLocation(app.location, data.manifest);
task.values.mailboxDomain = app.domain;
}
2020-03-29 17:11:10 -07:00
task.values.manifest = data.manifest;
task.args.oldManifest = app.manifest;
}
} else {
errorState = exports.ISTATE_PENDING_CONFIGURE;
if (data.dockerImage) {
let newManifest = _.extend({}, app.manifest, { dockerImage: data.dockerImage });
task.values.manifest = newManifest;
2019-11-23 18:35:51 -08:00
}
2020-03-29 17:11:10 -07:00
}
2019-09-21 19:45:55 -07:00
2021-08-20 09:19:44 -07:00
const taskId = await addTask(appId, errorState, task);
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 };
2019-09-21 19:45:55 -07:00
2021-07-14 11:07:19 -07:00
if (!backupInfo.manifest) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Could not get restore manifest');
if (backupInfo.encryptionVersion === 1) throw new BoxError(BoxError.BAD_FIELD, 'This encrypted backup was created with an older Cloudron version and has to be restored using the CLI tool');
2021-07-14 11:07:19 -07:00
// re-validate because this new box version may not accept old configs
error = checkManifestConstraints(backupInfo.manifest);
if (error) throw error;
2016-06-13 13:44:49 -07:00
2021-07-14 11:07:19 -07:00
let values = { manifest: backupInfo.manifest };
2021-10-01 09:37:33 -07:00
if (!backupInfo.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
values.mailboxName = mailboxNameForLocation(app.location, backupInfo.manifest);
values.mailboxDomain = app.domain;
}
2016-06-13 18:11:11 -07:00
2021-07-14 11:07:19 -07:00
const restoreConfig = { backupId, backupFormat: backupInfo.format };
2020-03-31 15:44:46 -07:00
2021-07-14 11:07:19 -07:00
const task = {
args: {
restoreConfig,
oldManifest: app.manifest,
skipDnsSetup: !!backupId, // if this is a restore, just skip dns setup. only re-installs should setup dns
overwriteDns: true
},
values
};
2021-08-20 09:19:44 -07:00
const taskId = await addTask(appId, exports.ISTATE_PENDING_RESTORE, task);
2021-08-20 09:19:44 -07:00
await eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { app: app, backupId: backupInfo.id, fromManifest: app.manifest, toManifest: backupInfo.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;
2020-03-29 17:11:10 -07:00
// all fields are optional
data.backupId = data.backupId || null;
data.backupFormat = data.backupFormat || null;
data.backupConfig = data.backupConfig || null;
const { backupId, backupFormat, backupConfig } = data;
2020-03-29 17:11:10 -07:00
let error = backupFormat ? validateBackupFormat(backupFormat) : null;
2021-08-20 09:19:44 -07:00
if (error) throw error;
2021-05-26 09:27:15 -07:00
error = checkAppState(app, exports.ISTATE_PENDING_IMPORT);
2021-08-20 09:19:44 -07:00
if (error) throw error;
2020-03-29 17:11:10 -07:00
2021-08-20 09:19:44 -07:00
// TODO: make this smarter to do a read-only test and check if the file exists in the storage backend
if (backupConfig) {
if (mounts.isMountProvider(backupConfig.provider)) {
backupConfig.mountPoint = `/mnt/appimport-${app.id}`;
error = mounts.validateMountOptions(backupConfig.provider, backupConfig.mountOptions);
if (error) throw error;
const mountObject = { // keep this in sync with the import code in apptask
name: `appimport-${app.id}`,
hostPath: backupConfig.mountPoint,
mountType: backupConfig.provider,
mountOptions: backupConfig.mountOptions
};
await mounts.tryAddMount(mountObject, { timeout: 10 });
}
error = await backups.testProviderConfig(backupConfig);
if (error) throw error;
}
2021-08-20 09:19:44 -07:00
if (backupConfig) {
if ('password' in backupConfig) {
backupConfig.encryption = backups.generateEncryptionKeysSync(backupConfig.password);
delete backupConfig.password;
} else {
backupConfig.encryption = null;
}
2021-08-20 09:19:44 -07:00
}
2021-08-20 09:19:44 -07:00
const restoreConfig = { backupId, backupFormat, backupConfig };
2021-08-20 09:19:44 -07:00
const task = {
args: {
restoreConfig,
oldManifest: app.manifest,
skipDnsSetup: false,
overwriteDns: true
},
values: {}
};
const taskId = await addTask(appId, exports.ISTATE_PENDING_IMPORT, task);
2021-08-20 09:19:44 -07:00
await eventlog.add(eventlog.ACTION_APP_IMPORT, auditSource, { app: app, backupId, 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-08-20 09:19:44 -07:00
const taskId = await addTask(appId, exports.ISTATE_PENDING_BACKUP, task);
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');
2021-05-25 21:31:48 -07:00
const location = data.location.toLowerCase(),
2017-11-02 22:17:44 +01:00
domain = data.domain.toLowerCase(),
2016-06-17 17:12:55 -05:00
portBindings = data.portBindings || null,
2018-05-13 21:02:57 -07:00
backupId = data.backupId,
2020-03-29 17:11:10 -07:00
overwriteDns = 'overwriteDns' in data ? data.overwriteDns : false,
skipDnsSetup = 'skipDnsSetup' in data ? data.skipDnsSetup : false,
2020-03-29 17:11:10 -07:00
appId = app.id;
2016-06-17 17:12:55 -05:00
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof location, 'string');
2017-11-02 22:17:44 +01:00
assert.strictEqual(typeof domain, 'string');
2016-06-17 17:12:55 -05:00
assert.strictEqual(typeof portBindings, 'object');
2021-07-14 11:07:19 -07:00
const locations = [{ subdomain: location, domain, type: 'primary' }];
2021-08-20 09:19:44 -07:00
const domainObjectMap = await validateLocations(locations);
const backupInfo = await backups.get(backupId);
2016-06-17 17:12:55 -05:00
2021-08-20 09:19:44 -07:00
if (!backupInfo.manifest) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Could not get restore config');
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
2021-08-20 09:19:44 -07:00
// re-validate because this new box version may not accept old configs
let error = checkManifestConstraints(manifest);
if (error) throw error;
2016-06-17 17:12:55 -05:00
2021-08-20 09:19:44 -07:00
error = validatePortBindings(portBindings, manifest);
if (error) throw error;
2021-08-20 09:19:44 -07:00
// should we copy the original app's mailbox settings instead?
2021-10-01 09:37:33 -07:00
const mailboxName = manifest.addons?.sendmail ? mailboxNameForLocation(location, manifest) : null;
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 obj = {
installationState: exports.ISTATE_PENDING_CLONE,
runState: exports.RSTATE_RUNNING,
memoryLimit: app.memoryLimit,
cpuShares: app.cpuShares,
accessRestriction: app.accessRestriction,
sso: !!app.sso,
mailboxName: mailboxName,
mailboxDomain: mailboxDomain,
enableBackup: app.enableBackup,
reverseProxyConfig: app.reverseProxyConfig,
env: app.env,
alternateDomains: [],
aliasDomains: [],
servicesConfig: app.servicesConfig,
label: app.label ? `${app.label}-clone` : '',
tags: app.tags,
enableAutomaticUpdate: app.enableAutomaticUpdate,
icon: icons.icon,
enableMailbox: app.enableMailbox
};
2016-06-17 17:12:55 -05:00
2021-08-20 09:19:44 -07:00
const [addError] = await safe(add(newAppId, appStoreId, manifest, location, domain, translatePortBindings(portBindings, manifest), obj));
if (addError && addError.reason === BoxError.ALREADY_EXISTS) throw getDuplicateErrorDetails(addError.message, locations, domainObjectMap, portBindings);
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
2021-09-30 10:45:25 -07:00
const restoreConfig = { backupId, 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
};
const taskId = await addTask(newAppId, exports.ISTATE_PENDING_CLONE, task);
2018-01-11 10:59:30 -08:00
2021-08-20 09:19:44 -07:00
const newApp = _.extend({}, _.omit(obj, 'icon'), { appStoreId, manifest, location, domain, portBindings });
newApp.fqdn = dns.fqdn(newApp.location, domainObjectMap[newApp.domain]);
newApp.alternateDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
newApp.aliasDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
2021-01-18 17:26:26 -08:00
2021-09-30 10:45:25 -07:00
await eventlog.add(eventlog.ACTION_APP_CLONE, auditSource, { appId: newAppId, oldAppId: appId, backupId, 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
};
const taskId = await addTask(appId, exports.ISTATE_PENDING_UNINSTALL, task);
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-08-20 09:19:44 -07:00
const taskId = await addTask(appId, exports.ISTATE_PENDING_START, task);
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-08-20 09:19:44 -07:00
const taskId = await addTask(appId, exports.ISTATE_PENDING_STOP, task);
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-08-20 09:19:44 -07:00
const taskId = await addTask(appId, exports.ISTATE_PENDING_RESTART, task);
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
}
function checkManifestConstraints(manifest) {
2016-06-13 18:02:57 -07:00
assert(manifest && typeof manifest === 'object');
2019-10-24 10:39:47 -07:00
if (manifest.manifestVersion > 2) return new BoxError(BoxError.BAD_FIELD, 'Manifest version must be <= 2');
2019-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');
}
return null;
}
2021-08-25 19:41:46 -07:00
async function exec(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
2021-08-25 19:41:46 -07:00
const execOptions = {
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,
Cmd: cmd
};
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
};
2021-08-25 19:41:46 -07:00
const stream = await docker.execContainer(app.containerId, { execOptions, startOptions, rows: options.rows, columns: options.columns });
return stream;
}
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`);
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-08-20 09:19:44 -07:00
const taskId = await addTask(appId, exports.ISTATE_PENDING_BACKUP, task);
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
2021-08-20 09:19:44 -07:00
async function restoreInstalledApps(options) {
2021-02-24 16:29:43 -08:00
assert.strictEqual(typeof options, 'object');
2016-05-24 10:33:10 -07:00
2021-08-20 09:19:44 -07:00
let apps = await list();
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) {
const [error, results] = await safe(backups.getByIdentifierAndStatePaged(app.id, backups.BACKUP_STATE_NORMAL, 1, 1));
let installationState, restoreConfig, oldManifest;
if (!error && results.length) {
installationState = exports.ISTATE_PENDING_RESTORE;
restoreConfig = { backupId: results[0].id, backupFormat: results[0].format };
oldManifest = app.manifest;
} else {
installationState = exports.ISTATE_PENDING_INSTALL;
restoreConfig = null;
oldManifest = null;
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
2021-08-20 09:19:44 -07:00
debug(`restoreInstalledApps: marking ${app.fqdn} for restore using restore config ${JSON.stringify(restoreConfig)}`);
2019-10-24 10:39:47 -07:00
2021-08-20 09:19:44 -07:00
const [addTaskError, taskId] = await safe(addTask(app.id, installationState, task));
if (addTaskError) debug(`restoreInstalledApps: error marking ${app.fqdn} for restore: ${JSON.stringify(addTaskError)}`);
else debug(`restoreInstalledApps: marked ${app.id} for restore with taskId ${taskId}`);
}
}
2016-05-24 10:33:10 -07:00
2021-08-20 09:19:44 -07:00
async function configureInstalledApps() {
let apps = await list();
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
2021-08-20 09:19:44 -07:00
for (const app of apps) {
debug(`configureInstalledApps: marking ${app.fqdn} for reconfigure`);
2021-08-20 09:19:44 -07:00
const task = {
args: {},
values: {},
scheduleNow: false, // task will be scheduled by autoRestartTasks when platform is ready
requireNullTaskId: false // ignore existing stale taskId
};
2019-09-24 20:29:01 -07:00
2021-08-20 09:19:44 -07:00
const [addTaskError, taskId] = await safe(addTask(app.id, exports.ISTATE_PENDING_CONFIGURE, task));
if (addTaskError) debug(`configureInstalledApps: error marking ${app.fqdn} for configure: ${JSON.stringify(addTaskError)}`);
else debug(`configureInstalledApps: marked ${app.id} for re-configure with taskId ${taskId}`);
}
2016-05-24 10:33:10 -07:00
}
2017-08-18 20:45:52 -07:00
2021-08-20 09:19:44 -07:00
async function restartAppsUsingAddons(changedAddons) {
2020-05-22 16:43:16 -07:00
assert(Array.isArray(changedAddons));
2021-08-20 09:19:44 -07:00
let apps = await list();
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-08-20 09:19:44 -07:00
const [addTaskError, taskId] = await safe(addTask(app.id, exports.ISTATE_PENDING_RESTART, task));
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-08-20 09:19:44 -07:00
async function schedulePendingTasks() {
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), { 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);
}
2020-03-29 17:11:10 -07:00
function downloadFile(app, filePath, callback) {
assert.strictEqual(typeof app, 'object');
2017-08-18 20:45:52 -07:00
assert.strictEqual(typeof filePath, 'string');
assert.strictEqual(typeof callback, 'function');
2020-03-29 17:11:10 -07:00
exec(app, { cmd: [ 'stat', '--printf=%F-%s', filePath ], tty: true }, function (error, stream) {
2017-08-18 20:45:52 -07:00
if (error) return callback(error);
var data = '';
stream.setEncoding('utf8');
stream.on('data', function (d) { data += d; });
stream.on('end', function () {
var parts = data.split('-');
2019-10-24 10:39:47 -07:00
if (parts.length !== 2) return callback(new BoxError(BoxError.NOT_FOUND, 'file does not exist'));
2017-08-20 18:44:26 -07:00
var type = parts[0], filename, cmd, size;
if (type === 'regular file') {
cmd = [ 'cat', filePath ];
size = parseInt(parts[1], 10);
filename = path.basename(filePath);
2019-10-24 10:39:47 -07:00
if (isNaN(size)) return callback(new BoxError(BoxError.NOT_FOUND, 'file does not exist'));
2017-08-20 18:44:26 -07:00
} else if (type === 'directory') {
cmd = ['tar', 'zcf', '-', '-C', filePath, '.'];
filename = path.basename(filePath) + '.tar.gz';
size = 0; // unknown
} else {
2019-10-24 10:39:47 -07:00
return callback(new BoxError(BoxError.NOT_FOUND, 'only files or dirs can be downloaded'));
2017-08-20 18:44:26 -07:00
}
2020-03-29 17:11:10 -07:00
exec(app, { cmd: cmd , tty: false }, function (error, stream) {
if (error) return callback(error);
2017-08-20 23:39:49 -07:00
var stdoutStream = new TransformStream({
transform: function (chunk, ignoredEncoding, callback) {
this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk;
2019-06-11 12:32:15 -07:00
for (;;) {
2017-08-20 23:39:49 -07:00
if (this._buffer.length < 8) break; // header is 8 bytes
var type = this._buffer.readUInt8(0);
var len = this._buffer.readUInt32BE(4);
if (this._buffer.length < (8 + len)) break; // not enough
var payload = this._buffer.slice(8, 8 + len);
this._buffer = this._buffer.slice(8+len); // consumed
if (type === 1) this.push(payload);
}
callback();
}
});
stream.pipe(stdoutStream);
2020-03-29 17:11:10 -07:00
callback(null, stdoutStream, { filename: filename, size: size });
});
});
2017-08-18 20:45:52 -07:00
});
}
2020-03-29 17:11:10 -07:00
function uploadFile(app, sourceFilePath, destFilePath, callback) {
assert.strictEqual(typeof app, 'object');
2017-08-18 20:45:52 -07:00
assert.strictEqual(typeof sourceFilePath, 'string');
assert.strictEqual(typeof destFilePath, 'string');
assert.strictEqual(typeof callback, 'function');
2019-03-04 12:28:27 -08:00
const done = once(function (error) {
safe.fs.unlinkSync(sourceFilePath); // remove file in /tmp
2019-12-09 15:02:51 -08:00
if (error) return callback(new BoxError(BoxError.FS_ERROR, error.message)); // blame it on filesystem for now
callback(null);
2019-03-04 12:28:27 -08:00
});
// the built-in bash printf understands "%q" but not /usr/bin/printf.
// ' gets replaced with '\'' . the first closes the quote and last one starts a new one
const escapedDestFilePath = safe.child_process.execSync(`printf %q '${destFilePath.replace(/'/g, '\'\\\'\'')}'`, { shell: '/bin/bash', encoding: 'utf8' });
debug(`uploadFile: ${sourceFilePath} -> ${escapedDestFilePath}`);
2020-03-29 17:11:10 -07:00
exec(app, { cmd: [ 'bash', '-c', `cat - > ${escapedDestFilePath}` ], tty: false }, function (error, stream) {
2019-03-04 12:28:27 -08:00
if (error) return done(error);
2017-08-18 20:45:52 -07:00
var readFile = fs.createReadStream(sourceFilePath);
2019-03-04 12:28:27 -08:00
readFile.on('error', done);
2017-08-18 20:45:52 -07:00
2019-03-04 12:28:27 -08:00
stream.on('error', done);
stream.on('finish', done);
2017-08-18 20:45:52 -07:00
2019-03-04 12:28:27 -08:00
readFile.pipe(stream);
2017-08-18 20:45:52 -07:00
});
}
2021-05-25 21:31:48 -07:00
2021-08-20 09:19:44 -07:00
async function backupConfig(app) {
2021-05-25 21:31:48 -07:00
assert.strictEqual(typeof app, 'object');
if (!safe.fs.writeFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/config.json'), JSON.stringify(app))) {
2021-08-20 09:19:44 -07:00
throw new BoxError(BoxError.FS_ERROR, 'Error creating config.json: ' + safe.error.message);
2021-05-25 21:31:48 -07:00
}
2021-08-20 09:19:44 -07:00
const [error, icons] = await safe(getIcons(app.id));
if (!error && icons.icon) safe.fs.writeFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/icon.png'), icons.icon);
2021-05-25 21:31:48 -07:00
}
2021-08-20 09:19:44 -07:00
async function restoreConfig(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);
}