Files
cloudron-box/src/apps.js
T

1536 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,
2018-06-25 16:45:15 -07:00
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,
2019-03-19 16:23:03 -07:00
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,
2016-05-24 10:33:10 -07:00
restoreInstalledApps: restoreInstalledApps,
configureInstalledApps: configureInstalledApps,
getAppConfig: getAppConfig,
2018-12-20 14:33:29 -08:00
getDataDir: getDataDir,
2017-08-18 20:45:52 -07:00
downloadFile: downloadFile,
uploadFile: uploadFile,
2018-08-12 22:08:19 -07:00
PORT_TYPE_TCP: 'tcp',
2018-08-13 08:33:09 -07:00
PORT_TYPE_UDP: 'udp',
2018-08-12 22:08:19 -07:00
// 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'),
2017-04-13 00:42:44 -07:00
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,
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:apps'),
2015-11-10 12:47:48 -08:00
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'),
2018-05-24 16:25:32 -07:00
mail = require('./mail.js'),
manifestFormat = require('cloudron-manifestformat'),
2019-03-04 12:28:27 -08:00
once = require('once'),
2017-11-07 09:09:30 -08:00
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
semver = require('semver'),
2019-07-26 10:49:29 -07:00
settings = require('./settings.js'),
2015-11-02 11:20:50 -08:00
spawn = require('child_process').spawn,
split = require('split'),
superagent = require('superagent'),
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'),
2017-08-13 17:44:31 -07:00
uuid = require('uuid'),
2018-04-29 10:47:34 -07:00
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';
2019-05-06 15:00:22 -07:00
AppsError.PLAN_LIMIT = 'Plan Limit';
2015-10-15 15:18:40 +02:00
AppsError.ACCESS_DENIED = 'Access denied';
2015-10-27 16:36:09 +01:00
AppsError.BAD_CERTIFICATE = 'Invalid certificate';
// validate the port bindings
2018-08-12 22:37:36 -07:00
function validatePortBindings(portBindings, manifest) {
2017-01-29 13:01:09 -08:00
assert.strictEqual(typeof portBindings, 'object');
2018-08-12 22:37:36 -07:00
assert.strictEqual(typeof manifest, 'object');
2017-01-29 13:01:09 -08:00
2018-06-04 21:24:14 +02:00
// keep the public ports in sync with firewall rules in setup/start/cloudron-firewall.sh
// these ports are reserved even if we listen only on 127.0.0.1 because we setup HostIp to be 127.0.0.1
// for custom tcp ports
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 */
2018-11-16 19:23:09 -08:00
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`);
2017-01-29 12:43:07 -08:00
}
// it is OK if there is no 1-1 mapping between values in manifest.tcpPorts and portBindings. missing values implies
// that the user wants the service disabled
2018-08-12 22:37:36 -07:00
const tcpPorts = manifest.tcpPorts || { };
2018-08-12 22:47:59 -07:00
const udpPorts = manifest.udpPorts || { };
for (let portName in portBindings) {
2018-08-12 22:47:59 -07:00
if (!(portName in tcpPorts) && !(portName in udpPorts)) return new AppsError(AppsError.BAD_FIELD, `Invalid portBindings ${portName}`);
}
return null;
}
2018-08-13 08:33:09 -07:00
function translatePortBindings(portBindings, manifest) {
assert.strictEqual(typeof portBindings, 'object');
assert.strictEqual(typeof manifest, 'object');
if (!portBindings) return null;
let result = {};
2018-08-13 08:33:09 -07:00
const tcpPorts = manifest.tcpPorts || { };
for (let portName in portBindings) {
2018-08-13 08:33:09 -07:00
const portType = portName in tcpPorts ? exports.PORT_TYPE_TCP : exports.PORT_TYPE_UDP;
result[portName] = { hostPort: portBindings[portName], type: portType };
}
return result;
}
2015-10-15 12:26:48 +02:00
function validateAccessRestriction(accessRestriction) {
assert.strictEqual(typeof accessRestriction, 'object');
2015-10-15 12:26:48 +02:00
if (accessRestriction === null) return null;
2015-10-15 12:26:48 +02:00
2016-02-09 13:03:52 -08:00
if (accessRestriction.users) {
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');
2016-02-09 13:03:52 -08:00
}
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');
2016-02-09 13:03:52 -08:00
}
2016-06-04 13:20:10 -07:00
// TODO: maybe validate if the users and groups actually exist
2015-10-15 12:26:48 +02:00
return null;
}
2016-02-11 17:39:15 +01:00
function validateMemoryLimit(manifest, memoryLimit) {
assert.strictEqual(typeof manifest, 'object');
assert.strictEqual(typeof memoryLimit, 'number');
2016-02-14 12:13:49 +01:00
var min = manifest.memoryLimit || constants.DEFAULT_MEMORY_LIMIT;
2017-11-07 09:09:30 -08:00
var max = os.totalmem() * 2; // this will overallocate since we don't allocate equal swap always (#466)
2016-02-11 17:39:15 +01:00
// allow 0, which indicates that it is not set, the one from the manifest will be choosen but we don't commit any user value
// this is needed so an app update can change the value in the manifest, and if not set by the user, the new value should be used
if (memoryLimit === 0) return null;
// a special value that indicates unlimited memory
if (memoryLimit === -1) return null;
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;
}
2017-07-14 12:19:27 -05:00
function validateRobotsTxt(robotsTxt) {
if (robotsTxt === null) return null;
// this is the nginx limit on inline strings. if we really hit this, we have to generate a file
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
2017-07-14 12:19:27 -05:00
return null;
}
2017-11-16 14:47:05 -08:00
function validateBackupFormat(format) {
2019-03-22 07:48:31 -07:00
assert.strictEqual(typeof format, 'string');
2017-11-16 14:47:05 -08:00
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;
}
2018-12-20 14:33:29 -08:00
function validateDataDir(dataDir) {
2019-01-18 14:48:31 -08:00
if (dataDir === '') return null; // revert back to default dataDir
2018-12-20 14:33:29 -08:00
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`);
}
2019-01-19 22:19:43 -08:00
// backup logic relies on paths not overlapping (because it recurses)
2018-12-20 14:33:29 -08:00
if (dataDir.startsWith(paths.APPS_DATA_DIR)) return new AppsError(AppsError.BAD_FIELD, `dataDir ${dataDir} cannot be inside apps data`);
2019-01-19 22:19:43 -08:00
// if we made it this far, it cannot start with any of these realistically
const fhs = [ '/bin', '/boot', '/etc', '/lib', '/lib32', '/lib64', '/proc', '/run', '/sbin', '/tmp', '/usr' ];
if (fhs.some((p) => dataDir.startsWith(p))) return new AppsError(AppsError.BAD_FIELD, `dataDir ${dataDir} cannot be placed inside this location`);
2018-12-20 14:33:29 -08:00
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,
2019-01-03 10:08:52 -08:00
fqdn: app.fqdn,
2016-06-13 13:48:53 -07:00
accessRestriction: app.accessRestriction,
portBindings: app.portBindings,
memoryLimit: app.memoryLimit,
2019-05-20 10:08:53 -07:00
robotsTxt: app.robotsTxt,
2018-06-29 16:37:53 +02:00
sso: app.sso,
2018-10-11 14:07:43 -07:00
alternateDomains: app.alternateDomains || [],
2018-12-20 14:33:29 -08:00
env: app.env,
dataDir: app.dataDir
2016-06-13 13:48:53 -07:00
};
}
2018-12-20 14:33:29 -08: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) {
2018-05-24 15:27:55 -07:00
return _.pick(app,
2019-08-26 15:28:29 -07:00
'id', 'appStoreId', 'installationState', 'installationProgress', 'runState', 'health', 'taskId',
2018-05-24 15:27:55 -07:00
'location', 'domain', 'fqdn', 'mailboxName',
2019-05-20 10:08:53 -07:00
'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');
2018-04-29 10:47:34 -07:00
}
2019-02-11 14:37:49 -08:00
// non-admins can only see these
2018-06-25 16:45:15 -07:00
function removeRestrictedFields(app) {
return _.pick(app,
2019-08-26 15:28:29 -07:00
'id', 'appStoreId', 'installationState', 'installationProgress', 'runState', 'health', 'taskId',
2019-04-24 14:25:23 +02:00
'location', 'domain', 'fqdn', 'manifest', 'portBindings', 'iconUrl', 'creationTime', 'ts', 'tags', 'label');
2018-06-25 16:45:15 -07:00
}
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);
2016-02-09 13:03:52 -08:00
// 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
2017-11-15 18:07:10 -08:00
2018-07-26 11:15:57 -07:00
if (!app.accessRestriction.groups) return callback(null, false);
2017-11-15 18:07:10 -08:00
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);
2017-11-15 18:07:10 -08:00
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);
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
});
}
2019-03-19 16:23:03 -07: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('@');
2019-07-26 10:49:29 -07:00
var url = settings.apiServerOrigin() + '/api/v1/apps/' + parts[0] + (parts[1] ? '/versions/' + parts[1] : '');
debug('downloading manifest from %s', url);
2016-09-12 12:53:51 -07:00
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';
}
2019-08-26 15:55:57 -07:00
function startAppTask(appId, callback) {
2019-08-27 11:38:12 -07:00
const logFile = path.join(paths.LOG_DIR, appId, 'apptask.log');
let task = tasks.startTask(tasks.TASK_APP, [ appId ], { logFile });
2019-08-26 15:55:57 -07:00
task.on('start', function (taskId) {
appdb.update(appId, { taskId: taskId }, function (error) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
get(appId, function (error, result) {
if (error) return callback(error);
callback(null, result);
});
});
});
task.on('error', (error) => callback(new AppsError(AppsError.INTERNAL_ERROR, error)));
}
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,
2017-01-19 11:20:24 -08:00
sso = 'sso' in data ? data.sso : null,
2017-04-11 12:49:21 -07:00
debugMode = data.debugMode || null,
2017-07-14 12:19:27 -05:00
robotsTxt = data.robotsTxt || null,
2017-08-16 14:12:07 -07:00
enableBackup = 'enableBackup' in data ? data.enableBackup : true,
2018-12-07 09:03:28 -08:00
enableAutomaticUpdate = 'enableAutomaticUpdate' in data ? data.enableAutomaticUpdate : true,
2017-11-16 14:47:05 -08:00
backupId = data.backupId || null,
2018-05-13 21:02:57 -07:00
backupFormat = data.backupFormat || 'tgz',
2018-10-11 14:07:43 -07:00
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);
2018-08-12 22:37:36 -07:00
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);
2017-07-14 12:19:27 -05:00
error = validateRobotsTxt(robotsTxt);
if (error) return callback(error);
2017-11-16 14:47:05 -08:00
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
2017-03-19 00:36:05 -07:00
if (sso === null) sso = !!manifest.addons['ldap'] || !!manifest.addons['oauth'];
2016-11-11 10:55:44 +05:30
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,
2018-12-07 09:03:28 -08:00
enableAutomaticUpdate: enableAutomaticUpdate,
2018-09-04 16:21:10 -07:00
robotsTxt: robotsTxt,
2018-10-11 14:07:43 -07:00
alternateDomains: alternateDomains,
env: env
};
2019-07-02 20:22:17 -07:00
appdb.add(appId, appStoreId, manifest, location, domain, 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
2019-08-26 15:55:57 -07:00
startAppTask(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);
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) {
2018-08-12 22:37:36 -07:00
error = validatePortBindings(data.portBindings, app.manifest);
2016-06-04 18:07:02 -07:00
if (error) return callback(error);
2018-08-13 08:33:09 -07:00
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);
2017-01-19 15:48:41 -08:00
}
2017-07-14 12:19:27 -05:00
if ('robotsTxt' in data) {
values.robotsTxt = data.robotsTxt || null;
error = validateRobotsTxt(values.robotsTxt);
if (error) return callback(error);
}
2018-05-24 16:25:32 -07:00
if ('mailboxName' in data) {
if (data.mailboxName) {
2018-09-25 11:26:35 -07:00
error = mail.validateName(data.mailboxName);
if (error) return callback(error);
2018-12-06 21:08:19 -08:00
values.mailboxName = data.mailboxName;
} else {
values.mailboxName = mailboxNameForLocation(location, app.manifest);
2018-09-25 11:26:35 -07:00
}
2018-09-25 17:04:17 -07:00
} else { // keep existing name or follow the new location
2018-12-06 21:08:19 -08:00
values.mailboxName = app.mailboxName.endsWith('.app') ? mailboxNameForLocation(location, app.manifest) : app.mailboxName;
2018-05-24 16:25:32 -07:00
}
if ('alternateDomains' in data) {
// TODO validate all subdomains [{ domain: '', subdomain: ''}]
values.alternateDomains = data.alternateDomains;
}
2018-10-11 14:07:43 -07:00
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);
2018-10-11 14:07:43 -07:00
}
2019-01-18 14:48:31 -08:00
if ('dataDir' in data && data.dataDir !== app.dataDir) {
2018-12-20 14:33:29 -08:00
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;
2018-12-07 09:03:28 -08:00
if ('enableAutomaticUpdate' in data) values.enableAutomaticUpdate = data.enableAutomaticUpdate;
2018-01-09 21:03:59 -08:00
values.oldConfig = getAppConfig(app);
2019-05-22 09:38:26 -07:00
debug(`configure: id:${appId}`);
2018-01-09 21:03:59 -08:00
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));
2016-12-15 16:41:16 +01:00
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
2019-08-26 21:36:18 -07:00
startAppTask(appId, function (error, result) {
if (error) return callback(error);
2018-01-09 21:03:59 -08:00
2018-12-06 21:08:19 -08:00
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: result });
2018-12-06 21:08:19 -08:00
callback(null);
2018-01-09 21:03:59 -08:00
});
2016-12-15 16:41:16 +01: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');
2019-05-22 09:38:26 -07:00
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;
2019-01-11 14:19:32 -08:00
// prevent user from installing a app with different manifest id over an existing app
// this allows cloudron install -f --app <appid> for an app installed from the appStore
if (app.manifest.id !== updateConfig.manifest.id) {
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 = '';
}
2019-01-27 14:07:40 -08:00
// suffix '0' if prerelease is missing for semver.lte to work as expected
const currentVersion = semver.prerelease(app.manifest.version) ? app.manifest.version : `${app.manifest.version}-0`;
const updateVersion = semver.prerelease(updateConfig.manifest.version) ? updateConfig.manifest.version : `${updateConfig.manifest.version}-0`;
if (app.appStoreId !== '' && semver.lte(updateVersion, currentVersion)) {
2019-01-11 14:19:32 -08:00
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'));
2017-01-19 11:20:24 -08:00
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
}
2016-02-14 12:10:22 +01: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));
2019-08-26 22:08:40 -07:00
startAppTask(appId, function (error) {
if (error) return callback(error);
2019-08-26 22:08:40 -07:00
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
2019-08-26 22:08:40 -07:00
// clear update indicator, if update fails, it will come back through the update checker
updateChecker.resetAppUpdateInfo(appId);
2019-08-26 22:08:40 -07:00
callback(null);
});
2016-06-04 19:19:00 -07:00
});
});
});
}
2017-04-18 20:32:57 -07:00
function getLogs(appId, options, callback) {
assert.strictEqual(typeof appId, 'string');
2017-04-18 20:32:57 -07:00
assert(options && typeof options === 'object');
assert.strictEqual(typeof callback, 'function');
2019-01-08 12:10:53 -08:00
assert.strictEqual(typeof options.lines, 'number');
assert.strictEqual(typeof options.format, 'string');
assert.strictEqual(typeof options.follow, 'boolean');
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);
2017-04-18 20:32:57 -07:00
2019-01-08 12:10:53 -08:00
var lines = options.lines === -1 ? '+1' : options.lines,
2018-06-04 21:24:02 +02:00
format = options.format || 'json',
2019-01-08 12:10:53 -08:00
follow = options.follow;
2017-04-18 20:32:57 -07:00
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 ];
2018-06-14 12:53:49 +02:00
if (follow) args.push('--follow', '--retry', '--quiet'); // same as -F. to make it work if file doesn't exist, --quiet to not output file headers, which are no logs
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);
2016-06-13 13:44:49 -07:00
// 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 }); };
2016-06-13 13:44:49 -07:00
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));
2016-06-13 13:44:49 -07:00
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
2016-06-13 18:11:11 -07:00
2017-11-16 14:47:05 -08:00
if (!backupInfo.manifest) callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Could not get restore manifest'));
2016-06-13 13:44:49 -07:00
// re-validate because this new box version may not accept old configs
error = checkManifestConstraints(backupInfo.manifest);
if (error) return callback(error);
2016-06-13 13:44:49 -07:00
var values = {
2017-11-16 14:47:05 -08:00
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)
};
2016-06-13 13:44:49 -07:00
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));
2019-08-26 22:03:10 -07:00
startAppTask(appId, function (error) {
if (error) return callback(error);
2019-08-26 22:03:10 -07:00
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
2019-08-26 22:03:10 -07:00
callback(null);
});
2016-06-13 13:44:49 -07:00
});
});
});
}
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));
2019-05-06 15:00:22 -07:00
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,
2018-05-13 21:02:57 -07:00
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
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,
2018-10-11 14:07:43 -07:00
robotsTxt: app.robotsTxt,
env: app.env
};
2019-07-02 20:22:17 -07:00
appdb.add(newAppId, app.appStoreId, manifest, location, domain, 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
2019-08-26 22:01:10 -07:00
startAppTask(newAppId, function (error, result) {
if (error) return callback(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
2019-08-26 22:01:10 -07: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));
2019-05-06 15:00:22 -07:00
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-26 21:56:55 -07:00
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));
startAppTask(appId, function (error, result) {
if (error) return callback(error);
2016-08-04 09:38:00 +02:00
2019-08-26 21:56:55 -07:00
eventlog.add(eventlog.ACTION_APP_UNINSTALL, auditSource, { appId: appId, app: result });
2016-05-01 21:37:08 -07:00
2019-08-26 21:56:55 -07:00
callback(null);
});
2016-08-04 09:38:00 +02:00
});
2016-02-09 11:41:59 -08: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));
2019-08-26 22:16:42 -07:00
startAppTask(appId, callback);
});
}
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));
2019-08-26 22:16:42 -07:00
startAppTask(appId, 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);
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) {
2018-06-08 11:44:22 -07:00
// 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;
}
2015-09-10 11:39:03 -07:00
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) {
2015-09-10 11:39:03 -07:00
if (error) {
debug('Cannot autoupdate app %s : %s', appId, error.message);
return iteratorDone();
2017-11-15 18:45:17 -08:00
}
2015-09-10 11:39:03 -07: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) {
2015-09-10 11:39:03 -07:00
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');
2016-01-17 16:05:47 +01:00
appdb.exists(appId, function (error, exists) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
2016-01-17 16:05:47 +01:00
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));
2019-08-26 22:11:27 -07:00
startAppTask(appId, callback);
});
});
}
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
});
});
}
2016-05-24 10:33:10 -07:00
function restoreInstalledApps(callback) {
assert.strictEqual(typeof callback, 'function');
getAll(function (error, apps) {
2016-05-24 10:33:10 -07:00
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)}`);
2018-05-22 12:05:55 -07:00
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
});
});
2016-05-24 10:33:10 -07:00
}, callback);
});
}
function configureInstalledApps(callback) {
assert.strictEqual(typeof callback, 'function');
getAll(function (error, apps) {
2016-05-24 10:33:10 -07:00
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
async.map(apps, function (app, iteratorDone) {
debug(`marking ${app.fqdn} for reconfigure`);
2016-05-24 10:33:10 -07:00
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
});
2016-05-24 10:33:10 -07:00
}, callback);
});
}
2017-08-18 20:45:52 -07:00
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) {
2017-08-18 20:45:52 -07:00
if (error) return callback(error);
var data = '';
stream.setEncoding('utf8');
stream.on('data', function (d) { data += d; });
stream.on('end', function () {
var parts = data.split('-');
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 });
});
});
2017-08-18 20:45:52 -07:00
});
}
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');
2019-03-04 12:28:27 -08:00
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) {
2019-03-04 12:28:27 -08:00
if (error) return done(error);
2017-08-18 20:45:52 -07:00
var readFile = fs.createReadStream(sourceFilePath);
2019-03-04 12:28:27 -08:00
readFile.on('error', done);
2017-08-18 20:45:52 -07:00
2019-03-04 12:28:27 -08:00
stream.on('error', done);
stream.on('finish', done);
2017-08-18 20:45:52 -07:00
2019-03-04 12:28:27 -08:00
readFile.pipe(stream);
2017-08-18 20:45:52 -07:00
});
}