Files
cloudron-box/src/apps.js

2039 lines
82 KiB
JavaScript
Raw Normal View History

'use strict';
exports = module.exports = {
2015-10-15 15:06:34 +02:00
hasAccessTo: hasAccessTo,
2018-06-25 16:40:16 -07:00
removeInternalFields: removeInternalFields,
removeRestrictedFields: removeRestrictedFields,
2015-10-15 15:06:34 +02:00
get: get,
2019-03-06 11:15:12 -08:00
getByContainerId: getByContainerId,
2016-02-18 15:43:46 +01:00
getByIpAddress: getByIpAddress,
getByFqdn: getByFqdn,
getAll: getAll,
2016-02-25 11:28:29 +01:00
getAllByUser: getAllByUser,
install: install,
uninstall: uninstall,
2019-09-08 16:57:08 -07:00
setAccessRestriction: setAccessRestriction,
setLabel: setLabel,
setIcon: setIcon,
setTags: setTags,
setMemoryLimit: setMemoryLimit,
setAutomaticBackup: setAutomaticBackup,
setAutomaticUpdate: setAutomaticUpdate,
setReverseProxyConfig: setReverseProxyConfig,
2019-09-08 16:57:08 -07:00
setCertificate: setCertificate,
setDebugMode: setDebugMode,
setEnvironment: setEnvironment,
setMailbox: setMailbox,
setLocation: setLocation,
setDataDir: setDataDir,
repair: repair,
2019-09-08 16:57:08 -07:00
restore: restore,
importApp: importApp,
2016-06-17 17:12:55 -05:00
clone: clone,
update: update,
backup: backup,
2016-01-19 13:35:18 +01:00
listBackups: listBackups,
getLogs: getLogs,
start: start,
stop: stop,
exec: exec,
checkManifestConstraints: checkManifestConstraints,
canAutoupdateApp: canAutoupdateApp,
2017-07-31 11:57:23 -07:00
autoupdateApps: autoupdateApps,
restoreInstalledApps: restoreInstalledApps,
configureInstalledApps: configureInstalledApps,
2019-09-24 20:29:01 -07:00
schedulePendingTasks: schedulePendingTasks,
getDataDir: getDataDir,
getIconPath: getIconPath,
downloadFile: downloadFile,
uploadFile: uploadFile,
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_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-08-30 13:12:49 -07:00
ISTATE_ERROR: 'error', // error executing last pending_* command
ISTATE_INSTALLED: 'installed', // app is installed
// run states
RSTATE_RUNNING: 'running',
RSTATE_STOPPED: 'stopped', // app stopped by us
// health states (keep in sync in UI)
HEALTH_HEALTHY: 'healthy',
HEALTH_UNHEALTHY: 'unhealthy',
HEALTH_ERROR: 'error',
HEALTH_DEAD: 'dead',
// exported for testing
2015-10-15 12:26:48 +02:00
_validatePortBindings: validatePortBindings,
_validateAccessRestriction: validateAccessRestriction,
2019-08-20 11:45:00 -07:00
_translatePortBindings: translatePortBindings,
_MOCK_GET_BY_IP_APP_ID: ''
};
var appdb = require('./appdb.js'),
appstore = require('./appstore.js'),
2017-04-13 01:11:20 -07:00
AppstoreError = require('./appstore.js').AppstoreError,
2019-08-28 15:00:55 -07:00
appTaskManager = require('./apptaskmanager.js'),
assert = require('assert'),
async = require('async'),
backups = require('./backups.js'),
2019-09-19 23:13:04 -07:00
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
debug = require('debug')('box:apps'),
docker = require('./docker.js'),
2018-01-09 21:03:59 -08:00
domaindb = require('./domaindb.js'),
domains = require('./domains.js'),
2016-05-01 21:37:08 -07:00
eventlog = require('./eventlog.js'),
fs = require('fs'),
mail = require('./mail.js'),
manifestFormat = require('cloudron-manifestformat'),
once = require('once'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
semver = require('semver'),
settings = require('./settings.js'),
2015-11-02 11:20:50 -08:00
spawn = require('child_process').spawn,
split = require('split'),
superagent = require('superagent'),
2019-08-26 15:55:57 -07:00
tasks = require('./tasks.js'),
2017-08-20 23:39:49 -07:00
TransformStream = require('stream').Transform,
updateChecker = require('./updatechecker.js'),
util = require('util'),
uuid = require('uuid'),
validator = require('validator'),
_ = require('underscore');
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
// 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
var RESERVED_PORTS = [
2017-01-29 12:38:54 -08:00
22, /* ssh */
25, /* smtp */
53, /* dns */
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-03-21 23:15:30 -07:00
2020, /* mail server */
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) */
2019-07-25 15:21:15 -07:00
constants.SYSADMIN_PORT, /* sysadmin app server (lo) */
2019-07-25 15:27:28 -07:00
constants.INTERNAL_SMTP_PORT, /* internal smtp port (lo) */
constants.LDAP_PORT,
3306, /* mysql (lo) */
2016-05-13 18:48:05 -07:00
4190, /* managesieve */
8000, /* ESXi monitoring */
8417, /* graphite (lo) */
];
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 });
if (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;
}
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;
}
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;
2019-10-24 10:39:47 -07:00
if (path.resolve(dataDir) !== dataDir) return new BoxError(BoxError.BAD_FIELD, 'dataDir must be an absolute path', { field: 'dataDir' });
// nfs shares will have the directory mounted already
let stat = safe.fs.lstatSync(dataDir);
if (stat) {
2019-10-24 10:39:47 -07:00
if (!stat.isDirectory()) return new BoxError(BoxError.BAD_FIELD, `dataDir ${dataDir} is not a directory`, { field: 'dataDir' });
let entries = safe.fs.readdirSync(dataDir);
2019-10-24 10:39:47 -07:00
if (!entries) return new BoxError(BoxError.BAD_FIELD, `dataDir ${dataDir} could not be listed`, { field: 'dataDir' });
if (entries.length !== 0) return new BoxError(BoxError.BAD_FIELD, `dataDir ${dataDir} is not empty`, { field: 'dataDir' });
}
// backup logic relies on paths not overlapping (because it recurses)
2019-10-24 10:39:47 -07:00
if (dataDir.startsWith(paths.APPS_DATA_DIR)) return new BoxError(BoxError.BAD_FIELD, `dataDir ${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' ];
2019-10-24 10:39:47 -07:00
if (fhs.some((p) => dataDir.startsWith(p))) return new BoxError(BoxError.BAD_FIELD, `dataDir ${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);
2019-10-24 10:39:47 -07:00
return new BoxError(BoxError.INTERNAL_ERROR, new 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
2019-10-24 10:39:47 -07:00
return new BoxError(BoxError.ALREADY_EXISTS, `Domain '${domains.fqdn(subdomain, domainObjectMap[domain])}' is in use`, { subdomain, domain });
2019-09-01 21:34:27 -07:00
}
2019-03-19 20:43:42 -07:00
}
// check if any of the port bindings conflict
for (let portName in portBindings) {
2019-10-24 10:39:47 -07:00
if (portBindings[portName] === parseInt(match[1])) return new BoxError(BoxError.ALREADY_EXISTS, `Port ${match[1]} is reserved`, { 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', 'mailboxName',
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit',
'sso', 'debugMode', 'reverseProxyConfig', 'enableBackup', 'creationTime', 'updateTime', 'ts', 'tags',
2019-07-02 20:22:17 -07:00
'label', 'alternateDomains', 'env', 'enableAutomaticUpdate', 'dataDir');
}
2019-02-11 14:37:49 -08:00
// non-admins can only see these
function removeRestrictedFields(app) {
return _.pick(app,
2019-08-30 11:18:42 -07:00
'id', 'appStoreId', 'installationState', 'error', 'runState', 'health', 'taskId',
2019-04-24 14:25:23 +02:00
'location', 'domain', 'fqdn', 'manifest', 'portBindings', 'iconUrl', 'creationTime', 'ts', 'tags', 'label');
}
function getIconUrlSync(app) {
2019-05-17 09:47:11 -07:00
const iconUrl = '/api/v1/apps/' + app.id + '/icon';
const userIconPath = `${paths.APP_ICONS_DIR}/${app.id}.user.png`;
if (safe.fs.existsSync(userIconPath)) return iconUrl;
const appstoreIconPath = `${paths.APP_ICONS_DIR}/${app.id}.png`;
if (safe.fs.existsSync(appstoreIconPath)) return iconUrl;
return null;
}
function getIconPath(appId, options, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
if (!options.original) {
const userIconPath = `${paths.APP_ICONS_DIR}/${appId}.user.png`;
if (safe.fs.existsSync(userIconPath)) return callback(null, userIconPath);
}
const appstoreIconPath = `${paths.APP_ICONS_DIR}/${appId}.png`;
if (safe.fs.existsSync(appstoreIconPath)) return callback(null, appstoreIconPath);
2019-10-24 10:39:47 -07:00
callback(new BoxError(BoxError.NOT_FOUND, 'No icon'));
}
2019-03-06 11:12:39 -08:00
function postProcess(app, domainObjectMap) {
let result = {};
for (let portName in app.portBindings) {
result[portName] = app.portBindings[portName].hostPort;
}
app.portBindings = result;
app.iconUrl = getIconUrlSync(app);
app.fqdn = domains.fqdn(app.location, domainObjectMap[app.domain]);
app.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
}
2016-02-09 12:48:21 -08:00
function hasAccessTo(app, user, callback) {
2015-10-15 15:06:34 +02:00
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof user, 'object');
2016-02-09 12:48:21 -08:00
assert.strictEqual(typeof callback, 'function');
2015-10-15 15:06:34 +02:00
2016-02-09 12:48:21 -08:00
if (app.accessRestriction === null) return callback(null, true);
// check user access
if (app.accessRestriction.users.some(function (e) { return e === user.id; })) return callback(null, true);
if (user.admin) return callback(null, true); // admins can always access any app
2018-07-26 11:15:57 -07:00
if (!app.accessRestriction.groups) return callback(null, false);
2018-07-26 11:15:57 -07:00
if (app.accessRestriction.groups.some(function (gid) { return user.groupIds.indexOf(gid) !== -1; })) return callback(null, true);
2018-07-26 11:15:57 -07:00
callback(null, false);
2015-10-15 15:06:34 +02:00
}
2019-03-06 11:12:39 -08:00
function getDomainObjectMap(callback) {
assert.strictEqual(typeof callback, 'function');
2018-09-22 16:09:11 -07:00
domaindb.getAll(function (error, domainObjects) {
2019-10-24 10:39:47 -07:00
if (error) return callback(error);
2019-03-06 11:12:39 -08:00
let domainObjectMap = {};
for (let d of domainObjects) { domainObjectMap[d.domain] = d; }
callback(null, domainObjectMap);
});
}
function get(appId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
getDomainObjectMap(function (error, domainObjectMap) {
if (error) return callback(error);
2018-09-22 16:09:11 -07:00
appdb.get(appId, function (error, app) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(new BoxError(BoxError.NOT_FOUND, 'No such app'));
if (error) return callback(error);
2018-01-09 21:03:59 -08:00
2019-03-06 11:12:39 -08:00
postProcess(app, domainObjectMap);
rework how app mailboxes are allocated Our current setup had a mailbox allocated for an app during app install (into the mailboxes table). This has many issues: * When set to a custom mailbox location, there was no way to access this mailbox even via IMAP. Even when using app credentials, we cannot use IMAP since the ldap logic was testing on the addon type (most of our apps only use sendmail addon and thus cannot recvmail). * The mailboxes table was being used to add hidden 'app' type entries. This made it very hard for the user to understand why a mailbox conflicts. For example, if you set an app to use custom mailbox 'blog', this is hidden from all views. The solution is to let an app send email as whatever mailbox name is allocated to it (which we now track in the apps table. the default is in the db already so that REST response contains it). When not using Cloudron email, it will just send mail as that mailbox and the auth checks the "app password" in the addons table. Any replies to that mailbox will end up in the domain's mail server (not our problem). When using cloudron email, the app can send mail like above. Any responses will not end anywhere and bounce since there is no 'mailbox'. This is the expected behavior. If user wants to access this mailbox name, he can create a concrete mailbox and set himself as owner OR set this as an alias. For apps using the recvmail addon, the workflow is to actually create a mailbox at some point. Currently, we have no UI for this 'flow'. It's fine because we have only meemo using it. Intuitive much!
2018-12-06 21:08:19 -08:00
callback(null, app);
2018-01-09 21:03:59 -08:00
});
});
}
2019-03-06 11:15:12 -08:00
function getByContainerId(containerId, callback) {
assert.strictEqual(typeof containerId, 'string');
2016-02-18 15:43:46 +01:00
assert.strictEqual(typeof callback, 'function');
2019-03-06 11:12:39 -08:00
getDomainObjectMap(function (error, domainObjectMap) {
if (error) return callback(error);
2016-02-18 15:43:46 +01:00
2019-03-06 11:15:12 -08:00
appdb.getByContainerId(containerId, function (error, app) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(new BoxError(BoxError.NOT_FOUND, 'No such app'));
if (error) return callback(error);
2019-03-06 11:15:12 -08:00
postProcess(app, domainObjectMap);
2018-01-09 21:03:59 -08:00
2019-03-06 11:15:12 -08:00
callback(null, app);
2016-02-18 15:43:46 +01:00
});
});
}
// returns the app associated with this IP (app or scheduler)
2019-03-06 11:15:12 -08:00
function getByIpAddress(ip, callback) {
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
2019-08-20 13:34:18 -07:00
// this is only used by the ldap test. the apps tests still uses proper docker
if (constants.TEST && exports._MOCK_GET_BY_IP_APP_ID) return get(exports._MOCK_GET_BY_IP_APP_ID, callback);
2019-08-20 11:45:00 -07:00
2019-03-06 11:15:12 -08:00
docker.getContainerIdByIp(ip, function (error, containerId) {
2019-10-24 10:39:47 -07:00
if (error) return callback(error);
2019-03-06 11:15:12 -08:00
docker.inspect(containerId, function (error, result) {
2019-10-24 10:39:47 -07:00
if (error) return callback(error);
const appId = safe.query(result, 'Config.Labels.appId', null);
2019-10-24 10:39:47 -07:00
if (!appId) return callback(new BoxError(BoxError.NOT_FOUND, 'No such app'));
get(appId, callback);
});
2019-03-06 11:15:12 -08:00
});
}
function getByFqdn(fqdn, callback) {
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof callback, 'function');
getAll(function (error, result) {
if (error) return callback(error);
var app = result.find(function (a) { return a.fqdn === fqdn; });
2019-10-24 10:39:47 -07:00
if (!app) return callback(new BoxError(BoxError.NOT_FOUND, 'No such app'));
callback(null, app);
});
}
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
2019-03-06 11:12:39 -08:00
getDomainObjectMap(function (error, domainObjectMap) {
if (error) return callback(error);
2018-09-22 16:09:11 -07:00
appdb.getAll(function (error, apps) {
if (error) return callback(error);
2018-01-09 21:03:59 -08:00
2019-03-06 11:12:39 -08:00
apps.forEach((app) => postProcess(app, domainObjectMap));
2019-03-06 11:12:39 -08:00
callback(null, apps);
2018-01-09 21:03:59 -08:00
});
});
}
2016-02-25 11:28:29 +01:00
function getAllByUser(user, callback) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof callback, 'function');
getAll(function (error, result) {
if (error) return callback(error);
2017-02-16 20:11:09 -08:00
async.filter(result, function (app, iteratorDone) {
hasAccessTo(app, user, iteratorDone);
}, callback);
2016-02-25 11:28:29 +01:00
});
}
function downloadManifest(appStoreId, manifest, callback) {
2019-10-24 10:39:47 -07:00
if (!appStoreId && !manifest) return callback(new BoxError(BoxError.BAD_FIELD, 'Neither manifest nor appStoreId provided'));
2016-06-07 15:36:45 -07:00
if (!appStoreId) return callback(null, '', manifest);
var parts = appStoreId.split('@');
var url = settings.apiServerOrigin() + '/api/v1/apps/' + parts[0] + (parts[1] ? '/versions/' + parts[1] : '');
debug('downloading manifest from %s', url);
superagent.get(url).timeout(30 * 1000).end(function (error, result) {
2019-10-24 10:39:47 -07:00
if (error && !error.response) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Network error downloading manifest:' + error.message));
2019-10-24 10:39:47 -07:00
if (result.statusCode !== 200) return callback(new BoxError(BoxError.NOT_FOUND, util.format('Failed to get app info from store.', result.statusCode, result.text)));
callback(null, parts[0], result.body.manifest);
});
}
function mailboxNameForLocation(location, manifest) {
return (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app';
}
function scheduleTask(appId, installationState, taskId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof installationState, 'string');
2019-09-24 10:28:50 -07:00
assert.strictEqual(typeof taskId, 'string');
assert.strictEqual(typeof callback, 'function');
appTaskManager.scheduleTask(appId, taskId, 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;
2019-09-24 10:28:50 -07:00
// see also apptask makeTaskError
boxError.details.taskId = taskId;
boxError.details.installationState = installationState;
appdb.update(appId, { installationState: exports.ISTATE_ERROR, error: boxError.toPlainObject(), taskId: null }, callback);
} else if (!(installationState === exports.ISTATE_PENDING_UNINSTALL && !error)) { // clear out taskId except for successful uninstall
appdb.update(appId, { taskId: null }, callback);
}
});
}
function addTask(appId, installationState, task, callback) {
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-28 15:00:55 -07:00
assert.strictEqual(typeof callback, 'function');
2019-08-27 16:12:24 -07:00
const { args, values } = task;
// by default, a task can only run on installed state. if it's null, it can be run on any state
const requiredState = '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;
tasks.add(tasks.TASK_APP, [ appId, args ], function (error, taskId) {
2019-10-24 10:39:47 -07:00
if (error) return callback(error);
2019-09-24 20:29:01 -07:00
appdb.setTask(appId, _.extend({ installationState, taskId, error: null }, values), { requiredState, requireNullTaskId }, function (error) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(new BoxError(BoxError.BAD_STATE, 'Another task is scheduled for this app')); // could be because app went away OR a taskId exists
if (error) return callback(error);
2019-08-26 15:55:57 -07:00
2019-09-24 20:29:01 -07:00
if (scheduleNow) scheduleTask(appId, installationState, taskId, NOOP_CALLBACK);
2019-08-26 15:55:57 -07:00
2019-08-28 12:51:00 -07:00
callback(null, { taskId });
2019-08-26 15:55:57 -07:00
});
});
}
function checkAppState(app, state) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof state, 'string');
2019-10-24 10:39:47 -07:00
if (app.taskId) return new BoxError(BoxError.BAD_STATE, `Not allowed in this app state : ${app.installationState} / ${app.runState}`);
2019-09-23 10:35:42 -07:00
if (app.installationState === exports.ISTATE_ERROR) {
2019-10-24 10:39:47 -07:00
if (state !== exports.ISTATE_PENDING_UNINSTALL) return new BoxError(BoxError.BAD_STATE, 'Not allowed in error state');
2019-09-23 10:35:42 -07:00
}
return null;
}
function validateLocations(locations, callback) {
assert(Array.isArray(locations));
assert.strictEqual(typeof callback, 'function');
getDomainObjectMap(function (error, domainObjectMap) {
if (error) return callback(error);
for (let location of locations) {
2019-10-24 10:39:47 -07:00
if (!(location.domain in domainObjectMap)) return callback(new BoxError(BoxError.BAD_FIELD, 'No such domain', { field: 'location', domain: location.domain, subdomain: location.subdomain }));
error = domains.validateHostname(location.subdomain, domainObjectMap[location.domain]);
2019-10-24 10:39:47 -07:00
if (error) return callback(new BoxError(BoxError.BAD_FIELD, 'Bad location: ' + error.message, { field: 'location', domain: location.domain, subdomain: location.subdomain }));
}
callback(null, domainObjectMap);
});
}
2018-09-04 16:37:08 -07:00
function install(data, user, auditSource, callback) {
2016-06-03 23:22:38 -07:00
assert(data && typeof data === 'object');
2018-09-04 16:37:08 -07:00
assert(user && typeof user === 'object');
2016-06-03 23:22:38 -07:00
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
var location = data.location.toLowerCase(),
2017-11-02 22:17:44 +01:00
domain = data.domain.toLowerCase(),
2016-06-03 23:22:38 -07:00
portBindings = data.portBindings || null,
accessRestriction = data.accessRestriction || null,
icon = data.icon || null,
cert = data.cert || null,
key = data.key || null,
memoryLimit = data.memoryLimit || 0,
sso = 'sso' in data ? data.sso : null,
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 || [],
env = data.env || {},
2019-03-22 07:48:31 -07:00
mailboxName = data.mailboxName || '',
label = data.label || null,
tags = data.tags || [],
overwriteDns = 'overwriteDns' in data ? data.overwriteDns : false;
2016-06-03 23:22:38 -07:00
assert(data.appStoreId || data.manifest); // atleast one of them is required
downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) {
if (error) return callback(error);
error = manifestFormat.parse(manifest);
2019-10-24 10:39:47 -07:00
if (error) return callback(new BoxError(BoxError.BAD_FIELD, 'Manifest error: ' + error.message));
error = checkManifestConstraints(manifest);
if (error) return callback(error);
error = validatePortBindings(portBindings, manifest);
if (error) return callback(error);
error = validateAccessRestriction(accessRestriction);
if (error) return callback(error);
error = validateMemoryLimit(manifest, memoryLimit);
if (error) return callback(error);
error = validateDebugMode(debugMode);
if (error) return callback(error);
2019-03-22 07:48:31 -07:00
error = validateLabel(label);
if (error) return callback(error);
error = validateTags(tags);
if (error) return callback(error);
2019-10-24 10:39:47 -07:00
if ('sso' in data && !('optionalSso' in manifest)) return callback(new BoxError(BoxError.BAD_FIELD, 'sso can only be specified for apps with optionalSso'));
2016-12-09 18:43:26 -08:00
// if sso was unspecified, enable it by default if possible
if (sso === null) sso = !!manifest.addons['ldap'] || !!manifest.addons['oauth'];
2018-10-18 11:19:32 -07:00
error = validateEnv(env);
if (error) return callback(error);
if (mailboxName) {
error = mail.validateName(mailboxName);
2019-10-24 10:39:47 -07:00
if (error) return callback(new BoxError(BoxError.BAD_FIELD, error.message, { field: 'mailboxName' }));
} else {
mailboxName = mailboxNameForLocation(location, manifest);
}
2016-07-09 12:25:00 -07:00
var appId = uuid.v4();
if (icon) {
2019-10-24 10:39:47 -07:00
if (!validator.isBase64(icon)) return callback(new BoxError(BoxError.BAD_FIELD, 'icon is not base64', { field: 'icon' }));
2019-03-21 20:06:14 -07:00
if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.png'), Buffer.from(icon, 'base64'))) {
2019-10-24 10:39:47 -07:00
return callback(new BoxError(BoxError.FS_ERROR, 'Error saving icon:' + safe.error.message));
}
}
const locations = [{subdomain: location, domain}].concat(alternateDomains);
validateLocations(locations, function (error, domainObjectMap) {
if (error) return callback(error);
2018-01-26 19:31:06 -08:00
if (cert && key) {
error = reverseProxy.validateCertificate(location, domainObjectMap[domain], { cert, key });
2019-10-24 10:39:47 -07:00
if (error) return callback(new BoxError(BoxError.BAD_FIELD, error.message, { field: 'cert' }));
2018-01-26 19:31:06 -08:00
}
2018-01-09 21:03:59 -08:00
debug('Will install app with id : ' + appId);
2016-06-17 16:43:35 -05:00
var data = {
accessRestriction: accessRestriction,
memoryLimit: memoryLimit,
sso: sso,
debugMode: debugMode,
mailboxName: mailboxName,
enableBackup: enableBackup,
enableAutomaticUpdate: enableAutomaticUpdate,
alternateDomains: alternateDomains,
2019-09-06 12:24:01 -07:00
env: env,
label: label,
tags: tags,
runState: exports.RSTATE_RUNNING,
installationState: exports.ISTATE_PENDING_INSTALL
};
2019-07-02 20:22:17 -07:00
appdb.add(appId, appStoreId, manifest, location, domain, translatePortBindings(portBindings, manifest), data, function (error) {
if (error && error.reason === BoxError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error.message, locations, domainObjectMap, portBindings));
if (error) return callback(error);
2019-05-05 10:31:42 -07:00
purchaseApp({ appId: appId, appstoreId: appStoreId, manifestId: manifest.id }, function (error) {
if (error) return callback(error);
2018-01-09 21:03:59 -08:00
// save cert to boxdata/certs
if (cert && key) {
let error = reverseProxy.setAppCertificateSync(location, domainObjectMap[domain], { cert, key });
2019-10-24 10:39:47 -07:00
if (error) return callback(error);
2018-01-09 21:03:59 -08:00
}
2016-05-01 21:37:08 -07:00
const task = {
args: { restoreConfig: null, overwriteDns },
values: { },
requiredState: data.installationState
};
addTask(appId, data.installationState, task, function (error, result) {
if (error) return callback(error);
const newApp = _.extend({}, data, { appStoreId, manifest, location, domain, portBindings });
newApp.fqdn = domains.fqdn(newApp.location, domainObjectMap[newApp.domain]);
newApp.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
2019-09-06 12:24:01 -07:00
eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId: appId, app: newApp, taskId: result.taskId });
2018-01-09 21:03:59 -08:00
2019-08-27 20:55:49 -07:00
callback(null, { id : appId, taskId: result.taskId });
});
2018-01-09 21:03:59 -08:00
});
});
});
});
}
2019-09-08 16:57:08 -07:00
function setAccessRestriction(appId, accessRestriction, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof accessRestriction, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
get(appId, function (error, app) {
if (error) return callback(error);
error = validateAccessRestriction(accessRestriction);
if (error) return callback(error);
appdb.update(appId, { accessRestriction: accessRestriction }, function (error) {
if (error) return callback(error);
2019-09-08 16:57:08 -07:00
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: app, accessRestriction: accessRestriction });
callback();
});
});
}
function setLabel(appId, label, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof label, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
get(appId, function (error, app) {
if (error) return callback(error);
error = validateLabel(label);
if (error) return callback(error);
appdb.update(appId, { label: label }, function (error) {
if (error) return callback(error);
2019-09-08 16:57:08 -07:00
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: app, label: label });
callback();
});
});
}
function setTags(appId, tags, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
assert(Array.isArray(tags));
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
get(appId, function (error, app) {
if (error) return callback(error);
error = validateTags(tags);
if (error) return callback(error);
appdb.update(appId, { tags: tags }, function (error) {
if (error) return callback(error);
2019-09-08 16:57:08 -07:00
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: app, tags: tags });
callback();
});
});
}
function setIcon(appId, icon, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
assert(icon === null || typeof icon === 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
get(appId, function (error, app) {
if (error) return callback(error);
if (icon) {
2019-10-24 10:39:47 -07:00
if (!validator.isBase64(icon)) return callback(new BoxError(BoxError.BAD_FIELD, 'icon is not base64', { field: 'icon' }));
2019-09-08 16:57:08 -07:00
if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.user.png'), Buffer.from(icon, 'base64'))) {
2019-10-24 10:39:47 -07:00
return callback(new BoxError(BoxError.FS_ERROR, 'Error saving icon:' + safe.error.message));
2019-09-08 16:57:08 -07:00
}
} else {
safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, appId + '.user.png'));
}
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: app, icon: icon });
callback();
});
}
function setMemoryLimit(appId, memoryLimit, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof memoryLimit, 'number');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
get(appId, function (error, app) {
if (error) return callback(error);
error = checkAppState(app, exports.ISTATE_PENDING_RESIZE);
if (error) return callback(error);
2019-09-08 16:57:08 -07:00
error = validateMemoryLimit(app.manifest, memoryLimit);
if (error) return callback(error);
const task = {
args: {},
values: { memoryLimit }
};
addTask(appId, exports.ISTATE_PENDING_RESIZE, task, function (error, result) {
2019-09-08 16:57:08 -07:00
if (error) return callback(error);
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: app, memoryLimit: memoryLimit, taskId: result.taskId });
callback(null, { taskId: result.taskId });
});
});
}
function setEnvironment(appId, env, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof env, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
get(appId, function (error, app) {
if (error) return callback(error);
error = checkAppState(app, exports.ISTATE_PENDING_RECREATE_CONTAINER);
if (error) return callback(error);
2019-09-08 16:57:08 -07:00
error = validateEnv(env);
if (error) return callback(error);
const task = {
args: {},
values: { env }
};
addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task, function (error, result) {
2019-09-08 16:57:08 -07:00
if (error) return callback(error);
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: app, env: env, taskId: result.taskId });
callback(null, { taskId: result.taskId });
});
});
}
function setDebugMode(appId, debugMode, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof debugMode, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
get(appId, function (error, app) {
if (error) return callback(error);
error = checkAppState(app, exports.ISTATE_PENDING_DEBUG);
if (error) return callback(error);
2019-09-08 16:57:08 -07:00
error = validateDebugMode(debugMode);
if (error) return callback(error);
const task = {
args: {},
values: { debugMode }
};
addTask(appId, exports.ISTATE_PENDING_DEBUG, task, function (error, result) {
2019-09-08 16:57:08 -07:00
if (error) return callback(error);
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: app, debugMode: debugMode, taskId: result.taskId });
callback(null, { taskId: result.taskId });
});
});
}
function setMailbox(appId, mailboxName, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
2019-09-09 15:58:58 -07:00
assert(mailboxName === null || typeof mailboxName === 'string');
2019-09-08 16:57:08 -07:00
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
get(appId, function (error, app) {
if (error) return callback(error);
error = checkAppState(app, exports.ISTATE_PENDING_RECREATE_CONTAINER);
if (error) return callback(error);
2019-09-08 16:57:08 -07:00
if (mailboxName) {
error = mail.validateName(mailboxName);
2019-10-24 10:39:47 -07:00
if (error) return callback(new BoxError(BoxError.BAD_FIELD, error.message, { field: 'mailboxName' }));
2019-09-08 16:57:08 -07:00
} else {
mailboxName = mailboxNameForLocation(app.location, app.manifest);
}
const task = {
args: {},
values: { mailboxName }
};
addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task, function (error, result) {
2019-09-08 16:57:08 -07:00
if (error) return callback(error);
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: app, mailboxName: mailboxName, taskId: result.taskId });
callback(null, { taskId: result.taskId });
});
});
}
function setAutomaticBackup(appId, enable, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof enable, 'boolean');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
get(appId, function (error, app) {
if (error) return callback(error);
appdb.update(appId, { enableBackup: enable }, function (error) {
if (error) return callback(error);
2019-09-08 16:57:08 -07:00
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: app, enableBackup: enable });
callback();
});
});
}
function setAutomaticUpdate(appId, enable, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof enable, 'boolean');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
get(appId, function (error, app) {
if (error) return callback(error);
appdb.update(appId, { enableAutomaticUpdate: enable }, function (error) {
if (error) return callback(error);
2019-09-08 16:57:08 -07:00
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: app, enableAutomaticUpdate: enable });
callback();
});
});
}
function setReverseProxyConfig(appId, reverseProxyConfig, auditSource, callback) {
2019-09-08 16:57:08 -07:00
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof reverseProxyConfig, 'object');
2019-09-08 16:57:08 -07:00
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
reverseProxyConfig = _.extend({ robotsTxt: null, csp: null }, reverseProxyConfig);
2019-09-08 16:57:08 -07:00
get(appId, function (error, app) {
if (error) return callback(error);
error = validateCsp(reverseProxyConfig.csp);
if (error) return callback(error);
error = validateRobotsTxt(reverseProxyConfig.robotsTxt);
if (error) return callback(error);
reverseProxy.writeAppConfig(_.extend({}, app, { reverseProxyConfig }), function (error) {
2019-10-24 10:39:47 -07:00
if (error) return callback(error);
2019-09-09 21:41:55 -07:00
appdb.update(appId, { reverseProxyConfig }, function (error) {
if (error) return callback(error);
2019-09-08 16:57:08 -07:00
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, reverseProxyConfig });
2019-09-08 16:57:08 -07:00
2019-09-09 21:41:55 -07:00
callback();
});
2019-09-08 16:57:08 -07:00
});
});
}
function setCertificate(appId, bundle, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
assert(bundle && typeof bundle === 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
get(appId, function (error, app) {
if (error) return callback(error);
domains.get(app.domain, function (error, domainObject) {
2019-10-24 10:39:47 -07:00
if (error && error.reason === BoxError.NOT_FOUND) return callback(new BoxError(BoxError.NOT_FOUND, 'No such domain'));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, 'Could not get domain info:' + error.message));
2019-09-08 16:57:08 -07:00
if (bundle.cert && bundle.key) {
error = reverseProxy.validateCertificate(app.location, domainObject, { cert: bundle.cert, key: bundle.key });
2019-10-24 10:39:47 -07:00
if (error) return callback(new BoxError(BoxError.BAD_FIELD, error.message, { field: 'cert' }));
2019-09-08 16:57:08 -07:00
}
error = reverseProxy.setAppCertificateSync(app.location, domainObject, { cert: bundle.cert, key: bundle.key });
2019-10-24 10:39:47 -07:00
if (error) return callback(error);
2019-09-08 16:57:08 -07:00
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: app, cert: bundle.cert, key: bundle.key });
callback();
});
});
}
function setLocation(appId, data, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
get(appId, function (error, app) {
if (error) return callback(error);
error = checkAppState(app, exports.ISTATE_PENDING_LOCATION_CHANGE);
if (error) return callback(error);
2019-09-08 16:57:08 -07:00
let values = {
location: data.location.toLowerCase(),
domain: data.domain.toLowerCase(),
2019-09-08 16:57:08 -07:00
// these are intentionally reset, if not set
portBindings: null,
alternateDomains: []
};
if ('portBindings' in data) {
error = validatePortBindings(data.portBindings, app.manifest);
if (error) return callback(error);
values.portBindings = translatePortBindings(data.portBindings || null, app.manifest);
}
// move the mailbox name to match the new location
if (app.mailboxName.endsWith('.app')) values.mailboxName = mailboxNameForLocation(values.location, app.manifest);
2019-09-08 16:57:08 -07:00
if ('alternateDomains' in data) {
values.alternateDomains = data.alternateDomains;
}
const locations = [{subdomain: values.location, domain: values.domain}].concat(values.alternateDomains);
2019-09-08 16:57:08 -07:00
validateLocations(locations, function (error, domainObjectMap) {
if (error) return callback(error);
2019-09-08 16:57:08 -07:00
const task = {
args: {
oldConfig: _.pick(app, 'location', 'domain', 'fqdn', 'alternateDomains', 'portBindings'),
overwriteDns: !!data.overwriteDns
},
values
2019-09-10 15:23:47 -07:00
};
addTask(appId, exports.ISTATE_PENDING_LOCATION_CHANGE, task, function (error, result) {
2019-10-24 10:39:47 -07:00
if (error && error.reason === BoxError.ALREADY_EXISTS) error = getDuplicateErrorDetails(error.message, locations, domainObjectMap, data.portBindings);
2019-09-08 16:57:08 -07:00
if (error) return callback(error);
values.fqdn = domains.fqdn(values.location, domainObjectMap[values.domain]);
values.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, _.extend({ appId, app, taskId: result.taskId }, values));
2019-09-08 16:57:08 -07:00
callback(null, { taskId: result.taskId });
});
});
});
}
function setDataDir(appId, dataDir, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
2019-09-09 16:37:59 -07:00
assert(dataDir === null || typeof dataDir === 'string');
2019-09-08 16:57:08 -07:00
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
get(appId, function (error, app) {
if (error) return callback(error);
error = checkAppState(app, exports.ISTATE_PENDING_DATA_DIR_MIGRATION);
if (error) return callback(error);
2019-09-08 16:57:08 -07:00
error = validateDataDir(dataDir);
if (error) return callback(error);
const task = {
args: { oldDataDir: app.dataDir },
values: { dataDir: dataDir }
};
addTask(appId, exports.ISTATE_PENDING_DATA_DIR_MIGRATION, task, function (error, result) {
2019-09-08 16:57:08 -07:00
if (error) return callback(error);
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: app, dataDir: dataDir, taskId: result.taskId });
callback(null, { taskId: result.taskId });
});
});
}
2016-06-04 19:06:16 -07:00
function update(appId, data, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
2016-06-04 19:06:16 -07:00
assert(data && typeof data === 'object');
2016-05-01 21:37:08 -07:00
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug(`update: id:${appId}`);
const skipBackup = !!data.skipBackup;
2019-01-11 13:19:28 -08:00
get(appId, function (error, app) {
2016-06-04 19:19:00 -07:00
if (error) return callback(error);
error = checkAppState(app, exports.ISTATE_PENDING_UPDATE);
if (error) return callback(error);
2016-06-04 19:06:16 -07:00
2019-01-11 13:19:28 -08:00
downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) {
if (error) return callback(error);
2019-01-11 13:19:28 -08:00
error = manifestFormat.parse(manifest);
2019-10-24 10:39:47 -07:00
if (error) return callback(new BoxError(BoxError.BAD_FIELD, 'Manifest error:' + error.message));
2019-01-11 13:19:28 -08:00
error = checkManifestConstraints(manifest);
if (error) return callback(error);
var updateConfig = { skipBackup, manifest };
// 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) {
2019-10-24 10:39:47 -07:00
if (!data.force) return callback(new BoxError(BoxError.BAD_FIELD, 'manifest id does not match. force to override'));
// clear appStoreId so that this app does not get updates anymore
updateConfig.appStoreId = '';
}
// 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)) {
2019-10-24 10:39:47 -07:00
if (!data.force) return callback(new BoxError(BoxError.BAD_FIELD, 'Downgrades are not permitted for apps installed from AppStore. force to override'));
}
2019-01-11 13:19:28 -08:00
if ('icon' in data) {
if (data.icon) {
2019-10-24 10:39:47 -07:00
if (!validator.isBase64(data.icon)) return callback(new BoxError(BoxError.BAD_FIELD, 'icon is not base64', { field: 'icon' }));
2019-01-11 13:19:28 -08:00
2019-05-17 12:50:08 -07:00
if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.user.png'), Buffer.from(data.icon, 'base64'))) {
2019-10-24 10:39:47 -07:00
return callback(new BoxError(BoxError.FS_ERROR, 'Error saving icon:' + safe.error.message));
2019-01-11 13:19:28 -08:00
}
} else {
2019-05-17 12:50:08 -07:00
safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, appId + '.user.png'));
2016-06-04 19:19:00 -07:00
}
}
// do not update apps in debug mode
2019-10-24 10:39:47 -07:00
if (app.debugMode && !data.force) return callback(new BoxError(BoxError.BAD_STATE, 'debug mode enabled. force to override'));
2016-06-04 19:19:00 -07:00
// Ensure we update the memory limit in case the new app requires more memory as a minimum
2017-11-16 12:36:07 -08:00
// 0 and -1 are special updateConfig for memory limit indicating unset and unlimited
if (app.memoryLimit > 0 && updateConfig.manifest.memoryLimit && app.memoryLimit < updateConfig.manifest.memoryLimit) {
updateConfig.memoryLimit = updateConfig.manifest.memoryLimit;
2016-06-04 19:19:00 -07:00
}
const task = {
args: { updateConfig },
values: {}
};
addTask(appId, exports.ISTATE_PENDING_UPDATE, task, function (error, result) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId, app, skipBackup, toManifest: manifest, fromManifest: app.manifest, force: data.force, taskId: result.taskId });
2016-05-01 21:37:08 -07:00
// clear update indicator, if update fails, it will come back through the update checker
updateChecker.resetAppUpdateInfo(appId);
callback(null, { taskId: result.taskId });
2016-06-04 19:19:00 -07:00
});
});
});
}
function getLogs(appId, options, callback) {
assert.strictEqual(typeof appId, 'string');
assert(options && typeof options === 'object');
assert.strictEqual(typeof callback, 'function');
assert.strictEqual(typeof options.lines, 'number');
assert.strictEqual(typeof options.format, 'string');
assert.strictEqual(typeof options.follow, 'boolean');
debug('Getting logs for %s', appId);
2015-11-02 11:20:50 -08:00
2018-09-18 14:15:23 -07:00
get(appId, function (error, app) {
if (error) return callback(error);
var lines = options.lines === -1 ? '+1' : options.lines,
2018-06-04 21:24:02 +02:00
format = options.format || 'json',
follow = options.follow;
2018-06-11 20:09:38 +02:00
assert.strictEqual(typeof format, 'string');
2018-06-04 21:24:02 +02:00
var args = [ '--lines=' + lines ];
if (follow) args.push('--follow', '--retry', '--quiet'); // same as -F. to make it work if file doesn't exist, --quiet to not output file headers, which are no logs
2018-06-12 14:55:58 -07:00
args.push(path.join(paths.LOG_DIR, appId, 'apptask.log'));
2018-06-15 14:58:07 +02:00
args.push(path.join(paths.LOG_DIR, appId, 'app.log'));
2018-09-18 14:15:23 -07:00
if (app.manifest.addons && app.manifest.addons.redis) args.push(path.join(paths.LOG_DIR, `redis-${appId}/app.log`));
var cp = spawn('/usr/bin/tail', args);
2015-11-02 11:20:50 -08:00
var transformStream = split(function mapper(line) {
2018-06-04 21:24:02 +02:00
if (format !== 'json') return line + '\n';
var data = line.split(' '); // logs are <ISOtimestamp> <msg>
var timestamp = (new Date(data[0])).getTime();
if (isNaN(timestamp)) timestamp = 0;
2018-06-14 12:21:43 +02:00
var message = line.slice(data[0].length+1);
// ignore faulty empty logs
if (!timestamp && !message) return;
2018-06-04 21:24:02 +02:00
return JSON.stringify({
realtimeTimestamp: timestamp * 1000,
2018-06-14 12:21:43 +02:00
message: message,
2018-06-04 21:24:02 +02:00
source: appId
}) + '\n';
2015-11-02 11:20:50 -08:00
});
2015-11-02 11:20:50 -08:00
transformStream.close = cp.kill.bind(cp, 'SIGKILL'); // closing stream kills the child process
2015-11-02 11:20:50 -08:00
cp.stdout.pipe(transformStream);
2015-11-02 11:20:50 -08:00
return callback(null, transformStream);
});
}
function repair(appId, data, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug('Will repair app with id:%s', appId);
get(appId, function (error, app) {
if (error) return callback(error);
const appError = app.error || {}; // repair can always be called
2019-09-24 10:28:50 -07:00
const newState = appError.installationState ? appError.installationState : exports.ISTATE_PENDING_CONFIGURE;
debug(`Repairing app with error: ${JSON.stringify(error)} and state: ${newState}`);
let values = _.pick(data, 'location', 'domain', 'alternateDomains');
const locations = (values.location ? [ { subdomain: values.location, domain: values.domain } ] : []).concat(values.alternateDomains || []);
validateLocations(locations, function (error, domainObjectMap) {
if (error) return callback(error);
tasks.get(appError.taskId || '', function (error, task) {
let args = !error ? task.args[1] : {}; // pick args for the failed task. the first argument is the app id
if ('backupId' in data) {
args.restoreConfig = data.backupId ? { backupId: data.backupId, backupFormat: data.backupFormat, oldManifest: app.manifest } : null; // when null, apptask simply reinstalls
}
args.overwriteDns = 'overwriteDns' in data ? data.overwriteDns : false;
// create a new task instead of updating the old one, since it helps tracking
addTask(appId, newState, { args, values, requiredState: null }, function (error, result) {
2019-10-24 10:39:47 -07:00
if (error && error.reason === BoxError.ALREADY_EXISTS) error = getDuplicateErrorDetails(error.message, locations, domainObjectMap, { /* portBindings */});
if (error) return callback(error);
eventlog.add(eventlog.ACTION_APP_REPAIR, auditSource, { taskId: result.taskId, app, newState });
callback(null, { taskId: result.taskId });
});
2019-09-24 10:28:50 -07:00
});
});
});
}
2016-06-13 10:08:58 -07:00
function restore(appId, data, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
2016-06-13 10:08:58 -07:00
assert.strictEqual(typeof data, 'object');
2016-05-01 21:37:08 -07:00
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug('Will restore app with id:%s', appId);
get(appId, function (error, app) {
if (error) return callback(error);
error = checkAppState(app, exports.ISTATE_PENDING_RESTORE);
if (error) return callback(error);
// for empty or null backupId, use existing manifest to mimic a reinstall
var func = data.backupId ? backups.get.bind(null, data.backupId) : function (next) { return next(null, { manifest: app.manifest }); };
func(function (error, backupInfo) {
2019-10-24 10:39:47 -07:00
if (error && error.reason === BoxError.NOT_FOUND) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
if (error && error.reason === BoxError.EXTERNAL_ERROR) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
2016-06-13 18:11:11 -07:00
2019-10-24 10:39:47 -07:00
if (!backupInfo.manifest) callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Could not get restore manifest'));
// re-validate because this new box version may not accept old configs
error = checkManifestConstraints(backupInfo.manifest);
if (error) return callback(error);
const restoreConfig = { backupId: data.backupId, backupFormat: backupInfo.format, oldManifest: app.manifest };
const task = {
args: {
restoreConfig,
overwriteDns: true
},
values: {
manifest: backupInfo.manifest
}
};
addTask(appId, exports.ISTATE_PENDING_RESTORE, task, function (error, result) {
if (error) return callback(error);
2019-09-06 12:24:01 -07:00
eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { app: app, backupId: backupInfo.id, fromManifest: app.manifest, toManifest: backupInfo.manifest, taskId: result.taskId });
2016-05-01 21:37:08 -07:00
callback(null, { taskId: result.taskId });
});
});
});
}
function importApp(appId, data, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug('Will import app with id:%s', appId);
get(appId, function (error, app) {
if (error) return callback(error);
error = validateBackupFormat(data.backupFormat);
if (error) return callback(error);
error = checkAppState(app, exports.ISTATE_PENDING_RESTORE);
if (error) return callback(error);
// TODO: check if the file exists in the storage backend
const restoreConfig = { backupId: data.backupId, backupFormat: data.backupFormat, oldManifest: app.manifest };
const task = {
args: {
restoreConfig,
overwriteDns: true
},
values: {}
};
addTask(appId, exports.ISTATE_PENDING_RESTORE, task, function (error, result) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { app: app, backupId: data.backupId, fromManifest: app.manifest, toManifest: app.manifest, taskId: result.taskId });
callback(null, { taskId: result.taskId });
});
});
}
2019-05-05 10:31:42 -07:00
function purchaseApp(data, callback) {
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
2019-05-06 14:29:56 -07:00
appstore.purchaseApp(data, function (error) {
2019-05-05 10:31:42 -07:00
if (!error) return callback();
// if purchase failed, rollback the appdb record
2019-05-05 10:46:43 -07:00
appdb.del(data.appId, function (delError) {
if (delError) debug('install: Failed to rollback app installation.', delError);
2019-05-05 10:31:42 -07:00
2019-10-24 10:39:47 -07:00
if (error.reason === AppstoreError.NOT_FOUND) return callback(new BoxError(BoxError.NOT_FOUND, error.message));
if (error && error.reason === AppstoreError.PLAN_LIMIT) return callback(new BoxError(BoxError.PLAN_LIMIT, error.message));
if (error && error.reason === AppstoreError.INVALID_TOKEN) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
if (error && error.reason === AppstoreError.LICENSE_ERROR) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
if (error && error.reason === AppstoreError.NOT_REGISTERED) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
if (error && error.reason === AppstoreError.EXTERNAL_ERROR) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
2019-05-05 10:31:42 -07:00
2019-10-24 10:39:47 -07:00
callback(new BoxError(BoxError.DATABASE_ERROR, error));
2019-05-05 10:31:42 -07:00
});
});
}
2018-09-04 16:37:08 -07:00
function clone(appId, data, user, auditSource, callback) {
2016-06-17 17:12:55 -05:00
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof data, 'object');
2018-09-04 16:37:08 -07:00
assert(user && typeof user === 'object');
2016-06-17 17:12:55 -05:00
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug('Will clone app with id:%s', appId);
var 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,
mailboxName = data.mailboxName || '',
overwriteDns = 'overwriteDns' in data ? data.overwriteDns : false;
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');
get(appId, function (error, app) {
if (error) return callback(error);
2016-06-17 17:12:55 -05:00
backups.get(backupId, function (error, backupInfo) {
2019-10-24 10:39:47 -07:00
if (error && error.reason === BoxError.EXTERNAL_ERROR) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
if (error && error.reason === BoxError.NOT_FOUND) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Backup not found'));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
2016-06-17 17:12:55 -05:00
2019-10-24 10:39:47 -07:00
if (!backupInfo.manifest) callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Could not get restore config'));
2016-06-17 17:12:55 -05:00
2019-09-06 12:24:01 -07:00
const manifest = backupInfo.manifest, appStoreId = app.appStoreId;
2016-06-17 17:12:55 -05:00
// re-validate because this new box version may not accept old configs
error = checkManifestConstraints(manifest);
2016-06-17 17:12:55 -05:00
if (error) return callback(error);
error = validatePortBindings(portBindings, manifest);
2016-06-17 17:12:55 -05:00
if (error) return callback(error);
if (mailboxName) {
error = mail.validateName(mailboxName);
2019-10-24 10:39:47 -07:00
if (error) return callback(new BoxError(BoxError.BAD_FIELD, error.message, { field: 'mailboxName' }));
} else {
mailboxName = mailboxNameForLocation(location, manifest);
}
const locations = [{subdomain: location, domain}];
validateLocations(locations, function (error, domainObjectMap) {
if (error) return callback(error);
2016-06-17 17:12:55 -05:00
var newAppId = uuid.v4();
2018-01-11 10:59:30 -08:00
var data = {
2019-08-30 13:12:49 -07:00
installationState: exports.ISTATE_PENDING_CLONE,
runState: exports.RSTATE_RUNNING,
memoryLimit: app.memoryLimit,
accessRestriction: app.accessRestriction,
sso: !!app.sso,
mailboxName: mailboxName,
enableBackup: app.enableBackup,
robotsTxt: app.robotsTxt,
env: app.env,
alternateDomains: []
};
2019-09-06 12:24:01 -07:00
appdb.add(newAppId, appStoreId, manifest, location, domain, translatePortBindings(portBindings, manifest), data, function (error) {
if (error && error.reason === BoxError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error.message, locations, domainObjectMap, portBindings));
if (error) return callback(error);
2016-06-17 17:12:55 -05:00
2019-05-05 10:31:42 -07:00
purchaseApp({ appId: newAppId, appstoreId: app.appStoreId, manifestId: manifest.id }, function (error) {
if (error) return callback(error);
2018-01-11 10:59:30 -08:00
2019-09-30 09:58:13 -07:00
const restoreConfig = { backupId: backupId, backupFormat: backupInfo.format, oldManifest: null };
const task = {
args: { restoreConfig, overwriteDns },
values: {},
requiredState: exports.ISTATE_PENDING_CLONE
};
addTask(newAppId, exports.ISTATE_PENDING_CLONE, task, function (error, result) {
2019-08-26 22:01:10 -07:00
if (error) return callback(error);
2019-09-06 12:24:01 -07:00
const newApp = _.extend({}, data, { appStoreId, manifest, location, domain, portBindings });
newApp.fqdn = domains.fqdn(newApp.location, domainObjectMap[newApp.domain]);
newApp.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
2019-09-06 12:24:01 -07:00
eventlog.add(eventlog.ACTION_APP_CLONE, auditSource, { appId: newAppId, oldAppId: appId, backupId: backupId, oldApp: app, newApp: newApp, taskId: result.taskId });
2016-06-17 17:12:55 -05:00
2019-08-27 20:55:49 -07:00
callback(null, { id: newAppId, taskId: result.taskId });
});
2018-01-11 10:59:30 -08:00
});
2016-06-17 17:12:55 -05:00
});
});
});
});
}
2016-05-01 21:37:08 -07:00
function uninstall(appId, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
2016-05-01 21:37:08 -07:00
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug(`Will uninstall app with id : ${appId}`);
get(appId, function (error, app) {
2016-08-04 09:38:00 +02:00
if (error) return callback(error);
error = checkAppState(app, exports.ISTATE_PENDING_UNINSTALL);
if (error) return callback(error);
2019-05-06 14:29:56 -07:00
appstore.unpurchaseApp(appId, { appstoreId: app.appStoreId, manifestId: app.manifest.id }, function (error) {
2019-10-24 10:39:47 -07:00
if (error && error.reason === AppstoreError.NOT_FOUND) return callback(new BoxError(BoxError.NOT_FOUND));
if (error && error.reason === AppstoreError.INVALID_TOKEN) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
if (error && error.reason === AppstoreError.LICENSE_ERROR) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
if (error && error.reason === AppstoreError.EXTERNAL_ERROR) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
if (error) return callback(error);
const task = {
args: {},
values: {},
requiredState: null // can run in any state, as long as no task is active
};
addTask(appId, exports.ISTATE_PENDING_UNINSTALL, task, function (error, result) {
if (error) return callback(error);
2016-08-04 09:38:00 +02:00
2019-09-06 12:24:01 -07:00
eventlog.add(eventlog.ACTION_APP_UNINSTALL, auditSource, { appId: appId, app: app, taskId: result.taskId });
2016-05-01 21:37:08 -07:00
callback(null, { taskId: result.taskId });
2016-08-04 09:38:00 +02:00
});
});
});
}
function start(appId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
debug('Will start app with id:%s', appId);
get(appId, function (error, app) {
if (error) return callback(error);
error = checkAppState(app, exports.ISTATE_PENDING_START);
if (error) return callback(error);
const task = {
args: {},
values: { runState: exports.RSTATE_RUNNING }
};
addTask(appId, exports.ISTATE_PENDING_START, task, function (error, result) {
if (error) return callback(error);
callback(null, { taskId: result.taskId });
});
});
}
function stop(appId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
debug('Will stop app with id:%s', appId);
get(appId, function (error, app) {
if (error) return callback(error);
error = checkAppState(app, exports.ISTATE_PENDING_STOP);
if (error) return callback(error);
const task = {
args: {},
values: { runState: exports.RSTATE_STOPPED }
};
addTask(appId, exports.ISTATE_PENDING_STOP, task, function (error, result) {
if (error) return callback(error);
callback(null, { taskId: result.taskId });
});
});
}
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;
}
function exec(appId, options, callback) {
assert.strictEqual(typeof appId, 'string');
assert(options && typeof options === 'object');
assert.strictEqual(typeof callback, 'function');
var cmd = options.cmd || [ '/bin/bash' ];
assert(util.isArray(cmd) && cmd.length > 0);
get(appId, function (error, app) {
if (error) return callback(error);
2019-08-30 13:12:49 -07:00
if (app.installationState !== exports.ISTATE_INSTALLED || app.runState !== exports.RSTATE_RUNNING) {
2019-10-24 10:39:47 -07:00
return callback(new BoxError(BoxError.BAD_STATE, 'App not installed or running'));
2016-02-12 12:32:51 -08:00
}
var container = docker.connection.getContainer(app.containerId);
var execOptions = {
AttachStdin: true,
AttachStdout: true,
2016-01-18 21:36:05 -08:00
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)
2016-01-18 11:16:06 -08:00
Tty: options.tty,
Cmd: cmd
};
container.exec(execOptions, function (error, exec) {
2019-10-24 10:39:47 -07:00
if (error && error.statusCode === 409) return callback(new BoxError(BoxError.BAD_STATE, error.message)); // container restarting/not running
if (error) return callback(error);
2018-10-27 14:15:52 -07:00
var startOptions = {
Detach: false,
2016-01-18 11:16:06 -08:00
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
};
exec.start(startOptions, function(error, stream /* in hijacked mode, this is a net.socket */) {
2019-10-24 10:39:47 -07:00
if (error) return callback(error);
if (options.rows && options.columns) {
// there is a race where resizing too early results in a 404 "no such exec"
// https://git.cloudron.io/cloudron/box/issues/549
setTimeout(function () {
exec.resize({ h: options.rows, w: options.columns }, function (error) { if (error) debug('Error resizing console', error); });
}, 2000);
}
2015-11-10 21:56:17 -08:00
return callback(null, stream);
});
});
});
}
function canAutoupdateApp(app, newManifest) {
if (!app.enableAutomaticUpdate) return new Error('Automatic update disabled');
if ((semver.major(app.manifest.version) !== 0) && (semver.major(app.manifest.version) !== semver.major(newManifest.version))) return new Error('Major version change'); // major changes are blocking
const newTcpPorts = newManifest.tcpPorts || { };
const newUdpPorts = newManifest.udpPorts || { };
const portBindings = app.portBindings; // this is never null
for (let portName in portBindings) {
if (!(portName in newTcpPorts) && !(portName in newUdpPorts)) return new Error(`${portName} was in use but new update removes it`);
}
// it's fine if one or more (unused) keys got removed
return null;
}
function autoupdateApps(updateInfo, auditSource, callback) { // updateInfo is { appId -> { manifest } }
assert.strictEqual(typeof updateInfo, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
if (!updateInfo) return callback(null);
async.eachSeries(Object.keys(updateInfo), function iterator(appId, iteratorDone) {
get(appId, function (error, app) {
if (error) {
debug('Cannot autoupdate app %s : %s', appId, error.message);
return iteratorDone();
2017-11-15 18:45:17 -08:00
}
error = canAutoupdateApp(app, updateInfo[appId].manifest);
if (error) {
debug('app %s requires manual update. %s', appId, error.message);
return iteratorDone();
}
2016-06-04 19:06:16 -07:00
var data = {
2016-06-18 13:24:27 -05:00
manifest: updateInfo[appId].manifest,
force: false
2016-06-04 19:06:16 -07:00
};
update(appId, data, auditSource, function (error) {
if (error) debug('Error initiating autoupdate of %s. %s', appId, error.message);
iteratorDone(null);
});
});
}, callback);
}
function backup(appId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
get(appId, function (error, app) {
if (error) return callback(error);
error = checkAppState(app, exports.ISTATE_PENDING_BACKUP);
if (error) return callback(error);
const task = {
args: {},
values: {}
};
addTask(appId, exports.ISTATE_PENDING_BACKUP, task, (error, result) => {
if (error) return callback(error);
2019-08-27 20:55:49 -07:00
callback(null, { taskId: result.taskId });
});
});
}
2016-03-08 08:57:28 -08:00
function listBackups(page, perPage, appId, callback) {
assert(typeof page === 'number' && page > 0);
assert(typeof perPage === 'number' && perPage > 0);
2016-01-19 13:35:18 +01:00
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
appdb.exists(appId, function (error, exists) {
2019-10-24 10:39:47 -07:00
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (!exists) return callback(new BoxError(BoxError.NOT_FOUND));
2016-01-19 13:35:18 +01:00
2016-03-08 08:57:28 -08:00
backups.getByAppIdPaged(page, perPage, appId, function (error, results) {
2019-10-24 10:39:47 -07:00
if (error) return callback(error);
2016-01-19 13:35:18 +01:00
2016-03-08 08:57:28 -08:00
callback(null, results);
2016-01-19 13:35:18 +01:00
});
});
}
function restoreInstalledApps(callback) {
assert.strictEqual(typeof callback, 'function');
getAll(function (error, apps) {
2019-10-24 10:39:47 -07:00
if (error) return callback(error);
apps = apps.filter(app => app.installationState !== exports.ISTATE_ERROR); // remove errored apps. let them be 'repaired' by hand
2019-09-24 20:29:01 -07:00
apps = apps.filter(app => app.installationState !== exports.ISTATE_PENDING_RESTORE); // safeguard against tasks being created non-stop if we crash on startup
2019-09-24 20:29:01 -07:00
async.eachSeries(apps, function (app, iteratorDone) {
2017-11-17 22:29:13 -08:00
backups.getByAppIdPaged(1, 1, app.id, function (error, results) {
let installationState, restoreConfig;
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;
}
2019-09-24 20:29:01 -07:00
const task = {
args: { restoreConfig, overwriteDns: true },
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
debug(`restoreInstalledApps: marking ${app.fqdn} for restore using restore config ${JSON.stringify(restoreConfig)}`);
addTask(app.id, installationState, task, function (error, result) {
2019-09-24 20:29:01 -07:00
if (error) debug(`restoreInstalledApps: error marking ${app.fqdn} for restore: ${JSON.stringify(error)}`);
else debug(`restoreInstalledApps: marked ${app.id} for restore with taskId ${result.taskId}`);
2017-11-17 22:29:13 -08:00
2019-09-24 20:29:01 -07:00
iteratorDone(); // ignore error
2017-11-17 22:29:13 -08:00
});
});
}, callback);
});
}
function configureInstalledApps(callback) {
assert.strictEqual(typeof callback, 'function');
getAll(function (error, apps) {
2019-10-24 10:39:47 -07:00
if (error) return callback(error);
apps = apps.filter(app => app.installationState !== exports.ISTATE_ERROR); // remove errored apps. let them be 'repaired' by hand
2019-09-24 20:29:01 -07:00
apps = apps.filter(app => app.installationState !== exports.ISTATE_PENDING_CONFIGURE); // safeguard against tasks being created non-stop if we crash on startup
2019-09-24 20:29:01 -07:00
async.eachSeries(apps, function (app, iteratorDone) {
debug(`configureInstalledApps: marking ${app.fqdn} for reconfigure`);
2019-09-24 20:29:01 -07:00
const oldConfig = _.pick(app, 'location', 'domain', 'fqdn', 'alternateDomains', 'portBindings');
const task = {
args: { oldConfig, overwriteDns: true },
2019-09-24 20:29:01 -07:00
values: {},
scheduleNow: false, // task will be scheduled by autoRestartTasks when platform is ready
requireNullTaskId: false // ignore existing stale taskId
};
2019-09-24 20:29:01 -07:00
addTask(app.id, exports.ISTATE_PENDING_CONFIGURE, task, function (error, result) {
if (error) debug(`configureInstalledApps: error marking ${app.fqdn} for configure: ${JSON.stringify(error)}`);
else debug(`configureInstalledApps: marked ${app.id} for re-configure with taskId ${result.taskId}`);
iteratorDone(); // ignore error
});
}, callback);
});
}
2019-09-24 20:29:01 -07:00
// auto-restart app tasks after a crash
function schedulePendingTasks(callback) {
2019-09-24 10:28:50 -07:00
assert.strictEqual(typeof callback, 'function');
2019-09-24 20:29:01 -07:00
debug('schedulePendingTasks: scheduling app tasks');
2019-09-24 10:28:50 -07:00
getAll(function (error, result) {
if (error) return callback(error);
result.forEach(function (app) {
if (!app.taskId) return; // if not in any pending state, do nothing
2019-09-24 20:29:01 -07:00
debug(`schedulePendingTasks: schedule task for ${app.fqdn} ${app.id}: state=${app.installationState},taskId=${app.taskId}`);
2019-09-24 10:28:50 -07:00
scheduleTask(app.id, app.installationState, app.taskId, NOOP_CALLBACK);
});
callback(null);
});
}
function downloadFile(appId, filePath, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof filePath, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`downloadFile: ${filePath}`); // no need to escape filePath because we don't rely on bash
exec(appId, { 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
}
2017-08-20 18:44:26 -07:00
exec(appId, { 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);
return callback(null, stdoutStream, { filename: filename, size: size });
});
});
});
}
function uploadFile(appId, sourceFilePath, destFilePath, callback) {
assert.strictEqual(typeof appId, 'string');
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
callback(error);
});
// 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(appId, { 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);
});
}