Files
cloudron-box/src/apps.js

1547 lines
64 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,
configure: configure,
uninstall: uninstall,
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,
downloadFile: downloadFile,
uploadFile: uploadFile,
2018-06-28 16:48:04 -07:00
setOwner: setOwner,
transferOwnership: transferOwnership,
PORT_TYPE_TCP: 'tcp',
PORT_TYPE_UDP: 'udp',
// exported for testing
2015-10-15 12:26:48 +02:00
_validatePortBindings: validatePortBindings,
_validateAccessRestriction: validateAccessRestriction,
_translatePortBindings: translatePortBindings
};
var appdb = require('./appdb.js'),
appstore = require('./appstore.js'),
2017-04-13 01:11:20 -07:00
AppstoreError = require('./appstore.js').AppstoreError,
assert = require('assert'),
async = require('async'),
backups = require('./backups.js'),
2016-06-13 18:11:11 -07:00
BackupsError = backups.BackupsError,
config = require('./config.js'),
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'),
2015-11-02 11:20:50 -08:00
spawn = require('child_process').spawn,
split = require('split'),
superagent = require('superagent'),
taskmanager = require('./taskmanager.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) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === '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;
}
}
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.PORT_RESERVED = 'Port Reserved';
AppsError.PORT_CONFLICT = 'Port Conflict';
AppsError.PLAN_LIMIT = 'Plan Limit';
AppsError.ACCESS_DENIED = 'Access denied';
2015-10-27 16:36:09 +01:00
AppsError.BAD_CERTIFICATE = 'Invalid certificate';
// 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) */
config.get('port'), /* app server (lo) */
2016-04-15 12:33:54 -07:00
config.get('sysadminPort'), /* sysadmin app server (lo) */
2016-05-24 00:52:35 -07:00
config.get('smtpPort'), /* internal smtp port (lo) */
2015-09-16 10:12:59 -07:00
config.get('ldapPort'), /* ldap server (lo) */
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`);
const hostPort = portBindings[portName];
if (!Number.isInteger(hostPort)) return new AppsError(AppsError.BAD_FIELD, `${hostPort} is not an integer`);
if (RESERVED_PORTS.indexOf(hostPort) !== -1) return new AppsError(AppsError.PORT_RESERVED, String(hostPort));
if (hostPort <= 1023 || hostPort > 65535) return new AppsError(AppsError.BAD_FIELD, `${hostPort} is not in permitted range`);
}
// 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}`);
}
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');
// 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');
return null;
}
function validateTags(tags) {
if (!Array.isArray(tags)) return new AppsError(AppsError.BAD_FIELD, 'tags must be an array of strings');
2019-03-22 07:48:31 -07:00
if (tags.length > 64) return new AppsError(AppsError.BAD_FIELD, 'Can only set up to 64 tags');
if (tags.some(tag => (!tag || typeof tag !== 'string'))) return new AppsError(AppsError.BAD_FIELD, 'tags must be an array of non-empty strings');
if (tags.some(tag => tag.length > 128)) return new AppsError(AppsError.BAD_FIELD, 'tag must be less than 128');
2019-03-22 07:48:31 -07:00
return null;
}
2018-10-18 11:19:32 -07:00
function validateEnv(env) {
for (let key in env) {
if (key.length > 512) return new AppsError(AppsError.BAD_FIELD, 'Max env var key length is 512');
// 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`);
}
return null;
}
function validateDataDir(dataDir) {
if (dataDir === '') return null; // revert back to default dataDir
if (path.resolve(dataDir) !== dataDir) return new AppsError(AppsError.BAD_FIELD, 'dataDir must be an absolute path');
// 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`);
let entries = safe.fs.readdirSync(dataDir);
if (!entries) return new AppsError(AppsError.BAD_FIELD, `dataDir ${dataDir} could not be listed`);
if (entries.length !== 0) return new AppsError(AppsError.BAD_FIELD, `dataDir ${dataDir} is not empty`);
}
// 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`);
// 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`);
return null;
}
2019-06-05 16:01:44 +02:00
function getDuplicateErrorDetails(error, location, domainObject, portBindings, alternateDomains) {
2019-03-19 20:43:42 -07:00
assert.strictEqual(error.reason, DatabaseError.ALREADY_EXISTS);
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 = error.message.match(/ER_DUP_ENTRY: Duplicate entry '(.*)' for key '(.*)'/);
if (!match) {
2017-02-07 10:48:51 -08:00
debug('Unexpected SQL error message.', error);
return new AppsError(AppsError.INTERNAL_ERROR, error);
}
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
if (match[1] === `${location}-${domainObject.domain}`) return new AppsError(AppsError.ALREADY_EXISTS, `Domain '${domains.fqdn(location, domainObject)}' is in use`);
// check alternateDomains
let tmp = alternateDomains.filter(function (d) {
return match[1] === `${d.subdomain}-${d.domain}`;
});
if (tmp.length > 0) return new AppsError(AppsError.ALREADY_EXISTS, `Alternate domain '${tmp[0].subdomain}.${tmp[0].domain}' is in use`);
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.PORT_CONFLICT, match[1]);
}
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,
'id', 'appStoreId', 'installationState', 'installationProgress', 'runState', 'health',
'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-04-24 14:25:23 +02:00
'label', 'alternateDomains', 'ownerId', 'env', 'enableAutomaticUpdate', 'dataDir');
}
2019-02-11 14:37:49 -08:00
// non-admins can only see these
function removeRestrictedFields(app) {
return _.pick(app,
2018-08-03 18:15:22 -07:00
'id', 'appStoreId', 'installationState', 'installationProgress', 'runState', 'health', 'ownerId',
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;
}
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
});
});
}
2019-03-06 11:15:12 -08:00
function getByIpAddress(ip, callback) {
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
docker.getContainerIdByIp(ip, function (error, containerId) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
getByContainerId(containerId, callback);
});
}
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 = config.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';
}
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',
ownerId = data.ownerId,
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(error);
} 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'));
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));
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 });
2018-01-26 19:31:06 -08:00
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
}
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,
restoreConfig: backupId ? { backupId: backupId, backupFormat: backupFormat } : null,
enableBackup: enableBackup,
enableAutomaticUpdate: enableAutomaticUpdate,
robotsTxt: robotsTxt,
alternateDomains: alternateDomains,
env: env
};
appdb.add(appId, appStoreId, manifest, location, domain, ownerId, translatePortBindings(portBindings, manifest), data, function (error) {
2019-06-05 16:01:44 +02:00
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error, 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
2018-01-09 21:03:59 -08:00
taskmanager.restartAppTask(appId);
2016-09-23 17:23:45 -07:00
// fetch fresh app object for eventlog
get(appId, function (error, result) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId: appId, app: result });
2018-01-09 21:03:59 -08:00
callback(null, { id : appId });
});
2018-01-09 21:03:59 -08:00
});
});
});
});
}
2018-09-04 16:37:08 -07:00
function configure(appId, data, user, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
2016-06-04 16:32:27 -07:00
assert(data && typeof data === 'object');
2018-09-04 16:37:08 -07:00
assert(user && typeof user === 'object');
2016-05-01 21:37:08 -07:00
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
get(appId, function (error, app) {
if (error) return callback(error);
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
let domain, location, portBindings, values = { };
if ('location' in data && 'domain' in data) {
location = values.location = data.location.toLowerCase();
domain = values.domain = data.domain.toLowerCase();
} else {
location = app.location;
domain = app.domain;
}
2017-11-02 22:17:44 +01:00
2016-06-04 18:07:02 -07:00
if ('accessRestriction' in data) {
values.accessRestriction = data.accessRestriction;
error = validateAccessRestriction(values.accessRestriction);
if (error) return callback(error);
}
2015-10-27 16:36:09 +01:00
2016-06-04 18:07:02 -07:00
if ('portBindings' in data) {
error = validatePortBindings(data.portBindings, app.manifest);
2016-06-04 18:07:02 -07:00
if (error) return callback(error);
values.portBindings = translatePortBindings(data.portBindings, app.manifest);
portBindings = data.portBindings;
2016-06-04 18:07:02 -07:00
} else {
portBindings = app.portBindings;
}
2016-06-04 18:07:02 -07:00
if ('memoryLimit' in data) {
values.memoryLimit = data.memoryLimit;
error = validateMemoryLimit(app.manifest, values.memoryLimit);
if (error) return callback(error);
}
if ('debugMode' in data) {
values.debugMode = data.debugMode;
error = validateDebugMode(values.debugMode);
if (error) return callback(error);
}
if ('robotsTxt' in data) {
values.robotsTxt = data.robotsTxt || null;
error = validateRobotsTxt(values.robotsTxt);
if (error) return callback(error);
}
if ('mailboxName' in data) {
if (data.mailboxName) {
error = mail.validateName(data.mailboxName);
if (error) return callback(error);
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
values.mailboxName = data.mailboxName;
} else {
values.mailboxName = mailboxNameForLocation(location, app.manifest);
}
2018-09-25 17:04:17 -07:00
} else { // keep existing name or follow the new location
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
values.mailboxName = app.mailboxName.endsWith('.app') ? mailboxNameForLocation(location, app.manifest) : app.mailboxName;
}
if ('alternateDomains' in data) {
// TODO validate all subdomains [{ domain: '', subdomain: ''}]
values.alternateDomains = data.alternateDomains;
}
if ('env' in data) {
values.env = data.env;
2018-10-18 11:19:32 -07:00
error = validateEnv(data.env);
if (error) return callback(error);
}
if ('dataDir' in data && data.dataDir !== app.dataDir) {
error = validateDataDir(data.dataDir);
if (error) return callback(error);
values.dataDir = data.dataDir;
}
2019-03-22 07:48:31 -07:00
if ('label' in data) {
error = validateLabel(data.label);
if (error) return callback(error);
values.label = data.label;
}
if ('tags' in data) {
error = validateTags(data.tags);
if (error) return callback(error);
values.tags = data.tags;
}
2019-05-17 12:50:08 -07:00
if ('icon' in data) {
if (data.icon) {
if (!validator.isBase64(data.icon)) return callback(new AppsError(AppsError.BAD_FIELD, 'icon is not base64'));
if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.user.png'), Buffer.from(data.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'));
}
}
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));
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));
2018-01-09 21:03:59 -08:00
// save cert to boxdata/certs. TODO: move this to apptask when we have a real task queue
if ('cert' in data && 'key' in data) {
if (data.cert && data.key) {
2018-11-05 22:36:16 -08:00
error = reverseProxy.validateCertificate(location, domainObject, { cert: data.cert, key: data.key });
2018-01-09 21:03:59 -08:00
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
}
2018-11-05 22:36:16 -08:00
error = reverseProxy.setAppCertificateSync(location, domainObject, { cert: data.cert, key: data.key });
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error setting cert: ' + error.message));
2018-01-09 21:03:59 -08:00
}
if ('enableBackup' in data) values.enableBackup = data.enableBackup;
if ('enableAutomaticUpdate' in data) values.enableAutomaticUpdate = data.enableAutomaticUpdate;
2018-01-09 21:03:59 -08:00
values.oldConfig = getAppConfig(app);
debug(`configure: id:${appId}`);
2018-01-09 21:03:59 -08:00
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
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_CONFIGURE, values, function (error) {
2019-06-05 16:01:44 +02:00
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error, location, domainObject, portBindings, data.alternateDomains));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
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
taskmanager.restartAppTask(appId);
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
// fetch fresh app object for eventlog
get(appId, function (error, result) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
2018-01-09 21:03:59 -08:00
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
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: result });
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);
2018-01-09 21:03:59 -08:00
});
});
});
});
}
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);
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
var updateConfig = { };
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'));
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
}
2017-11-16 12:36:07 -08:00
appdb.setInstallationCommand(appId, data.force ? appdb.ISTATE_PENDING_FORCE_UPDATE : appdb.ISTATE_PENDING_UPDATE, { updateConfig: updateConfig }, function (error) {
2016-06-04 19:19:00 -07:00
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
2016-06-04 19:19:00 -07:00
taskmanager.restartAppTask(appId);
eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId: appId, toManifest: manifest, fromManifest: app.manifest, force: data.force, app: app });
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);
2016-06-04 19:19:00 -07:00
callback(null);
});
});
});
}
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);
// 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);
var values = {
restoreConfig: data.backupId ? { backupId: data.backupId, backupFormat: backupInfo.format } : null, // when null, apptask simply reinstalls
manifest: backupInfo.manifest,
2016-06-13 13:48:53 -07:00
oldConfig: getAppConfig(app)
};
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_RESTORE, values, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
taskmanager.restartAppTask(appId);
eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { app: app, backupId: backupInfo.id, fromManifest: app.manifest, toManifest: backupInfo.manifest });
2016-05-01 21:37:08 -07:00
callback(null);
});
});
});
}
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,
ownerId = data.ownerId,
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');
assert(ownerId === null || typeof ownerId === 'string');
2016-06-17 17:12:55 -05:00
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
const manifest = backupInfo.manifest;
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(error);
} 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 = {
installationState: appdb.ISTATE_PENDING_CLONE,
memoryLimit: app.memoryLimit,
accessRestriction: app.accessRestriction,
restoreConfig: { backupId: backupId, backupFormat: backupInfo.format },
sso: !!app.sso,
mailboxName: mailboxName,
enableBackup: app.enableBackup,
robotsTxt: app.robotsTxt,
env: app.env
};
appdb.add(newAppId, app.appStoreId, manifest, location, domain, ownerId, translatePortBindings(portBindings, manifest), data, function (error) {
2019-06-05 16:01:44 +02:00
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error, 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
taskmanager.restartAppTask(newAppId);
2016-06-17 17:12:55 -05:00
// fetch fresh app object for eventlog
get(appId, function (error, result) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_APP_CLONE, auditSource, { appId: newAppId, oldAppId: appId, backupId: backupId, oldApp: app, newApp: result });
2016-06-17 17:12:55 -05:00
callback(null, { id : newAppId });
});
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:%s', appId);
get(appId, function (error, app) {
2016-08-04 09:38:00 +02:00
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) {
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));
2017-02-13 15:19:17 -08:00
taskmanager.stopAppTask(appId, function () {
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_UNINSTALL, 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));
2016-08-04 09:38:00 +02:00
eventlog.add(eventlog.ACTION_APP_UNINSTALL, auditSource, { appId: appId, app: app });
2016-05-01 21:37:08 -07:00
2017-02-13 15:19:17 -08:00
taskmanager.startAppTask(appId, callback);
});
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);
appdb.setRunCommand(appId, appdb.RSTATE_PENDING_START, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
taskmanager.restartAppTask(appId);
callback(null);
});
}
function stop(appId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
debug('Will stop app with id:%s', appId);
appdb.setRunCommand(appId, appdb.RSTATE_PENDING_STOP, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
taskmanager.restartAppTask(appId);
callback(null);
});
}
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(config.version(), manifest.maxBoxVersion)) {
return new AppsError(AppsError.BAD_FIELD, 'Box version exceeds Apps maxBoxVersion');
}
if (semver.valid(manifest.minBoxVersion) && semver.gt(manifest.minBoxVersion, config.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);
2016-02-12 12:32:51 -08:00
if (app.installationState !== appdb.ISTATE_INSTALLED || app.runState !== appdb.RSTATE_RUNNING) {
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');
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));
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_BACKUP, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
taskmanager.restartAppTask(appId);
callback(null);
});
});
}
2016-01-19 13:35:18 +01:00
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) {
var restoreConfig = !error && results.length ? { backupId: results[0].id, backupFormat: results[0].format } : null;
debug(`marking ${app.fqdn} for restore using restore config ${JSON.stringify(restoreConfig)}`);
appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_RESTORE, { restoreConfig: restoreConfig, oldConfig: getAppConfig(app) }, function (error) {
if (error) debug(`Error marking ${app.fqdn} for restore: ${JSON.stringify(error)}`);
2017-11-17 22:29:13 -08:00
iteratorDone(); // always succeed
});
});
}, 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.setInstallationCommand(app.id, appdb.ISTATE_PENDING_CONFIGURE, { oldConfig: null }, function (error) {
if (error) debug(`Error marking ${app.fqdn} for reconfigure: ${JSON.stringify(error)}`);
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);
});
}
2018-06-28 16:48:04 -07:00
function setOwner(appId, ownerId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
appdb.setOwner(appId, ownerId, function (error) {
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));
callback();
});
}
function transferOwnership(oldOwnerId, newOwnerId, callback) {
assert.strictEqual(typeof oldOwnerId, 'string');
assert.strictEqual(typeof newOwnerId, 'string');
assert.strictEqual(typeof callback, 'function');
appdb.transferOwnership(oldOwnerId, newOwnerId, function (error) {
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));
callback();
});
}