Files
cloudron-box/src/apps.js

1095 lines
47 KiB
JavaScript
Raw Normal View History

'use strict';
exports = module.exports = {
AppsError: AppsError,
2015-10-15 15:06:34 +02:00
hasAccessTo: hasAccessTo,
get: get,
2016-02-18 15:43:46 +01:00
getByIpAddress: getByIpAddress,
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,
2016-06-02 18:49:56 -07:00
updateApps: updateApps,
restoreInstalledApps: restoreInstalledApps,
configureInstalledApps: configureInstalledApps,
getAppConfig: getAppConfig,
// exported for testing
_validateHostname: validateHostname,
2015-10-15 12:26:48 +02:00
_validatePortBindings: validatePortBindings,
_validateAccessRestriction: validateAccessRestriction
};
var addons = require('./addons.js'),
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,
2015-12-11 12:24:52 -08:00
certificates = require('./certificates.js'),
config = require('./config.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:apps'),
docker = require('./docker.js'),
2016-05-01 21:37:08 -07:00
eventlog = require('./eventlog.js'),
fs = require('fs'),
groups = require('./groups.js'),
2016-09-23 17:23:45 -07:00
mailboxdb = require('./mailboxdb.js'),
manifestFormat = require('cloudron-manifestformat'),
path = require('path'),
paths = require('./paths.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'),
updateChecker = require('./updatechecker.js'),
2016-07-14 14:23:08 +02:00
url = require('url'),
util = require('util'),
uuid = require('node-uuid'),
validator = require('validator');
// 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.BILLING_REQUIRED = 'Billing Required';
AppsError.ACCESS_DENIED = 'Access denied';
2015-10-27 16:36:09 +01:00
AppsError.BAD_CERTIFICATE = 'Invalid certificate';
// Hostname validation comes from RFC 1123 (section 2.1)
// Domain name validation comes from RFC 2181 (Name syntax)
// https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
// We are validating the validity of the location-fqdn as host name
function validateHostname(location, fqdn) {
2016-05-11 15:04:22 -07:00
var RESERVED_LOCATIONS = [ constants.ADMIN_LOCATION, constants.API_LOCATION, constants.SMTP_LOCATION, constants.IMAP_LOCATION, constants.MAIL_LOCATION, constants.POSTMAN_LOCATION ];
2016-06-03 22:14:08 -07:00
if (RESERVED_LOCATIONS.indexOf(location) !== -1) return new AppsError(AppsError.BAD_FIELD, location + ' is reserved');
if (location === '') return null; // bare location
2016-06-03 22:14:08 -07:00
if ((location.length + 1 /*+ hyphen */ + fqdn.indexOf('.')) > 63) return new AppsError(AppsError.BAD_FIELD, 'Hostname length cannot be greater than 63');
if (location.match(/^[A-Za-z0-9-]+$/) === null) return new AppsError(AppsError.BAD_FIELD, 'Hostname can only contain alphanumerics and hyphen');
if (location[0] === '-' || location[location.length-1] === '-') return new AppsError(AppsError.BAD_FIELD, 'Hostname cannot start or end with hyphen');
if (location.length + 1 /* hyphen */ + fqdn.length > 253) return new AppsError(AppsError.BAD_FIELD, 'FQDN length exceeds 253 characters');
return null;
}
// validate the port bindings
function validatePortBindings(portBindings, tcpPorts) {
2017-01-29 13:01:09 -08:00
assert.strictEqual(typeof portBindings, 'object');
// keep the public ports in sync with firewall rules in scripts/initializeBaseUbuntuImage.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 */
2017-01-29 12:38:54 -08:00
202, /* caas ssh */
443, /* https */
2016-05-05 15:00:07 -07:00
465, /* smtps */
587, /* submission */
993, /* imaps */
2003, /* graphite (lo) */
2004, /* graphite (lo) */
2020, /* install server */
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 /* graphite (lo) */
];
if (!portBindings) return null;
var env;
for (env in portBindings) {
if (!/^[a-zA-Z0-9_]+$/.test(env)) return new AppsError(AppsError.BAD_FIELD, env + ' is not valid environment variable');
if (!Number.isInteger(portBindings[env])) return new AppsError(AppsError.BAD_FIELD, portBindings[env] + ' is not an integer');
2015-11-25 13:49:20 +01:00
if (RESERVED_PORTS.indexOf(portBindings[env]) !== -1) return new AppsError(AppsError.PORT_RESERVED, String(portBindings[env]));
if (portBindings[env] <= 1023 || portBindings[env] > 65535) return new AppsError(AppsError.BAD_FIELD, portBindings[env] + ' 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
tcpPorts = tcpPorts || { };
for (env in portBindings) {
if (!(env in tcpPorts)) return new AppsError(AppsError.BAD_FIELD, 'Invalid portBindings ' + env);
}
return null;
}
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;
2016-02-11 17:39:15 +01:00
var max = (4096 * 1024 * 1024);
// 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;
}
2016-07-14 14:23:08 +02:00
// https://tools.ietf.org/html/rfc7034
function validateXFrameOptions(xFrameOptions) {
assert.strictEqual(typeof xFrameOptions, 'string');
if (xFrameOptions === 'DENY') return null;
if (xFrameOptions === 'SAMEORIGIN') return null;
var parts = xFrameOptions.split(' ');
if (parts.length !== 2 || parts[0] !== 'ALLOW-FROM') return new AppsError(AppsError.BAD_FIELD, 'xFrameOptions must be "DENY", "SAMEORIGIN" or "ALLOW-FROM uri"' );
var uri = url.parse(parts[1]);
return (uri.protocol === 'http:' || uri.protocol === 'https:') ? null : new AppsError(AppsError.BAD_FIELD, 'xFrameOptions ALLOW-FROM uri must be a valid http[s] uri' );
}
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 getDuplicateErrorDetails(location, portBindings, error) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof portBindings, 'object');
assert.strictEqual(error.reason, DatabaseError.ALREADY_EXISTS);
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);
}
// check if the location conflicts
if (match[1] === location) return new AppsError(AppsError.ALREADY_EXISTS);
// check if any of the port bindings conflict
for (var env in portBindings) {
if (portBindings[env] === parseInt(match[1])) return new AppsError(AppsError.PORT_CONFLICT, match[1]);
}
return new AppsError(AppsError.ALREADY_EXISTS);
}
2016-06-13 13:48:53 -07:00
function getAppConfig(app) {
return {
manifest: app.manifest,
location: app.location,
accessRestriction: app.accessRestriction,
portBindings: app.portBindings,
memoryLimit: app.memoryLimit,
2016-07-15 11:08:11 +02:00
xFrameOptions: app.xFrameOptions || 'SAMEORIGIN',
2016-06-13 13:48:53 -07:00
altDomain: app.altDomain
};
}
function getIconUrlSync(app) {
2017-01-24 10:13:25 -08:00
var iconPath = paths.APP_ICONS_DIR + '/' + app.id + '.png';
return fs.existsSync(iconPath) ? '/api/v1/apps/' + app.id + '/icon' : null;
}
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);
// check group access
if (!app.accessRestriction.groups) return callback(null, false);
async.some(app.accessRestriction.groups, function (groupId, iteratorDone) {
2017-02-16 20:11:09 -08:00
groups.isMember(groupId, user.id, iteratorDone);
}, function (error, result) {
callback(null, !error && result);
});
2015-10-15 15:06:34 +02:00
}
function get(appId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
appdb.get(appId, function (error, app) {
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));
app.iconUrl = getIconUrlSync(app);
app.fqdn = app.altDomain || config.appFqdn(app.location);
app.cnameTarget = app.altDomain ? config.appFqdn(app.location) : null;
callback(null, app);
});
}
2016-02-18 15:43:46 +01: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));
appdb.getByContainerId(containerId, function (error, app) {
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));
app.iconUrl = getIconUrlSync(app);
app.fqdn = app.altDomain || config.appFqdn(app.location);
app.cnameTarget = app.altDomain ? config.appFqdn(app.location) : null;
2016-02-18 15:43:46 +01:00
callback(null, app);
});
});
}
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
appdb.getAll(function (error, apps) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
apps.forEach(function (app) {
app.iconUrl = getIconUrlSync(app);
app.fqdn = app.altDomain || config.appFqdn(app.location);
app.cnameTarget = app.altDomain ? config.appFqdn(app.location) : null;
});
callback(null, apps);
});
}
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));
2016-06-04 13:03:09 -07:00
if (result.statusCode !== 200) return callback(new AppsError(AppsError.BAD_FIELD, util.format('Failed to get app info from store.', result.statusCode, result.text)));
callback(null, parts[0], result.body.manifest);
});
}
function install(data, auditSource, callback) {
2016-06-03 23:22:38 -07:00
assert(data && typeof data === 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
var location = data.location.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,
2016-07-14 14:23:08 +02:00
altDomain = data.altDomain || null,
xFrameOptions = data.xFrameOptions || 'SAMEORIGIN',
sso = 'sso' in data ? data.sso : null,
2017-04-11 12:49:21 -07:00
debugMode = data.debugMode || null,
backupId = data.backupId || null;
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 = validateHostname(location, config.fqdn());
if (error) return callback(error);
error = validatePortBindings(portBindings, manifest.tcpPorts);
if (error) return callback(error);
error = validateAccessRestriction(accessRestriction);
if (error) return callback(error);
error = validateMemoryLimit(manifest, memoryLimit);
if (error) return callback(error);
2016-07-14 14:23:08 +02:00
error = validateXFrameOptions(xFrameOptions);
if (error) return callback(error);
error = validateDebugMode(debugMode);
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'];
2017-04-03 16:54:06 -07:00
if (altDomain !== null && !validator.isFQDN(altDomain)) return callback(new AppsError(AppsError.BAD_FIELD, 'Invalid external domain'));
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'));
2017-01-24 10:13:25 -08:00
if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.png'), new Buffer(icon, 'base64'))) {
return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving icon:' + safe.error.message));
}
}
error = certificates.validateCertificate(cert, key, config.appFqdn(location));
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
2015-10-28 22:09:19 +01:00
debug('Will install app with id : ' + appId);
appstore.purchase(appId, appStoreId, 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.BILLING_REQUIRED) return callback(new AppsError(AppsError.BILLING_REQUIRED, error.message));
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));
2016-06-17 16:43:35 -05:00
var data = {
accessRestriction: accessRestriction,
memoryLimit: memoryLimit,
2016-07-14 14:23:08 +02:00
altDomain: altDomain,
xFrameOptions: xFrameOptions,
sso: sso,
debugMode: debugMode,
2017-04-11 12:49:21 -07:00
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app',
lastBackupId: backupId
2016-06-17 16:43:35 -05:00
};
appdb.add(appId, appStoreId, manifest, location, portBindings, data, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
// save cert to boxdata/certs
if (cert && key) {
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.user.cert'), cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.user.key'), key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
}
taskmanager.restartAppTask(appId);
2016-05-01 21:37:08 -07:00
2017-04-11 12:49:21 -07:00
eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId: appId, location: location, manifest: manifest, backupId: backupId });
2016-09-23 17:23:45 -07:00
callback(null, { id : appId });
});
});
});
}
2016-06-04 16:32:27 -07:00
function configure(appId, data, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
2016-06-04 16:32:27 -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');
2016-06-04 18:07:02 -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'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
2016-06-04 18:07:02 -07:00
var location, portBindings, values = { };
if ('location' in data) {
location = values.location = data.location.toLowerCase();
error = validateHostname(values.location, config.fqdn());
if (error) return callback(error);
} else {
location = app.location;
}
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 ('altDomain' in data) {
values.altDomain = data.altDomain;
2017-04-03 16:54:06 -07:00
if (values.altDomain !== null && !validator.isFQDN(values.altDomain)) return callback(new AppsError(AppsError.BAD_FIELD, 'Invalid external domain'));
2016-06-04 18:07:02 -07:00
}
2016-04-19 00:22:56 -07:00
2016-06-04 18:07:02 -07:00
if ('portBindings' in data) {
portBindings = values.portBindings = data.portBindings;
error = validatePortBindings(values.portBindings, app.manifest.tcpPorts);
if (error) return callback(error);
} 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);
}
2016-07-14 15:16:05 +02:00
if ('xFrameOptions' in data) {
values.xFrameOptions = data.xFrameOptions;
error = validateXFrameOptions(values.xFrameOptions);
if (error) return callback(error);
}
if ('debugMode' in data) {
values.debugMode = data.debugMode;
error = validateDebugMode(values.debugMode);
if (error) return callback(error);
}
// save cert to boxdata/certs. TODO: move this to apptask when we have a real task queue
2016-06-04 18:07:02 -07:00
if ('cert' in data && 'key' in data) {
if (data.cert && data.key) {
error = certificates.validateCertificate(data.cert, data.key, config.appFqdn(location));
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.user.cert'), data.cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.user.key'), data.key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
} else { // remove existing cert/key
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.user.cert'))) debug('Error removing cert: ' + safe.error.message);
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.user.key'))) debug('Error removing key: ' + safe.error.message);
}
}
2016-06-13 13:48:53 -07:00
values.oldConfig = getAppConfig(app);
debug('Will configure app with id:%s values:%j', appId, values);
var oldName = (app.location ? app.location : app.manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app';
var newName = (location ? location : app.manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app';
mailboxdb.updateName(oldName, newName, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new AppsError(AppsError.ALREADY_EXISTS, 'This mailbox is already taken'));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_CONFIGURE, values, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
taskmanager.restartAppTask(appId);
2016-05-01 21:37:08 -07:00
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId });
callback(null);
});
});
});
}
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('Will update app with id:%s', appId);
2016-06-04 19:19:00 -07:00
downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) {
if (error) return callback(error);
2016-06-04 19:06:16 -07:00
2016-06-04 19:19:00 -07:00
var values = { };
2016-06-04 19:19:00 -07:00
error = manifestFormat.parse(manifest);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest error:' + error.message));
2016-06-04 19:19:00 -07:00
error = checkManifestConstraints(manifest);
if (error) return callback(error);
2016-06-04 19:19:00 -07:00
values.manifest = manifest;
2016-06-04 19:19:00 -07:00
if ('portBindings' in data) {
values.portBindings = data.portBindings;
error = validatePortBindings(data.portBindings, values.manifest.tcpPorts);
if (error) return callback(error);
}
2016-06-04 19:19:00 -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'));
2017-01-24 10:13:25 -08:00
if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.png'), new Buffer(data.icon, 'base64'))) {
2016-06-04 19:19:00 -07:00
return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving icon:' + safe.error.message));
}
} else {
2017-01-24 10:13:25 -08:00
safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, appId + '.png'));
2016-06-04 19:19:00 -07:00
}
}
2016-06-04 19:19:00 -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'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
// 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 !== values.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
2016-06-04 19:19:00 -07:00
values.appStoreId = '';
}
// 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
// 0 and -1 are special values for memory limit indicating unset and unlimited
if (app.memoryLimit > 0 && values.manifest.memoryLimit && app.memoryLimit < values.manifest.memoryLimit) {
2016-06-04 19:19:00 -07:00
values.memoryLimit = values.manifest.memoryLimit;
}
2016-06-13 13:48:53 -07:00
values.oldConfig = getAppConfig(app);
2016-06-04 19:19:00 -07:00
appdb.setInstallationCommand(appId, data.force ? appdb.ISTATE_PENDING_FORCE_UPDATE : appdb.ISTATE_PENDING_UPDATE, values, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails('' /* location cannot conflict */, values.portBindings, error));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
2016-06-04 19:19:00 -07:00
taskmanager.restartAppTask(appId);
2016-06-18 13:24:27 -05:00
eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId: appId, toManifest: manifest, fromManifest: app.manifest, force: data.force });
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);
});
});
});
}
2015-11-02 11:20:50 -08:00
function appLogFilter(app) {
2015-11-02 14:23:02 -08:00
var names = [ app.id ].concat(addons.getContainerNamesSync(app, app.manifest.addons));
2015-11-02 11:20:50 -08:00
return names.map(function (name) { return 'CONTAINER_NAME=' + name; });
}
function getLogs(appId, lines, follow, callback) {
assert.strictEqual(typeof appId, 'string');
2015-11-02 11:20:50 -08:00
assert.strictEqual(typeof lines, 'number');
assert.strictEqual(typeof follow, 'boolean');
assert.strictEqual(typeof callback, 'function');
debug('Getting logs for %s', appId);
2015-11-02 11:20:50 -08:00
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
2015-11-02 11:20:50 -08:00
var args = [ '--output=json', '--no-pager', '--lines=' + lines ];
if (follow) args.push('--follow');
args = args.concat(appLogFilter(app));
2015-11-02 11:20:50 -08:00
var cp = spawn('/bin/journalctl', args);
2015-11-02 11:20:50 -08:00
var transformStream = split(function mapper(line) {
var obj = safe.JSON.parse(line);
if (!obj) return undefined;
2015-11-02 11:20:50 -08:00
var source = obj.CONTAINER_NAME.slice(app.id.length + 1);
2015-11-02 14:26:15 -08:00
return JSON.stringify({
realtimeTimestamp: obj.__REALTIME_TIMESTAMP,
monotonicTimestamp: obj.__MONOTONIC_TIMESTAMP,
message: obj.MESSAGE,
source: source || 'main'
2015-11-10 11:31:05 +01:00
}) + '\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);
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
// for empty or null backupId, use existing manifest to mimic a reinstall
2016-06-13 18:04:22 -07:00
var func = data.backupId ? backups.getRestoreConfig.bind(null, data.backupId) : function (next) { return next(null, { manifest: app.manifest }); };
func(function (error, restoreConfig) {
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 (!restoreConfig) callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Could not get restore config'));
// re-validate because this new box version may not accept old configs
error = checkManifestConstraints(restoreConfig.manifest);
if (error) return callback(error);
var values = {
lastBackupId: data.backupId || null, // when null, apptask simply reinstalls
manifest: restoreConfig.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, { appId: appId });
2016-05-01 21:37:08 -07:00
callback(null);
});
});
});
}
2016-06-17 17:12:55 -05:00
function clone(appId, data, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug('Will clone app with id:%s', appId);
var location = data.location.toLowerCase(),
portBindings = data.portBindings || null,
backupId = data.backupId;
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof portBindings, 'object');
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
backups.getRestoreConfig(backupId, function (error, restoreConfig) {
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, error.message));
2016-06-17 17:12:55 -05:00
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (!restoreConfig) callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Could not get restore config'));
// re-validate because this new box version may not accept old configs
error = checkManifestConstraints(restoreConfig.manifest);
if (error) return callback(error);
error = validateHostname(location, config.fqdn());
if (error) return callback(error);
error = validatePortBindings(portBindings, restoreConfig.manifest.tcpPorts);
if (error) return callback(error);
var newAppId = uuid.v4(), appStoreId = app.appStoreId, manifest = restoreConfig.manifest;
appstore.purchase(newAppId, appStoreId, 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.BILLING_REQUIRED) return callback(new AppsError(AppsError.BILLING_REQUIRED, error.message));
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));
2016-06-17 17:12:55 -05:00
var data = {
installationState: appdb.ISTATE_PENDING_CLONE,
memoryLimit: app.memoryLimit,
2016-06-17 17:53:27 -05:00
accessRestriction: app.accessRestriction,
2016-07-14 15:16:05 +02:00
xFrameOptions: app.xFrameOptions,
2016-11-28 12:45:32 +01:00
lastBackupId: backupId,
sso: !!app.sso,
2017-02-13 15:15:07 -08:00
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app'
2016-06-17 17:12:55 -05:00
};
appdb.add(newAppId, appStoreId, manifest, location, portBindings, data, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
2016-06-17 17:12:55 -05:00
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
taskmanager.restartAppTask(newAppId);
2016-06-17 17:12:55 -05:00
eventlog.add(eventlog.ACTION_APP_CLONE, auditSource, { appId: newAppId, oldAppId: appId, backupId: backupId, location: location, manifest: manifest });
2016-06-17 17:12:55 -05:00
callback(null, { id : newAppId });
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, result) {
2016-08-04 09:38:00 +02:00
if (error) return callback(error);
appstore.unpurchase(appId, result.appStoreId, 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.BILLING_REQUIRED) return callback(new AppsError(AppsError.BILLING_REQUIRED, error.message));
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
2017-02-13 15:19:17 -08:00
eventlog.add(eventlog.ACTION_APP_UNINSTALL, auditSource, { appId: appId });
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');
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, 'minBoxVersion exceeds Box 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);
appdb.get(appId, function (error, app) {
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-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) {
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) {
exec.resize({ h: options.rows, w: options.columns }, function (error) { if (error) debug('Error resizing console', error); });
}
2015-11-10 21:56:17 -08:00
return callback(null, stream);
});
});
});
}
2016-06-02 18:49:56 -07:00
function updateApps(updateInfo, auditSource, callback) { // updateInfo is { appId -> { manifest } }
assert.strictEqual(typeof updateInfo, 'object');
2016-06-02 18:49:56 -07:00
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
function canAutoupdateApp(app, newManifest) {
2016-06-18 14:51:49 -05:00
var newTcpPorts = newManifest.tcpPorts || { };
var oldTcpPorts = app.manifest.tcpPorts || { };
var portBindings = app.portBindings; // this is never null
for (var env in newTcpPorts) {
if (!(env in oldTcpPorts)) return new Error(env + ' is required from user');
}
for (env in portBindings) {
if (!(env in newTcpPorts)) return new Error(env + ' was in use but new update removes it');
}
// it's fine if one or more (unused) keys got removed
return null;
}
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();
}
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');
appdb.getAll(function (error, apps) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
async.map(apps, function (app, iteratorDone) {
debug('marking %s for restore', app.location || app.id);
appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_RESTORE, { oldConfig: null }, function (error) {
if (error) debug('did not mark %s for restore', app.location || app.id, error);
iteratorDone(); // always succeed
});
}, callback);
});
}
function configureInstalledApps(callback) {
assert.strictEqual(typeof callback, 'function');
appdb.getAll(function (error, apps) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
async.map(apps, function (app, iteratorDone) {
debug('marking %s for reconfigure', app.location || app.id);
appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_CONFIGURE, { oldConfig: null }, function (error) {
if (error) debug('did not mark %s for reconfigure', app.location || app.id, error);
iteratorDone(); // always succeed
});
}, callback);
});
}