Files
cloudron-box/src/apps.js

1825 lines
77 KiB
JavaScript
Raw Normal View History

'use strict';
exports = module.exports = {
AppsError: AppsError,
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,
setRobotsTxt: setRobotsTxt,
setCertificate: setCertificate,
setDebugMode: setDebugMode,
setEnvironment: setEnvironment,
setMailbox: setMailbox,
setLocation: setLocation,
setDataDir: setDataDir,
restore: restore,
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,
getAppConfig: getAppConfig,
getDataDir: getDataDir,
getIconPath: getIconPath,
downloadFile: downloadFile,
uploadFile: uploadFile,
PORT_TYPE_TCP: 'tcp',
PORT_TYPE_UDP: 'udp',
2019-08-30 12:42:38 -07:00
// error codes
ETASK_STOPPED: 'task_stopped', // user stopped a task
ETASK_CRASHED: 'task_crashed', // apptask crashed
2019-08-30 13:12:49 -07:00
// installation codes (keep in sync in UI)
ISTATE_PENDING_INSTALL: 'pending_install', // installs and fresh reinstalls
ISTATE_PENDING_CLONE: 'pending_clone', // clone
ISTATE_PENDING_CONFIGURE: 'pending_configure', // config (location, port) changes and on infra update
2019-09-08 16:57:08 -07:00
ISTATE_PENDING_CREATE_CONTAINER: 'pending_create_container',
ISTATE_PENDING_LOCATION_CHANGE: 'pending_location_change',
ISTATE_PENDING_DATA_DIR_MIGRATION: 'pending_data_dir_migration',
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_ERROR: 'error', // error executing last pending_* command
ISTATE_INSTALLED: 'installed', // app is installed
// run states
RSTATE_RUNNING: 'running',
RSTATE_PENDING_START: 'pending_start',
RSTATE_PENDING_STOP: 'pending_stop',
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'),
2016-06-13 18:11:11 -07:00
BackupsError = backups.BackupsError,
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.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'),
2018-04-29 11:20:12 -07:00
DomainsError = require('./domains.js').DomainsError,
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');
// http://dustinsenos.com/articles/customErrorsInNode
// http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
function AppsError(reason, errorOrMessage, details) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
assert(typeof details === 'object' || typeof details === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
2016-08-05 14:00:53 +02:00
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
this.details = details || {};
}
util.inherits(AppsError, Error);
AppsError.INTERNAL_ERROR = 'Internal Error';
AppsError.EXTERNAL_ERROR = 'External Error';
AppsError.ALREADY_EXISTS = 'Already Exists';
AppsError.NOT_FOUND = 'Not Found';
AppsError.BAD_FIELD = 'Bad Field';
AppsError.BAD_STATE = 'Bad State';
AppsError.PLAN_LIMIT = 'Plan Limit';
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) {
if (!/^[a-zA-Z0-9_]+$/.test(portName)) return new AppsError(AppsError.BAD_FIELD, `${portName} is not a valid environment variable`, { field: 'portBindings', portName: portName });
const hostPort = portBindings[portName];
if (!Number.isInteger(hostPort)) return new AppsError(AppsError.BAD_FIELD, `${hostPort} is not an integer`, { field: 'portBindings', portName: portName });
if (RESERVED_PORTS.indexOf(hostPort) !== -1) return new AppsError(AppsError.BAD_FIELD, `Port ${hostPort} is reserved.`, { field: 'portBindings', portName: portName });
if (hostPort <= 1023 || hostPort > 65535) return new AppsError(AppsError.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) {
if (!(portName in tcpPorts) && !(portName in udpPorts)) return new AppsError(AppsError.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) {
if (!Array.isArray(accessRestriction.users)) return new AppsError(AppsError.BAD_FIELD, 'users array property required');
if (!accessRestriction.users.every(function (e) { return typeof e === 'string'; })) return new AppsError(AppsError.BAD_FIELD, 'All users have to be strings');
}
if (accessRestriction.groups) {
if (!Array.isArray(accessRestriction.groups)) return new AppsError(AppsError.BAD_FIELD, 'groups array property required');
if (!accessRestriction.groups.every(function (e) { return typeof e === 'string'; })) return new AppsError(AppsError.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;
if (memoryLimit < min) return new AppsError(AppsError.BAD_FIELD, 'memoryLimit too small');
if (memoryLimit > max) return new AppsError(AppsError.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;
if ('cmd' in debugMode && debugMode.cmd !== null && !Array.isArray(debugMode.cmd)) return new AppsError(AppsError.BAD_FIELD, 'debugMode.cmd must be an array or null' );
if ('readonlyRootfs' in debugMode && typeof debugMode.readonlyRootfs !== 'boolean') return new AppsError(AppsError.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
if (robotsTxt.length > 4096) return new AppsError(AppsError.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 validateBackupFormat(format) {
2019-03-22 07:48:31 -07:00
assert.strictEqual(typeof format, 'string');
if (format === 'tgz' || format == 'rsync') return null;
return new AppsError(AppsError.BAD_FIELD, 'Invalid backup format');
}
2019-03-22 07:48:31 -07:00
function validateLabel(label) {
if (label === null) return null;
if (label.length > 128) return new AppsError(AppsError.BAD_FIELD, 'label must be less than 128', { field: 'label' });
2019-03-22 07:48:31 -07:00
return null;
}
function validateTags(tags) {
if (!Array.isArray(tags)) return new AppsError(AppsError.BAD_FIELD, 'tags must be an array of strings', { field: 'tags' });
if (tags.length > 64) return new AppsError(AppsError.BAD_FIELD, 'Can only set up to 64 tags', { field: 'tags' });
2019-03-22 07:48:31 -07:00
if (tags.some(tag => (!tag || typeof tag !== 'string'))) return new AppsError(AppsError.BAD_FIELD, 'tags must be an array of non-empty strings', { field: 'tags' });
if (tags.some(tag => tag.length > 128)) return new AppsError(AppsError.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) {
if (key.length > 512) return new AppsError(AppsError.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
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) return new AppsError(AppsError.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.resolve(dataDir) !== dataDir) return new AppsError(AppsError.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) {
if (!stat.isDirectory()) return new AppsError(AppsError.BAD_FIELD, `dataDir ${dataDir} is not a directory`, { field: 'dataDir' });
let entries = safe.fs.readdirSync(dataDir);
if (!entries) return new AppsError(AppsError.BAD_FIELD, `dataDir ${dataDir} could not be listed`, { field: 'dataDir' });
if (entries.length !== 0) return new AppsError(AppsError.BAD_FIELD, `dataDir ${dataDir} is not empty`, { field: 'dataDir' });
}
// backup logic relies on paths not overlapping (because it recurses)
if (dataDir.startsWith(paths.APPS_DATA_DIR)) return new AppsError(AppsError.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' ];
if (fhs.some((p) => dataDir.startsWith(p))) return new AppsError(AppsError.BAD_FIELD, `dataDir ${dataDir} cannot be placed inside this location`, { field: 'dataDir' });
return null;
}
function getDuplicateErrorDetails(errorMessage, location, domainObject, portBindings, alternateDomains) {
assert.strictEqual(typeof errorMessage, 'string');
assert.strictEqual(typeof location, 'string');
2019-03-19 20:43:42 -07:00
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof portBindings, 'object');
2019-06-05 16:01:44 +02:00
assert(Array.isArray(alternateDomains));
var match = errorMessage.match(/ER_DUP_ENTRY: Duplicate entry '(.*)' for key '(.*)'/);
if (!match) {
debug('Unexpected SQL error message.', errorMessage);
return new AppsError(AppsError.INTERNAL_ERROR, new Error(errorMessage));
}
2019-06-05 16:01:44 +02:00
// check if the location or alternateDomains conflicts
2019-03-19 20:43:42 -07:00
if (match[2] === 'subdomain') {
2019-06-05 16:01:44 +02:00
// mysql reports a unique conflict with a dash: eg. domain:example.com subdomain:test => test-example.com
2019-09-01 21:34:27 -07:00
if (match[1] === `${location}-${domainObject.domain}`) {
return new AppsError(AppsError.ALREADY_EXISTS, `Domain '${domains.fqdn(location, domainObject)}' is in use`, { location: location, domain: domainObject.domain });
2019-09-01 21:34:27 -07:00
}
2019-06-05 16:01:44 +02:00
2019-09-01 21:34:27 -07:00
for (let d of alternateDomains) {
if (match[1] !== `${d.subdomain}-${d.domain}`) continue;
2019-06-05 16:01:44 +02:00
return new AppsError(AppsError.ALREADY_EXISTS, `Alternate domain '${d.subdomain}.${d.domain}' is in use`, { location: d.subdomain, domain: d.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 AppsError(AppsError.ALREADY_EXISTS, `Port ${match[1]} is reserved`, { portName });
}
2019-09-03 15:17:48 -07:00
if (match[2] === 'dataDir') {
return new AppsError(AppsError.BAD_FIELD, `Data directory ${match[1]} is in use`, { field: 'dataDir' });
}
return new AppsError(AppsError.ALREADY_EXISTS, `${match[2]} '${match[1]}' is in use`);
}
// app configs that is useful for 'archival' into the app backup config.json
2016-06-13 13:48:53 -07:00
function getAppConfig(app) {
return {
manifest: app.manifest,
location: app.location,
domain: app.domain,
fqdn: app.fqdn,
2016-06-13 13:48:53 -07:00
accessRestriction: app.accessRestriction,
portBindings: app.portBindings,
memoryLimit: app.memoryLimit,
robotsTxt: app.robotsTxt,
2018-06-29 16:37:53 +02:00
sso: app.sso,
alternateDomains: app.alternateDomains || [],
env: app.env,
dataDir: app.dataDir
2016-06-13 13:48:53 -07:00
};
}
function getDataDir(app, dataDir) {
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',
2019-04-11 17:06:10 +02:00
'sso', 'debugMode', 'robotsTxt', '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);
callback(new AppsError(AppsError.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) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 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 === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
2018-01-09 21:03:59 -08:00
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
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 === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
2018-09-22 16:09:11 -07:00
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 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) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
docker.inspect(containerId, function (error, result) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
const appId = safe.query(result, 'Config.Labels.appId', null);
if (!appId) return callback(new AppsError(AppsError.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; });
if (!app) return callback(new AppsError(AppsError.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(new AppsError(AppsError.INTERNAL_ERROR, 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) {
2016-06-07 15:36:45 -07:00
if (!appStoreId && !manifest) return callback(new AppsError(AppsError.BAD_FIELD, 'Neither manifest nor appStoreId provided'));
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) {
if (error && !error.response) return callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Network error downloading manifest:' + error.message));
if (result.statusCode !== 200) return callback(new AppsError(AppsError.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, args, values, callback) {
2019-08-28 15:00:55 -07:00
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof args, 'object');
assert.strictEqual(typeof values, 'object');
2019-08-28 15:00:55 -07:00
assert.strictEqual(typeof callback, 'function');
2019-08-27 16:12:24 -07:00
assert(values.installationState || values.runState);
tasks.add(tasks.TASK_APP, [ appId, args ], function (error, taskId) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
2019-08-30 11:18:42 -07:00
values.error = null;
values.taskId = taskId;
appdb.setTask(appId, values, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new AppsError(AppsError.ALREADY_EXISTS, error.message));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE, 'Another task is scheduled for this app')); // could be because app went away OR a taskId exists
2019-08-26 15:55:57 -07:00
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
2019-08-28 15:00:55 -07:00
appTaskManager.scheduleTask(appId, taskId, function (error) {
2019-08-29 13:43:45 -07:00
debug(`scheduleTask: task ${taskId} of $${appId} completed`);
2019-09-03 15:55:57 -07:00
if (error && (error.code === tasks.ECRASHED || error.code === tasks.ESTOPPED)) { // if task crashed, update the error
2019-08-28 13:10:04 -07:00
debug(`Apptask crashed/stopped: ${error.message}`);
2019-08-30 12:42:38 -07:00
const code = error.crashed ? exports.ETASK_CRASHED : exports.ETASK_STOPPED;
2019-08-30 13:12:49 -07:00
appdb.update(appId, { installationState: exports.ISTATE_ERROR, error: { code: code, message: error.message }, taskId: null }, NOOP_CALLBACK);
2019-09-03 15:55:57 -07:00
} else if (values.installationState !== exports.ISTATE_PENDING_UNINSTALL) { // clear out taskId since it's done
2019-08-29 13:43:45 -07:00
appdb.update(appId, { taskId: null }, NOOP_CALLBACK);
2019-08-28 13:10:04 -07:00
}
});
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
});
});
}
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,
robotsTxt = data.robotsTxt || null,
enableBackup = 'enableBackup' in data ? data.enableBackup : true,
enableAutomaticUpdate = 'enableAutomaticUpdate' in data ? data.enableAutomaticUpdate : true,
backupId = data.backupId || null,
backupFormat = data.backupFormat || 'tgz',
alternateDomains = data.alternateDomains || [],
env = data.env || {},
2019-03-22 07:48:31 -07:00
mailboxName = data.mailboxName || '',
label = data.label || null,
2019-04-18 13:06:00 +02:00
tags = data.tags || [];
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);
if (error) return callback(new AppsError(AppsError.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);
error = validateRobotsTxt(robotsTxt);
if (error) return callback(error);
error = validateBackupFormat(backupFormat);
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);
2016-11-19 21:37:39 +05:30
if ('sso' in data && !('optionalSso' in manifest)) return callback(new AppsError(AppsError.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);
if (error) return callback(new AppsError(AppsError.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) {
if (!validator.isBase64(icon)) return callback(new AppsError(AppsError.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'))) {
return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving icon:' + safe.error.message));
}
}
2018-01-09 21:03:59 -08:00
domains.get(domain, function (error, domainObject) {
2018-04-29 11:20:12 -07:00
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
2018-01-09 21:03:59 -08:00
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
2015-10-28 22:09:19 +01:00
2018-08-30 20:05:08 -07:00
error = domains.validateHostname(location, domainObject);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Bad location: ' + error.message, { field: 'location' }));
2018-01-26 19:31:06 -08:00
if (cert && key) {
2018-11-05 22:36:16 -08:00
error = reverseProxy.validateCertificate(location, domainObject, { cert, key });
if (error) return callback(new AppsError(AppsError.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,
robotsTxt: robotsTxt,
alternateDomains: alternateDomains,
2019-09-06 12:24:01 -07:00
env: env,
label: label,
tags: tags
};
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 === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error.message, location, domainObject, portBindings, data.alternateDomains));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, error.message));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 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) {
2018-11-05 22:36:16 -08:00
let error = reverseProxy.setAppCertificateSync(location, domainObject, { cert, key });
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error setting cert: ' + error.message));
2018-01-09 21:03:59 -08:00
}
2016-05-01 21:37:08 -07:00
const restoreConfig = backupId ? { backupId: backupId, backupFormat: backupFormat } : null;
2019-08-30 13:12:49 -07:00
scheduleTask(appId, { restoreConfig }, { installationState: exports.ISTATE_PENDING_INSTALL }, function (error, result) {
if (error) return callback(error);
2019-09-06 12:24:01 -07:00
const newApp = _.extend({ fqdn: domains.fqdn(location, domainObject) }, data, { appStoreId, manifest, location, domain, portBindings });
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 && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
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 && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
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 && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
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) {
if (!validator.isBase64(icon)) return callback(new AppsError(AppsError.BAD_FIELD, 'icon is not base64', { field: 'icon' }));
if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.user.png'), Buffer.from(icon, 'base64'))) {
return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving icon:' + safe.error.message));
}
} 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 = validateMemoryLimit(app.manifest, memoryLimit);
if (error) return callback(error);
scheduleTask(appId, {}, { installationState: exports.ISTATE_PENDING_CREATE_CONTAINER, memoryLimit: memoryLimit }, function (error, result) {
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 = validateEnv(env);
if (error) return callback(error);
2019-09-09 15:35:02 -07:00
scheduleTask(appId, {}, { installationState: exports.ISTATE_PENDING_CREATE_CONTAINER, env: env }, 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 = validateDebugMode(debugMode);
if (error) return callback(error);
2019-09-09 15:35:02 -07:00
scheduleTask(appId, {}, { installationState: exports.ISTATE_PENDING_CREATE_CONTAINER, debugMode: debugMode }, 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);
if (mailboxName) {
error = mail.validateName(mailboxName);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message, { field: 'mailboxName' }));
} else {
mailboxName = mailboxNameForLocation(app.location, app.manifest);
}
2019-09-09 15:58:58 -07:00
scheduleTask(appId, {}, { installationState: exports.ISTATE_PENDING_CREATE_CONTAINER, mailboxName: mailboxName }, 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 && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
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 && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: app, enableAutomaticUpdate: enable });
callback();
});
});
}
function setRobotsTxt(appId, robotsTxt, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
2019-09-09 21:41:55 -07:00
assert(robotsTxt === null || typeof robotsTxt === '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 = validateRobotsTxt(robotsTxt);
if (error) return callback(error);
2019-09-09 21:41:55 -07:00
reverseProxy.writeAppConfig(_.extend({}, app, { robotsTxt }), function (error) {
if (error) return callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Error writing nginx config'));
appdb.update(appId, { robotsTxt: robotsTxt }, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
2019-09-08 16:57:08 -07:00
2019-09-09 21:41:55 -07:00
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: app, robotsTxt: robotsTxt });
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) {
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
if (bundle.cert && bundle.key) {
error = reverseProxy.validateCertificate(app.location, domainObject, { cert: bundle.cert, key: bundle.key });
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message, { field: 'cert' }));
}
error = reverseProxy.setAppCertificateSync(app.location, domainObject, { cert: bundle.cert, key: bundle.key });
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error setting cert: ' + error.message));
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);
let values = {
installationState: exports.ISTATE_PENDING_LOCATION_CHANGE,
// these are intentionally reset, if not set
portBindings: null,
alternateDomains: []
};
values.location = data.location.toLowerCase();
values.domain = data.domain.toLowerCase();
if ('portBindings' in data) {
error = validatePortBindings(data.portBindings, app.manifest);
if (error) return callback(error);
values.portBindings = translatePortBindings(data.portBindings || null, app.manifest);
}
if ('alternateDomains' in data) {
// TODO validate all subdomains [{ domain: '', subdomain: ''}]
values.alternateDomains = data.alternateDomains;
}
2019-09-09 15:58:58 -07:00
// 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
domains.get(values.domain, function (error, domainObject) {
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
error = domains.validateHostname(values.location, domainObject);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Bad location: ' + error.message, { field: 'location' }));
scheduleTask(appId, { oldConfig: getAppConfig(app) }, values, function (error, result) {
if (error && error.reason === AppsError.ALREADY_EXISTS) error = getDuplicateErrorDetails(error.message, values.location, domainObject, data.portBindings, app.alternateDomains);
if (error) return callback(error);
2019-09-10 13:36:41 -07:00
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: app, location: values.location, domain: values.domain, portBindings: values.portBindings, alternateDomains: values.alternateDomain, taskId: result.taskId });
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 = validateDataDir(dataDir);
if (error) return callback(error);
scheduleTask(appId, { oldConfig: getAppConfig(app) }, { installationState: exports.ISTATE_PENDING_DATA_DIR_MIGRATION, dataDir: dataDir }, function (error, result) {
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}`);
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);
if (app.taskId) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState} / ${app.runState}`));
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-08-29 10:59:05 -07:00
var updateConfig = {
skipNotification: data.force,
skipBackup: data.force
};
2019-01-11 13:19:28 -08:00
error = manifestFormat.parse(manifest);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest error:' + error.message));
2019-01-11 13:19:28 -08:00
error = checkManifestConstraints(manifest);
if (error) return callback(error);
2019-01-11 13:19:28 -08:00
updateConfig.manifest = 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) {
if (!data.force) return callback(new AppsError(AppsError.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)) {
if (!data.force) return callback(new AppsError(AppsError.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) {
if (!validator.isBase64(data.icon)) return callback(new AppsError(AppsError.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-01-11 13:19:28 -08:00
return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving icon:' + safe.error.message));
}
} 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
if (app.debugMode && !data.force) return callback(new AppsError(AppsError.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
}
2019-08-30 13:12:49 -07:00
scheduleTask(appId, { updateConfig: updateConfig }, { installationState: exports.ISTATE_PENDING_UPDATE }, function (error, result) {
if (error) return callback(error);
2019-09-06 12:24:01 -07:00
eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId: appId, toManifest: manifest, fromManifest: app.manifest, force: data.force, app: app, 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);
});
}
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);
if (app.taskId) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState} / ${app.runState}`));
// 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) {
if (error && error.reason === BackupsError.NOT_FOUND) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
2016-06-13 18:11:11 -07:00
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
2016-06-13 18:11:11 -07:00
if (!backupInfo.manifest) callback(new AppsError(AppsError.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);
let values = {
2019-08-30 13:12:49 -07:00
installationState: exports.ISTATE_PENDING_RESTORE,
manifest: backupInfo.manifest
};
const restoreConfig = data.backupId ? { backupId: data.backupId, backupFormat: backupInfo.format, oldManifest: app.manifest } : null; // when null, apptask simply reinstalls
scheduleTask(appId, { restoreConfig }, values, 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 });
});
});
});
}
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
if (error.reason === AppstoreError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, error.message));
if (error && error.reason === AppstoreError.PLAN_LIMIT) return callback(new AppsError(AppsError.PLAN_LIMIT, error.message));
if (error && error.reason === AppstoreError.INVALID_TOKEN) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
if (error && error.reason === AppstoreError.LICENSE_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
if (error && error.reason === AppstoreError.NOT_REGISTERED) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
2019-05-05 10:31:42 -07:00
if (error && error.reason === AppstoreError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
callback(new AppsError(AppsError.INTERNAL_ERROR, error));
});
});
}
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 || '';
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) {
2016-06-17 17:12:55 -05:00
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
if (error && error.reason === BackupsError.NOT_FOUND) return callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Backup not found'));
2016-06-17 17:12:55 -05:00
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (!backupInfo.manifest) callback(new AppsError(AppsError.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);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message, { field: 'mailboxName' }));
} else {
mailboxName = mailboxNameForLocation(location, manifest);
}
2018-01-11 10:59:30 -08:00
domains.get(domain, function (error, domainObject) {
2018-04-29 11:20:12 -07:00
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new AppsError(AppsError.EXTERNAL_ERROR, 'No such domain'));
2018-01-11 10:59:30 -08:00
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
2016-06-17 17:12:55 -05:00
2018-08-30 20:05:08 -07:00
error = domains.validateHostname(location, domainObject);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Bad location: ' + error.message));
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,
memoryLimit: app.memoryLimit,
accessRestriction: app.accessRestriction,
sso: !!app.sso,
mailboxName: mailboxName,
enableBackup: app.enableBackup,
robotsTxt: app.robotsTxt,
env: app.env
};
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 === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error.message, location, domainObject, portBindings, []));
2016-06-17 17:12:55 -05:00
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
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
const restoreConfig = { backupId: backupId, backupFormat: backupInfo.format };
2019-08-30 13:12:49 -07:00
scheduleTask(newAppId, { restoreConfig }, { installationState: exports.ISTATE_PENDING_CLONE }, 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 });
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);
if (app.taskId) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState} / ${app.runState}`));
2019-05-06 14:29:56 -07:00
appstore.unpurchaseApp(appId, { appstoreId: app.appStoreId, manifestId: app.manifest.id }, function (error) {
2017-04-13 01:11:20 -07:00
if (error && error.reason === AppstoreError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
if (error && error.reason === AppstoreError.INVALID_TOKEN) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
if (error && error.reason === AppstoreError.LICENSE_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
2017-04-13 01:11:20 -07:00
if (error && error.reason === AppstoreError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
2019-08-30 13:12:49 -07:00
scheduleTask(appId, { /* args */ }, { installationState: exports.ISTATE_PENDING_UNINSTALL }, 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);
if (app.taskId) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState} / ${app.runState}`));
2019-08-30 13:12:49 -07:00
scheduleTask(appId, { /* args */ }, { runState: exports.RSTATE_PENDING_START }, callback);
});
}
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);
if (app.taskId) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState} / ${app.runState}`));
2019-08-30 13:12:49 -07:00
scheduleTask(appId, { /* args */ }, { runState: exports.RSTATE_PENDING_STOP }, callback);
});
}
function checkManifestConstraints(manifest) {
2016-06-13 18:02:57 -07:00
assert(manifest && typeof manifest === 'object');
2019-06-05 16:01:44 +02:00
if (manifest.manifestVersion > 2) return new AppsError(AppsError.BAD_FIELD, 'Manifest version must be <= 2');
if (!manifest.dockerImage) return new AppsError(AppsError.BAD_FIELD, 'Missing dockerImage'); // dockerImage is optional in manifest
if (semver.valid(manifest.maxBoxVersion) && semver.gt(constants.VERSION, manifest.maxBoxVersion)) {
return new AppsError(AppsError.BAD_FIELD, 'Box version exceeds Apps maxBoxVersion');
}
if (semver.valid(manifest.minBoxVersion) && semver.gt(manifest.minBoxVersion, constants.VERSION)) {
return new AppsError(AppsError.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) {
2016-02-12 12:32:51 -08:00
return callback(new AppsError(AppsError.BAD_STATE, 'App not installed or running'));
}
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) {
2018-10-27 14:15:52 -07:00
if (error && error.statusCode === 409) return callback(new AppsError(AppsError.BAD_STATE, error.message)); // container restarting/not running
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
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 */) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 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);
if (app.taskId) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState} / ${app.runState}`));
2019-08-30 13:12:49 -07:00
scheduleTask(appId, { /* args */ }, { installationState: exports.ISTATE_PENDING_BACKUP }, (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) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (!exists) return callback(new AppsError(AppsError.NOT_FOUND));
2016-03-08 08:57:28 -08:00
backups.getByAppIdPaged(page, perPage, appId, function (error, results) {
2016-01-19 13:35:18 +01:00
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
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) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
async.map(apps, function (app, iteratorDone) {
2017-11-17 22:29:13 -08:00
backups.getByAppIdPaged(1, 1, app.id, function (error, results) {
const restoreConfig = !error && results.length ? { backupId: results[0].id, backupFormat: results[0].format, oldManifest: app.manifest } : null;
debug(`marking ${app.fqdn} for restore using restore config ${JSON.stringify(restoreConfig)}`);
appdb.update(app.id, { taskId: null }, function (error) { // clear any stale taskId
if (error) debug(`Error marking ${app.fqdn} for restore: ${JSON.stringify(error)}`);
2017-11-17 22:29:13 -08:00
2019-08-30 13:12:49 -07:00
scheduleTask(app.id, { restoreConfig }, { installationState: exports.ISTATE_PENDING_RESTORE }, () => iteratorDone()); // always succeed
2017-11-17 22:29:13 -08:00
});
});
}, callback);
});
}
function configureInstalledApps(callback) {
assert.strictEqual(typeof callback, 'function');
getAll(function (error, apps) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
async.map(apps, function (app, iteratorDone) {
debug(`marking ${app.fqdn} for reconfigure`);
appdb.update(app.id, { taskId: null }, function (error) { // clear any stale taskId
if (error) debug(`Error marking ${app.fqdn} for reconfigure: ${JSON.stringify(error)}`);
2019-08-30 13:12:49 -07:00
scheduleTask(app.id, { oldConfig: getAppConfig(app) }, { installationState: exports.ISTATE_PENDING_CONFIGURE }, () => iteratorDone()); // always succeed
});
}, callback);
});
}
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('-');
if (parts.length !== 2) return callback(new AppsError(AppsError.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);
if (isNaN(size)) return callback(new AppsError(AppsError.NOT_FOUND, 'file does not exist'));
} else if (type === 'directory') {
cmd = ['tar', 'zcf', '-', '-C', filePath, '.'];
filename = path.basename(filePath) + '.tar.gz';
size = 0; // unknown
} else {
return callback(new AppsError(AppsError.NOT_FOUND, 'only files or dirs can be downloaded'));
}
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);
});
}