Files
cloudron-box/src/apps.js

2576 lines
102 KiB
JavaScript
Raw Normal View History

'use strict';
exports = module.exports = {
2021-09-21 10:00:47 -07:00
canAccess,
isOperator,
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,
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,
setOperators,
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,
setInbox,
2020-10-27 17:11:50 -07:00
setLocation,
setDataDir,
repair,
restore,
importApp,
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,
getIcon,
2021-01-20 12:12:14 -08:00
getMemoryLimit,
getLimits,
getSchedulerConfig,
2020-10-27 17:11:50 -07:00
listEventlog,
2020-10-27 17:11:50 -07:00
downloadFile,
uploadFile,
backupConfig,
restoreConfig,
PORT_TYPE_TCP: 'tcp',
PORT_TYPE_UDP: 'udp',
// 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
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
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
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,
_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'),
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'),
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'),
mail = require('./mail.js'),
manifestFormat = require('cloudron-manifestformat'),
mounts = require('./mounts.js'),
once = require('once'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
semver = require('semver'),
services = require('./services.js'),
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'),
uuid = require('uuid'),
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',
'apps.label', 'apps.tagsJson', 'apps.taskId', 'apps.reverseProxyConfigJson', 'apps.servicesConfigJson', 'apps.operatorsJson',
'apps.sso', 'apps.debugModeJson', 'apps.enableBackup', 'apps.proxyAuth', 'apps.containerIp', 'apps.crontab',
'apps.creationTime', 'apps.updateTime', 'apps.enableAutomaticUpdate',
'apps.enableMailbox', 'apps.mailboxName', 'apps.mailboxDomain', 'apps.enableInbox', 'apps.inboxName', 'apps.inboxDomain',
2021-08-20 09:19:44 -07:00
'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(',');
// validate the port bindings
function validatePortBindings(portBindings, manifest) {
2017-01-29 13:01:09 -08:00
assert.strictEqual(typeof portBindings, 'object');
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) */
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) */
3478, /* turn,stun */
2016-05-13 18:48:05 -07:00
4190, /* managesieve */
5349, /* turn,stun TLS */
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
const tcpPorts = manifest.tcpPorts || { };
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;
}
function translatePortBindings(portBindings, manifest) {
assert.strictEqual(typeof portBindings, 'object');
assert.strictEqual(typeof manifest, 'object');
if (!portBindings) return null;
let result = {};
const tcpPorts = manifest.tcpPorts || { };
for (let portName in portBindings) {
const portType = portName in tcpPorts ? exports.PORT_TYPE_TCP : exports.PORT_TYPE_UDP;
result[portName] = { hostPort: portBindings[portName], type: portType };
}
return result;
}
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);
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
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;
}
// 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
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');
}
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');
}
// 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');
var min = manifest.memoryLimit || constants.DEFAULT_MEMORY_LIMIT;
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;
}
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
return null;
}
function validateCsp(csp) {
if (csp === null) return null;
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-24 10:39:47 -07:00
if (csp.includes('"')) return new BoxError(BoxError.BAD_FIELD, 'CSP cannot contains double quotes', { field: 'csp' });
return null;
}
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-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;
}
function validateDataDir(dataDir) {
2019-09-09 16:37:59 -07:00
if (dataDir === null) return null;
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' });
// 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' });
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' });
}
// 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' });
// 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' });
return null;
}
function getDuplicateErrorDetails(errorMessage, locations, domainObjectMap, portBindings) {
assert.strictEqual(typeof errorMessage, 'string');
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);
return new BoxError(BoxError.DATABASE_ERROR, errorMessage);
}
// check if a location conflicts
2019-03-19 20:43:42 -07:00
if (match[2] === 'subdomain') {
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) {
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`);
}
function getDataDir(app, dataDir) {
2019-09-15 21:51:38 -07:00
assert(dataDir === null || typeof dataDir === 'string');
return dataDir || path.join(paths.APPS_DATA_DIR, app.id, 'data');
}
2018-06-25 16:40:16 -07:00
function removeInternalFields(app) {
return _.pick(app,
2019-08-30 11:18:42 -07:00
'id', 'appStoreId', 'installationState', 'error', 'runState', 'health', 'taskId',
'location', 'domain', 'fqdn', 'crontab',
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'cpuShares', 'operators',
'sso', 'debugMode', 'reverseProxyConfig', 'enableBackup', 'creationTime', 'updateTime', 'ts', 'tags',
'label', 'alternateDomains', 'aliasDomains', 'env', 'enableAutomaticUpdate', 'dataDir', 'mounts',
'enableMailbox', 'mailboxName', 'mailboxDomain', 'enableInbox', 'inboxName', 'inboxDomain');
}
2019-02-11 14:37:49 -08:00
// non-admins can only see these
function removeRestrictedFields(app) {
return _.pick(app,
'id', 'appStoreId', 'installationState', 'error', 'runState', 'health', 'taskId', 'accessRestriction', 'alternateDomains', 'aliasDomains', 'sso',
'location', 'domain', 'fqdn', 'manifest', 'portBindings', 'iconUrl', 'creationTime', 'ts', 'tags', 'label', 'enableBackup');
}
2021-08-20 09:19:44 -07:00
async function getIcon(app, options) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
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');
2021-08-20 09:19:44 -07:00
if (!options.original && icons.icon) return icons.icon;
if (icons.appStoreIcon) return icons.appStoreIcon;
2021-08-20 09:19:44 -07:00
return null;
}
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;
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.enableInbox = !!result.enableInbox;
2021-08-20 09:19:44 -07:00
result.proxyAuth = !!result.proxyAuth;
result.hasIcon = !!result.hasIcon;
result.hasAppStoreIcon = !!result.hasAppStoreIcon;
assert(result.debugModeJson === null || typeof result.debugModeJson === 'string');
result.debugMode = safe.JSON.parse(result.debugModeJson);
delete result.debugModeJson;
assert(result.servicesConfigJson === null || typeof result.servicesConfigJson === 'string');
result.servicesConfig = safe.JSON.parse(result.servicesConfigJson) || {};
delete result.servicesConfigJson;
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;
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
}
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');
if (!app.operators) return isAdmin(user);
2016-02-09 12:48:21 -08: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;
return isAdmin(user);
}
function canAccess(app, user) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof user, 'object');
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;
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),
accessRestriction = data.accessRestriction || null,
accessRestrictionJson = JSON.stringify(accessRestriction),
memoryLimit = data.memoryLimit || 0,
cpuShares = data.cpuShares || 512,
installationState = data.installationState,
runState = data.runState,
sso = 'sso' in data ? data.sso : null,
debugModeJson = data.debugMode ? JSON.stringify(data.debugMode) : null,
env = data.env || {},
label = data.label || null,
tagsJson = data.tags ? JSON.stringify(data.tags) : null,
mailboxName = data.mailboxName || null,
mailboxDomain = data.mailboxDomain || null,
reverseProxyConfigJson = data.reverseProxyConfig ? JSON.stringify(data.reverseProxyConfig) : null,
servicesConfigJson = data.servicesConfig ? JSON.stringify(data.servicesConfig) : null,
enableMailbox = data.enableMailbox || false,
icon = data.icon || null;
2021-08-20 09:19:44 -07:00
const queries = [];
2021-08-20 09:19:44 -07:00
queries.push({
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, cpuShares, '
+ 'sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson, servicesConfigJson, icon, '
+ 'enableMailbox) '
2021-08-20 09:19:44 -07:00
+ ' 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 = [ ];
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');
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 ] }
];
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');
}
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
}
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');
}
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');
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;
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;
}
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');
assert.strictEqual(typeof installationState, 'string');
assert.strictEqual(typeof task, 'object'); // { args, values }
2019-08-27 16:12:24 -07:00
const { args, values } = task;
// 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 ]);
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
}
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-23 10:35:42 -07:00
if (app.installationState === exports.ISTATE_ERROR) {
// allow task to be called again if that was the errored task
if (app.error.installationState === state) return null;
// allow uninstall from any state
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
}
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
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');
}
return null;
}
2021-08-20 09:19:44 -07:00
async function validateLocations(locations) {
assert(Array.isArray(locations));
2021-08-20 09:19:44 -07:00
const domainObjectMap = await getDomainObjectMap();
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 *.
}
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;
}
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,
enableBackup = 'enableBackup' in data ? data.enableBackup : true,
enableAutomaticUpdate = 'enableAutomaticUpdate' in data ? data.enableAutomaticUpdate : true,
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,
tags = data.tags || [],
overwriteDns = 'overwriteDns' in data ? data.overwriteDns : false,
skipDnsSetup = 'skipDnsSetup' in data ? data.skipDnsSetup : false,
appStoreId = data.appStoreId,
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 enableMailbox = 'enableMailbox' in data ? data.enableMailbox : true;
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-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' });
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) {
assert.strictEqual(typeof app, 'object');
2019-09-08 16:57:08 -07:00
assert.strictEqual(typeof accessRestriction, 'object');
assert.strictEqual(typeof auditSource, 'object');
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
}
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;
const error = validateAccessRestriction(operators); // not a typo. same structure for operators and accessRestriction
if (error) throw error;
await update(appId, { operators });
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, operators });
}
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) {
assert.strictEqual(typeof app, 'object');
2019-09-08 16:57:08 -07:00
assert.strictEqual(typeof label, 'string');
assert.strictEqual(typeof auditSource, 'object');
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) {
assert.strictEqual(typeof app, 'object');
2019-09-08 16:57:08 -07:00
assert(Array.isArray(tags));
assert.strictEqual(typeof auditSource, 'object');
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) {
assert.strictEqual(typeof app, 'object');
2019-09-08 16:57:08 -07:00
assert(icon === null || typeof icon === 'string');
assert.strictEqual(typeof auditSource, 'object');
const appId = app.id;
2019-09-08 16:57:08 -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' });
icon = Buffer.from(icon, 'base64');
}
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) {
assert.strictEqual(typeof app, 'object');
2019-09-08 16:57:08 -07:00
assert.strictEqual(typeof memoryLimit, 'number');
assert.strictEqual(typeof auditSource, 'object');
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
error = validateMemoryLimit(app.manifest, memoryLimit);
2021-08-20 09:19:44 -07:00
if (error) throw error;
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) {
assert.strictEqual(typeof app, 'object');
2020-01-28 21:30:35 -08:00
assert.strictEqual(typeof cpuShares, 'number');
assert.strictEqual(typeof auditSource, 'object');
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
error = validateCpuShares(cpuShares);
2021-08-20 09:19:44 -07:00
if (error) throw error;
2020-01-28 21:30:35 -08: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) {
assert.strictEqual(typeof app, 'object');
2019-09-08 16:57:08 -07:00
assert.strictEqual(typeof env, 'object');
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;
2019-09-08 16:57:08 -07:00
error = validateEnv(env);
2021-08-20 09:19:44 -07:00
if (error) throw error;
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) {
assert.strictEqual(typeof app, 'object');
2019-09-08 16:57:08 -07:00
assert.strictEqual(typeof debugMode, 'object');
assert.strictEqual(typeof auditSource, 'object');
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
error = validateDebugMode(debugMode);
2021-08-20 09:19:44 -07:00
if (error) throw error;
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) {
assert.strictEqual(typeof app, 'object');
2021-03-16 22:38:59 -07:00
assert.strictEqual(typeof data, 'object');
2019-09-08 16:57:08 -07:00
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof data.enable, 'boolean');
const enableMailbox = data.enable;
2021-03-16 22:38:59 -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;
if (!app.manifest.addons?.sendmail) throw new BoxError(BoxError.BAD_FIELD, 'App does not use sendmail');
const optional = 'optional' in app.manifest.addons.sendmail ? app.manifest.addons.sendmail.optional : false;
if (!optional && !enableMailbox) throw new BoxError(BoxError.BAD_FIELD, 'App requires sendmail to be enabled');
2021-06-29 14:26:34 -07:00
let mailboxName = data.mailboxName || null;
const mailboxDomain = data.mailboxDomain || null;
if (enableMailbox) {
await mail.getDomain(mailboxDomain); // check if domain exists
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);
}
}
const task = {
args: {},
values: { enableMailbox, mailboxName, mailboxDomain }
};
const taskId = await addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task);
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, mailboxName, mailboxDomain, taskId });
return { taskId };
}
async function setInbox(app, data, auditSource) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof data.enable, 'boolean');
const enableInbox = data.enable;
const appId = app.id;
let error = checkAppState(app, exports.ISTATE_PENDING_RECREATE_CONTAINER);
if (error) throw error;
if (!app.manifest.addons?.recvmail) throw new BoxError(BoxError.BAD_FIELD, 'App does not use recvmail addon');
const inboxName = data.inboxName || null;
const inboxDomain = data.inboxDomain || null;
if (enableInbox) {
const domain = await mail.getDomain(data.inboxDomain); // check if domain exists
if (!domain.enabled) throw new BoxError(BoxError.BAD_FIELD, 'Domain does not have incoming email enabled');
error = mail.validateName(data.inboxName);
if (error) throw new BoxError(BoxError.BAD_FIELD, error.message, { field: 'inboxName' });
2021-08-20 09:19:44 -07:00
}
2019-09-08 16:57:08 -07:00
2021-08-20 09:19:44 -07:00
const task = {
args: {},
values: { enableInbox, inboxName, inboxDomain }
2021-08-20 09:19:44 -07:00
};
const taskId = await addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task);
2019-11-14 21:43:14 -08:00
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, enableInbox, inboxName, inboxDomain, taskId });
2019-11-14 21:43:14 -08:00
2021-08-20 09:19:44 -07:00
return { taskId };
2019-09-08 16:57:08 -07:00
}
2021-08-20 09:19:44 -07:00
async function setAutomaticBackup(app, enable, auditSource) {
assert.strictEqual(typeof app, 'object');
2019-09-08 16:57:08 -07:00
assert.strictEqual(typeof enable, 'boolean');
assert.strictEqual(typeof auditSource, 'object');
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) {
assert.strictEqual(typeof app, 'object');
2019-09-08 16:57:08 -07:00
assert.strictEqual(typeof enable, 'boolean');
assert.strictEqual(typeof auditSource, 'object');
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) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof reverseProxyConfig, 'object');
2019-09-08 16:57:08 -07:00
assert.strictEqual(typeof auditSource, 'object');
reverseProxyConfig = _.extend({ robotsTxt: null, csp: null }, reverseProxyConfig);
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
error = validateRobotsTxt(reverseProxyConfig.robotsTxt);
2021-08-17 14:04:29 -07:00
if (error) throw error;
2021-08-17 14:04:29 -07:00
await reverseProxy.writeAppConfig(_.extend({}, app, { reverseProxyConfig }));
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) {
assert.strictEqual(typeof app, 'object');
assert(data && typeof data === 'object');
2019-09-08 16:57:08 -07:00
assert.strictEqual(typeof auditSource, 'object');
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) {
assert.strictEqual(typeof app, 'object');
2019-09-08 16:57:08 -07:00
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof auditSource, 'object');
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
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: []
};
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
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')) {
values.mailboxName = mailboxNameForLocation(values.location, app.manifest);
values.mailboxDomain = values.domain;
}
2019-09-08 16:57:08 -07:00
if ('alternateDomains' in data) {
values.alternateDomains = data.alternateDomains;
}
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) {
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');
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
error = validateDataDir(dataDir);
2021-08-20 09:19:44 -07:00
if (error) throw error;
const task = {
args: { newDataDir: dataDir },
values: {}
};
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) {
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;
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');
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
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;
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
// 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');
}
// 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');
}
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' });
data.icon = Buffer.from(data.icon, 'base64');
}
values.icon = data.icon;
}
2016-06-04 19:19:00 -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');
// 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;
}
if (!manifest.addons?.sendmail) { // clear if the update removed addon
values.mailboxName = values.mailboxDomain = null;
} else if (!app.mailboxName || app.mailboxName.endsWith('.app')) { // allocate since update added the addon
values.mailboxName = mailboxNameForLocation(app.location, manifest);
values.mailboxDomain = app.domain;
}
const task = {
args: { updateConfig },
values
};
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 };
}
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) {
assert.strictEqual(typeof app, 'object');
assert(options && typeof options === 'object');
assert.strictEqual(typeof options.lines, 'number');
assert.strictEqual(typeof options.format, 'string');
assert.strictEqual(typeof options.follow, 'boolean');
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,
format = options.format || 'json',
follow = options.follow;
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 ];
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) {
if (format !== 'json') return line + '\n';
2018-06-04 21:24:02 +02: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
// ignore faulty empty logs
if (!timestamp && !message) return;
return JSON.stringify({
realtimeTimestamp: timestamp * 1000,
message: message,
source: appId
}) + '\n';
});
transformStream.close = cp.kill.bind(cp, 'SIGKILL'); // closing stream kills the child process
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);
}
Fix repair If a task fails, we can either: * allow other task ops to be called - we cannot do this because the ops are fine-grained. for example, a restore failure removes many things and calling set-memory or set-location in that state won't make sense. * provide a generic repair route - this allows one to override args and call the failed task again. this is what we have now but has the issue that this repair function has to know about all the other op functions. for example, for argument validation. we can do some complicated refactoring to make it work if we want. * just a generic total re-configure - this does not work because clone/restore/backup/datadir/uninstall/update failure leaves the app in a state which re-configure cannot do anything about. * allow the failed op to be called again - this seems the easiest. we just allow the route to be called again in the error state. * if we hit a state where even providing extra args, cannot get you out of this "error" state, we have to provide some repair route. for example, maybe the container disappeared by some docke error. user clicks 'repair' to recreate the container. this route does not have to take any args. The final solution is: * a failed task can be called again via the route. so we can resubmit any args and we get validation * repair route just re-configures and can be called in any state to just rebuild container. re-configure is also doing only local changes (docker, nginx) * install/clone failures are fixed using repair route. updated manifest can be passed in. * UI shows backup selector for restore failures * UI shows domain selector for change location failulre
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) {
assert.strictEqual(typeof app, 'object');
Fix repair If a task fails, we can either: * allow other task ops to be called - we cannot do this because the ops are fine-grained. for example, a restore failure removes many things and calling set-memory or set-location in that state won't make sense. * provide a generic repair route - this allows one to override args and call the failed task again. this is what we have now but has the issue that this repair function has to know about all the other op functions. for example, for argument validation. we can do some complicated refactoring to make it work if we want. * just a generic total re-configure - this does not work because clone/restore/backup/datadir/uninstall/update failure leaves the app in a state which re-configure cannot do anything about. * allow the failed op to be called again - this seems the easiest. we just allow the route to be called again in the error state. * if we hit a state where even providing extra args, cannot get you out of this "error" state, we have to provide some repair route. for example, maybe the container disappeared by some docke error. user clicks 'repair' to recreate the container. this route does not have to take any args. The final solution is: * a failed task can be called again via the route. so we can resubmit any args and we get validation * repair route just re-configures and can be called in any state to just rebuild container. re-configure is also doing only local changes (docker, nginx) * install/clone failures are fixed using repair route. updated manifest can be passed in. * UI shows backup selector for restore failures * UI shows domain selector for change location failulre
2019-11-23 18:35:51 -08:00
assert.strictEqual(typeof data, 'object'); // { manifest }
assert.strictEqual(typeof auditSource, 'object');
const appId = app.id;
let errorState = (app.error && app.error.installationState) || exports.ISTATE_PENDING_CONFIGURE;
const task = {
args: {},
values: {},
requiredState: null
};
// 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 };
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}`);
error = checkManifestConstraints(data.manifest);
2021-08-20 09:19:44 -07:00
if (error) throw error;
if (!data.manifest.addons?.sendmail) { // 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;
}
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;
Fix repair If a task fails, we can either: * allow other task ops to be called - we cannot do this because the ops are fine-grained. for example, a restore failure removes many things and calling set-memory or set-location in that state won't make sense. * provide a generic repair route - this allows one to override args and call the failed task again. this is what we have now but has the issue that this repair function has to know about all the other op functions. for example, for argument validation. we can do some complicated refactoring to make it work if we want. * just a generic total re-configure - this does not work because clone/restore/backup/datadir/uninstall/update failure leaves the app in a state which re-configure cannot do anything about. * allow the failed op to be called again - this seems the easiest. we just allow the route to be called again in the error state. * if we hit a state where even providing extra args, cannot get you out of this "error" state, we have to provide some repair route. for example, maybe the container disappeared by some docke error. user clicks 'repair' to recreate the container. this route does not have to take any args. The final solution is: * a failed task can be called again via the route. so we can resubmit any args and we get validation * repair route just re-configures and can be called in any state to just rebuild container. re-configure is also doing only local changes (docker, nginx) * install/clone failures are fixed using repair route. updated manifest can be passed in. * UI shows backup selector for restore failures * UI shows domain selector for change location failulre
2019-11-23 18:35:51 -08: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 };
}
2021-07-14 11:07:19 -07:00
async function restore(app, backupId, auditSource) {
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');
const appId = app.id;
let error = checkAppState(app, exports.ISTATE_PENDING_RESTORE);
2021-07-14 11:07:19 -07:00
if (error) throw error;
// 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 };
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;
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 };
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) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof auditSource, 'object');
const appId = app.id;
// all fields are optional
data.backupId = data.backupId || null;
data.backupFormat = data.backupFormat || null;
data.backupConfig = data.backupConfig || null;
const { backupId, backupFormat, backupConfig } = data;
let error = backupFormat ? validateBackupFormat(backupFormat) : null;
2021-08-20 09:19:44 -07:00
if (error) throw error;
error = checkAppState(app, exports.ISTATE_PENDING_IMPORT);
2021-08-20 09:19:44 -07:00
if (error) throw error;
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) {
assert.strictEqual(typeof app, 'object');
2021-08-20 09:19:44 -07:00
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof auditSource, 'object');
const appId = app.id;
let error = checkAppState(app, exports.ISTATE_PENDING_BACKUP);
2021-08-20 09:19:44 -07:00
if (error) throw error;
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 };
}
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) {
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');
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,
backupId = data.backupId,
overwriteDns = 'overwriteDns' in data ? data.overwriteDns : false,
skipDnsSetup = 'skipDnsSetup' in data ? data.skipDnsSetup : false,
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
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
await eventlog.add(eventlog.ACTION_APP_CLONE, auditSource, { appId: newAppId, oldAppId: appId, backupId, oldApp: app, newApp, taskId });
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) {
assert.strictEqual(typeof app, 'object');
2016-05-01 21:37:08 -07:00
assert.strictEqual(typeof auditSource, 'object');
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) {
assert.strictEqual(typeof app, 'object');
2020-03-19 17:02:42 -07:00
assert.strictEqual(typeof auditSource, 'object');
const appId = app.id;
let error = checkAppState(app, exports.ISTATE_PENDING_START);
2021-08-20 09:19:44 -07:00
if (error) throw error;
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) {
assert.strictEqual(typeof app, 'object');
2020-03-19 17:02:42 -07:00
assert.strictEqual(typeof auditSource, 'object');
const appId = app.id;
let error = checkAppState(app, exports.ISTATE_PENDING_STOP);
2021-08-20 09:19:44 -07:00
if (error) throw error;
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);
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) {
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
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
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) {
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);
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');
}
2018-10-27 14:15:52 -07:00
2021-08-25 19:41:46 -07:00
const execOptions = {
AttachStdin: true,
AttachStdout: true,
AttachStderr: true,
// A pseudo tty is a terminal which processes can detect (for example, disable colored output)
// Creating a pseudo terminal also assigns a terminal driver which detects control sequences
// When passing binary data, tty must be disabled. In addition, the stdout/stderr becomes a single
// unified stream because of the nature of a tty (see https://github.com/docker/docker/issues/19696)
Tty: options.tty,
Cmd: cmd
};
2021-08-25 19:41:46 -07:00
const startOptions = {
Detach: false,
Tty: options.tty,
// hijacking upgrades the docker connection from http to tcp. because of this upgrade,
// we can work with half-close connections (not defined in http). this way, the client
// can properly signal that stdin is EOF by closing it's side of the socket. In http,
// the whole connection will be dropped when stdin get EOF.
// https://github.com/apocas/dockerode/commit/b4ae8a03707fad5de893f302e4972c1e758592fe
hijack: true,
stream: true,
stdin: true,
stdout: true,
stderr: true
};
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) {
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
return true;
}
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');
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}`);
}
}
async function backup(app, auditSource) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof auditSource, 'object');
const appId = app.id;
let error = checkAppState(app, exports.ISTATE_PENDING_BACKUP);
2021-08-20 09:19:44 -07:00
if (error) throw error;
const task = {
args: {},
values: {}
};
2021-08-20 09:19:44 -07:00
const taskId = await addTask(appId, exports.ISTATE_PENDING_BACKUP, task);
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) {
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
}
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');
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
};
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}`);
}
}
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
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}`);
}
}
2021-08-20 09:19:44 -07:00
async function restartAppsUsingAddons(changedAddons) {
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
2021-08-20 09:19:44 -07:00
for (const app of apps) {
debug(`restartAppsUsingAddons: marking ${app.fqdn} for restart`);
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}`);
}
}
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
}
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);
}
function downloadFile(app, filePath, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof filePath, 'string');
assert.strictEqual(typeof callback, 'function');
exec(app, { cmd: [ 'stat', '--printf=%F-%s', filePath ], tty: true }, function (error, stream) {
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
}
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);
callback(null, stdoutStream, { filename: filename, size: size });
});
});
});
}
function uploadFile(app, sourceFilePath, destFilePath, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof sourceFilePath, 'string');
assert.strictEqual(typeof destFilePath, 'string');
assert.strictEqual(typeof callback, 'function');
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);
});
// 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}`);
exec(app, { cmd: [ 'bash', '-c', `cat - > ${escapedDestFilePath}` ], tty: false }, function (error, stream) {
if (error) return done(error);
var readFile = fs.createReadStream(sourceFilePath);
readFile.on('error', done);
stream.on('error', done);
stream.on('finish', done);
readFile.pipe(stream);
});
}
2021-08-20 09:19:44 -07:00
async function backupConfig(app) {
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-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-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);
}