Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1eedd4b185 | |||
| 9d38edfe95 | |||
| f895ebba73 | |||
| 511287b16e | |||
| 530e06ec66 | |||
| 9cab383b43 | |||
| 9785ab82ed | |||
| 9d237e7bd6 | |||
| 7e9885012d | |||
| 1de785d97c | |||
| 2bd6566537 | |||
| 88fa4cf188 | |||
| b26167481e | |||
| 1b6af9bd12 | |||
| 0159963cb0 | |||
| 996041cabc | |||
| cb0352e33c | |||
| 3169f032c8 | |||
| 5ff8ee1a8f | |||
| d3f31a3ace | |||
| ac7e7f0db9 | |||
| 4c1e967dad | |||
| f3ccd5c074 | |||
| 8369c0e2c0 | |||
| 122a966e72 | |||
| 9c2ff2f862 | |||
| 0ba45e746b | |||
| 54c06cdabb | |||
| 5a2e10317c | |||
| 8292d52acf |
@@ -1609,3 +1609,19 @@
|
||||
* Customizable app icons
|
||||
* Remove obsolete X-Frame-Options from nginx configs
|
||||
* Give SFTP access based on access restriction
|
||||
|
||||
[4.1.1]
|
||||
* Add UI hint about SFTP access restriction
|
||||
|
||||
[4.1.2]
|
||||
* Accept incoming mail from a private relay
|
||||
* Fix issue where unused addon images were not pruned
|
||||
* Add UI for redirect from multiple domains
|
||||
* Allow apps to be relocated to custom data directory
|
||||
* Make all cloudron env vars have CLOUDRON_ prefix
|
||||
* Update manifest version to 2
|
||||
* Fix issue where DKIM keys were inaccessible
|
||||
* Fix DKIM selector conflict when adding same domain across multiple cloudrons
|
||||
* Fix name.com DNS backend issue for naked domains
|
||||
* Add DigitalOcean Frankfurt (fra1) region for backup storage
|
||||
|
||||
|
||||
@@ -46,12 +46,15 @@ apt-get -y install \
|
||||
openssh-server \
|
||||
pwgen \
|
||||
resolvconf \
|
||||
sudo \
|
||||
swaks \
|
||||
tzdata \
|
||||
unattended-upgrades \
|
||||
unbound \
|
||||
xfsprogs
|
||||
|
||||
# on some providers like scaleway the sudo file is changed and we want to keep the old one
|
||||
apt-get -o Dpkg::Options::="--force-confold" install -y sudo
|
||||
|
||||
# this ensures that unattended upgades are enabled, if it was disabled during ubuntu install time (see #346)
|
||||
# debconf-set-selection of unattended-upgrades/enable_auto_updates + dpkg-reconfigure does not work
|
||||
cp /usr/share/unattended-upgrades/20auto-upgrades /etc/apt/apt.conf.d/20auto-upgrades
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE mail ADD COLUMN dkimSelector VARCHAR(128) NOT NULL DEFAULT "cloudron"', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE mail DROP COLUMN dkimSelector', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -174,6 +174,8 @@ CREATE TABLE IF NOT EXISTS mail(
|
||||
catchAllJson TEXT,
|
||||
relayJson TEXT,
|
||||
|
||||
dkimSelector VARCHAR(128) NOT NULL DEFAULT "cloudron",
|
||||
|
||||
FOREIGN KEY(domain) REFERENCES domains(domain),
|
||||
PRIMARY KEY(domain))
|
||||
|
||||
|
||||
@@ -86,6 +86,9 @@ systemctl status --lines=100 cloudron.target box mysql unbound cloudron-syslog n
|
||||
echo -e $LINE"Box logs"$LINE >> $OUT
|
||||
tail -n 100 /home/yellowtent/platformdata/logs/box.log &>> $OUT
|
||||
|
||||
echo -e $LINE"Firewall chains"$LINE >> $OUT
|
||||
ip addr &>> $OUT
|
||||
|
||||
echo -e $LINE"Firewall chains"$LINE >> $OUT
|
||||
iptables -L &>> $OUT
|
||||
|
||||
|
||||
@@ -110,9 +110,6 @@ systemctl restart cloudron-syslog
|
||||
|
||||
$json -f /etc/cloudron/cloudron.conf -I -e "delete this.edition" # can be removed after 4.0
|
||||
|
||||
echo "==> Clearing custom.yml"
|
||||
rm -f /etc/cloudron/custom.yml
|
||||
|
||||
echo "==> Configuring sudoers"
|
||||
rm -f /etc/sudoers.d/${USER}
|
||||
cp "${script_dir}/start/sudoers" /etc/sudoers.d/${USER}
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
# add customizations here
|
||||
# after making changes run "sudo systemctl restart box"
|
||||
|
||||
# appstore:
|
||||
# blacklist:
|
||||
# - io.wekan.cloudronapp
|
||||
# - io.cloudron.openvpn
|
||||
# whitelist:
|
||||
# org.wordpress.cloudronapp: {}
|
||||
# chat.rocket.cloudronapp: {}
|
||||
# com.nextcloud.cloudronapp: {}
|
||||
#
|
||||
# backups:
|
||||
# configurable: true
|
||||
#
|
||||
|
||||
+74
-52
@@ -795,10 +795,12 @@ function setupOauth(app, options, callback) {
|
||||
clients.add(appId, clients.TYPE_OAUTH, redirectURI, scope, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
|
||||
|
||||
var env = [
|
||||
{ name: 'OAUTH_CLIENT_ID', value: result.id },
|
||||
{ name: 'OAUTH_CLIENT_SECRET', value: result.clientSecret },
|
||||
{ name: 'OAUTH_ORIGIN', value: config.adminOrigin() }
|
||||
{ name: `${envPrefix}OAUTH_CLIENT_ID`, value: result.id },
|
||||
{ name: `${envPrefix}OAUTH_CLIENT_SECRET`, value: result.clientSecret },
|
||||
{ name: `${envPrefix}OAUTH_ORIGIN`, value: config.adminOrigin() }
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting oauth addon config to %j', env);
|
||||
@@ -832,17 +834,19 @@ function setupEmail(app, options, callback) {
|
||||
|
||||
const mailInDomains = mailDomains.filter(function (d) { return d.enabled; }).map(function (d) { return d.domain; }).join(',');
|
||||
|
||||
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
|
||||
|
||||
// note that "external" access info can be derived from MAIL_DOMAIN (since it's part of user documentation)
|
||||
var env = [
|
||||
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_SMTP_PORT', value: '2525' },
|
||||
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_IMAP_PORT', value: '9993' },
|
||||
{ name: 'MAIL_SIEVE_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_SIEVE_PORT', value: '4190' },
|
||||
{ name: 'MAIL_DOMAIN', value: app.domain },
|
||||
{ name: 'MAIL_DOMAINS', value: mailInDomains },
|
||||
{ name: 'LDAP_MAILBOXES_BASE_DN', value: 'ou=mailboxes,dc=cloudron' }
|
||||
{ name: `${envPrefix}MAIL_SMTP_SERVER`, value: 'mail' },
|
||||
{ name: `${envPrefix}MAIL_SMTP_PORT`, value: '2525' },
|
||||
{ name: `${envPrefix}MAIL_IMAP_SERVER`, value: 'mail' },
|
||||
{ name: `${envPrefix}MAIL_IMAP_PORT`, value: '9993' },
|
||||
{ name: `${envPrefix}MAIL_SIEVE_SERVER`, value: 'mail' },
|
||||
{ name: `${envPrefix}MAIL_SIEVE_PORT`, value: '4190' },
|
||||
{ name: `${envPrefix}MAIL_DOMAIN`, value: app.domain },
|
||||
{ name: `${envPrefix}MAIL_DOMAINS`, value: mailInDomains },
|
||||
{ name: `${envPrefix}LDAP_MAILBOXES_BASE_DN`, value: 'ou=mailboxes,dc=cloudron' }
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting up Email');
|
||||
@@ -868,14 +872,16 @@ function setupLdap(app, options, callback) {
|
||||
|
||||
if (!app.sso) return callback(null);
|
||||
|
||||
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
|
||||
|
||||
var env = [
|
||||
{ name: 'LDAP_SERVER', value: '172.18.0.1' },
|
||||
{ name: 'LDAP_PORT', value: '' + config.get('ldapPort') },
|
||||
{ name: 'LDAP_URL', value: 'ldap://172.18.0.1:' + config.get('ldapPort') },
|
||||
{ name: 'LDAP_USERS_BASE_DN', value: 'ou=users,dc=cloudron' },
|
||||
{ name: 'LDAP_GROUPS_BASE_DN', value: 'ou=groups,dc=cloudron' },
|
||||
{ name: 'LDAP_BIND_DN', value: 'cn='+ app.id + ',ou=apps,dc=cloudron' },
|
||||
{ name: 'LDAP_BIND_PASSWORD', value: hat(4 * 128) } // this is ignored
|
||||
{ name: `${envPrefix}LDAP_SERVER`, value: '172.18.0.1' },
|
||||
{ name: `${envPrefix}LDAP_PORT`, value: '' + config.get('ldapPort') },
|
||||
{ name: `${envPrefix}LDAP_URL`, value: 'ldap://172.18.0.1:' + config.get('ldapPort') },
|
||||
{ name: `${envPrefix}LDAP_USERS_BASE_DN`, value: 'ou=users,dc=cloudron' },
|
||||
{ name: `${envPrefix}LDAP_GROUPS_BASE_DN`, value: 'ou=groups,dc=cloudron' },
|
||||
{ name: `${envPrefix}LDAP_BIND_DN`, value: 'cn='+ app.id + ',ou=apps,dc=cloudron' },
|
||||
{ name: `${envPrefix}LDAP_BIND_PASSWORD`, value: hat(4 * 128) } // this is ignored
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting up LDAP');
|
||||
@@ -905,14 +911,16 @@ function setupSendMail(app, options, callback) {
|
||||
|
||||
var password = error ? hat(4 * 48) : existingPassword; // see box#565 for password length
|
||||
|
||||
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
|
||||
|
||||
var env = [
|
||||
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_SMTP_PORT', value: '2525' },
|
||||
{ name: 'MAIL_SMTPS_PORT', value: '2465' },
|
||||
{ name: 'MAIL_SMTP_USERNAME', value: app.mailboxName + '@' + app.domain },
|
||||
{ name: 'MAIL_SMTP_PASSWORD', value: password },
|
||||
{ name: 'MAIL_FROM', value: app.mailboxName + '@' + app.domain },
|
||||
{ name: 'MAIL_DOMAIN', value: app.domain }
|
||||
{ name: `${envPrefix}MAIL_SMTP_SERVER`, value: 'mail' },
|
||||
{ name: `${envPrefix}MAIL_SMTP_PORT`, value: '2525' },
|
||||
{ name: `${envPrefix}MAIL_SMTPS_PORT`, value: '2465' },
|
||||
{ name: `${envPrefix}MAIL_SMTP_USERNAME`, value: app.mailboxName + '@' + app.domain },
|
||||
{ name: `${envPrefix}MAIL_SMTP_PASSWORD`, value: password },
|
||||
{ name: `${envPrefix}MAIL_FROM`, value: app.mailboxName + '@' + app.domain },
|
||||
{ name: `${envPrefix}MAIL_DOMAIN`, value: app.domain }
|
||||
];
|
||||
debugApp(app, 'Setting sendmail addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'sendmail', env, callback);
|
||||
@@ -941,13 +949,15 @@ function setupRecvMail(app, options, callback) {
|
||||
|
||||
var password = error ? hat(4 * 48) : existingPassword; // see box#565 for password length
|
||||
|
||||
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
|
||||
|
||||
var env = [
|
||||
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_IMAP_PORT', value: '9993' },
|
||||
{ name: 'MAIL_IMAP_USERNAME', value: app.mailboxName + '@' + app.domain },
|
||||
{ name: 'MAIL_IMAP_PASSWORD', value: password },
|
||||
{ name: 'MAIL_TO', value: app.mailboxName + '@' + app.domain },
|
||||
{ name: 'MAIL_DOMAIN', value: app.domain }
|
||||
{ name: `${envPrefix}MAIL_IMAP_SERVER`, value: 'mail' },
|
||||
{ name: `${envPrefix}MAIL_IMAP_PORT`, value: '9993' },
|
||||
{ name: `${envPrefix}MAIL_IMAP_USERNAME`, value: app.mailboxName + '@' + app.domain },
|
||||
{ name: `${envPrefix}MAIL_IMAP_PASSWORD`, value: password },
|
||||
{ name: `${envPrefix}MAIL_TO`, value: app.mailboxName + '@' + app.domain },
|
||||
{ name: `${envPrefix}MAIL_DOMAIN`, value: app.domain }
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting sendmail addon config to %j', env);
|
||||
@@ -992,6 +1002,7 @@ function startMysql(existingInfra, callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const cmd = `docker run --restart=always -d --name="mysql" \
|
||||
--hostname mysql \
|
||||
--net cloudron \
|
||||
--net-alias mysql \
|
||||
--log-driver syslog \
|
||||
@@ -1048,11 +1059,13 @@ function setupMySql(app, options, callback) {
|
||||
if (error) return callback(new Error('Error setting up mysql: ' + error));
|
||||
if (response.statusCode !== 201) return callback(new Error(`Error setting up mysql. Status code: ${response.statusCode} message: ${response.body.message}`));
|
||||
|
||||
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
|
||||
|
||||
var env = [
|
||||
{ name: 'MYSQL_USERNAME', value: data.username },
|
||||
{ name: 'MYSQL_PASSWORD', value: data.password },
|
||||
{ name: 'MYSQL_HOST', value: 'mysql' },
|
||||
{ name: 'MYSQL_PORT', value: '3306' }
|
||||
{ name: `${envPrefix}MYSQL_USERNAME`, value: data.username },
|
||||
{ name: `${envPrefix}MYSQL_PASSWORD`, value: data.password },
|
||||
{ name: `${envPrefix}MYSQL_HOST`, value: 'mysql' },
|
||||
{ name: `${envPrefix}MYSQL_PORT`, value: '3306' }
|
||||
];
|
||||
|
||||
if (options.multipleDatabases) {
|
||||
@@ -1204,6 +1217,7 @@ function startPostgresql(existingInfra, callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const cmd = `docker run --restart=always -d --name="postgresql" \
|
||||
--hostname postgresql \
|
||||
--net cloudron \
|
||||
--net-alias postgresql \
|
||||
--log-driver syslog \
|
||||
@@ -1258,13 +1272,15 @@ function setupPostgreSql(app, options, callback) {
|
||||
if (error) return callback(new Error('Error setting up postgresql: ' + error));
|
||||
if (response.statusCode !== 201) return callback(new Error(`Error setting up postgresql. Status code: ${response.statusCode} message: ${response.body.message}`));
|
||||
|
||||
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
|
||||
|
||||
var env = [
|
||||
{ name: 'POSTGRESQL_URL', value: `postgres://${data.username}:${data.password}@postgresql/${data.database}` },
|
||||
{ name: 'POSTGRESQL_USERNAME', value: data.username },
|
||||
{ name: 'POSTGRESQL_PASSWORD', value: data.password },
|
||||
{ name: 'POSTGRESQL_HOST', value: 'postgresql' },
|
||||
{ name: 'POSTGRESQL_PORT', value: '5432' },
|
||||
{ name: 'POSTGRESQL_DATABASE', value: data.database }
|
||||
{ name: `${envPrefix}POSTGRESQL_URL`, value: `postgres://${data.username}:${data.password}@postgresql/${data.database}` },
|
||||
{ name: `${envPrefix}POSTGRESQL_USERNAME`, value: data.username },
|
||||
{ name: `${envPrefix}POSTGRESQL_PASSWORD`, value: data.password },
|
||||
{ name: `${envPrefix}POSTGRESQL_HOST`, value: 'postgresql' },
|
||||
{ name: `${envPrefix}POSTGRESQL_PORT`, value: '5432' },
|
||||
{ name: `${envPrefix}POSTGRESQL_DATABASE`, value: data.database }
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting postgresql addon config to %j', env);
|
||||
@@ -1378,6 +1394,7 @@ function startMongodb(existingInfra, callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const cmd = `docker run --restart=always -d --name="mongodb" \
|
||||
--hostname mongodb \
|
||||
--net cloudron \
|
||||
--net-alias mongodb \
|
||||
--log-driver syslog \
|
||||
@@ -1430,13 +1447,15 @@ function setupMongoDb(app, options, callback) {
|
||||
if (error) return callback(new Error('Error setting up mongodb: ' + error));
|
||||
if (response.statusCode !== 201) return callback(new Error(`Error setting up mongodb. Status code: ${response.statusCode}`));
|
||||
|
||||
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
|
||||
|
||||
var env = [
|
||||
{ name: 'MONGODB_URL', value : `mongodb://${data.username}:${data.password}@mongodb/${data.database}` },
|
||||
{ name: 'MONGODB_USERNAME', value : data.username },
|
||||
{ name: 'MONGODB_PASSWORD', value: data.password },
|
||||
{ name: 'MONGODB_HOST', value : 'mongodb' },
|
||||
{ name: 'MONGODB_PORT', value : '27017' },
|
||||
{ name: 'MONGODB_DATABASE', value : data.database }
|
||||
{ name: `${envPrefix}MONGODB_URL`, value : `mongodb://${data.username}:${data.password}@mongodb/${data.database}` },
|
||||
{ name: `${envPrefix}MONGODB_USERNAME`, value : data.username },
|
||||
{ name: `${envPrefix}MONGODB_PASSWORD`, value: data.password },
|
||||
{ name: `${envPrefix}MONGODB_HOST`, value : 'mongodb' },
|
||||
{ name: `${envPrefix}MONGODB_PORT`, value : '27017' },
|
||||
{ name: `${envPrefix}MONGODB_DATABASE`, value : data.database }
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting mongodb addon config to %j', env);
|
||||
@@ -1572,6 +1591,7 @@ function setupRedis(app, options, callback) {
|
||||
const label = app.fqdn;
|
||||
// note that we do not add appId label because this interferes with the stop/start app logic
|
||||
const cmd = `docker run --restart=always -d --name=${redisName} \
|
||||
--hostname ${redisName} \
|
||||
--label=location=${label} \
|
||||
--net cloudron \
|
||||
--net-alias ${redisName} \
|
||||
@@ -1589,11 +1609,13 @@ function setupRedis(app, options, callback) {
|
||||
--label isCloudronManaged=true \
|
||||
--read-only -v /tmp -v /run ${tag}`;
|
||||
|
||||
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
|
||||
|
||||
var env = [
|
||||
{ name: 'REDIS_URL', value: 'redis://redisuser:' + redisPassword + '@redis-' + app.id },
|
||||
{ name: 'REDIS_PASSWORD', value: redisPassword },
|
||||
{ name: 'REDIS_HOST', value: redisName },
|
||||
{ name: 'REDIS_PORT', value: '6379' }
|
||||
{ name: `${envPrefix}REDIS_URL`, value: 'redis://redisuser:' + redisPassword + '@redis-' + app.id },
|
||||
{ name: `${envPrefix}REDIS_PASSWORD`, value: redisPassword },
|
||||
{ name: `${envPrefix}REDIS_HOST`, value: redisName },
|
||||
{ name: `${envPrefix}REDIS_PORT`, value: '6379' }
|
||||
];
|
||||
|
||||
async.series([
|
||||
|
||||
+18
-8
@@ -321,11 +321,12 @@ function validateDataDir(dataDir) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function getDuplicateErrorDetails(error, location, domainObject, portBindings) {
|
||||
function getDuplicateErrorDetails(error, location, domainObject, portBindings, alternateDomains) {
|
||||
assert.strictEqual(error.reason, DatabaseError.ALREADY_EXISTS);
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
assert(Array.isArray(alternateDomains));
|
||||
|
||||
var match = error.message.match(/ER_DUP_ENTRY: Duplicate entry '(.*)' for key '(.*)'/);
|
||||
if (!match) {
|
||||
@@ -333,10 +334,17 @@ function getDuplicateErrorDetails(error, location, domainObject, portBindings) {
|
||||
return new AppsError(AppsError.INTERNAL_ERROR, error);
|
||||
}
|
||||
|
||||
// check if the location conflicts
|
||||
// check if the location or alternateDomains conflicts
|
||||
if (match[2] === 'subdomain') {
|
||||
const fqdn = domains.fqdn(location, domainObject);
|
||||
return new AppsError(AppsError.ALREADY_EXISTS, `subdomain '${fqdn}' is in use`);
|
||||
// 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`);
|
||||
}
|
||||
|
||||
// check if any of the port bindings conflict
|
||||
@@ -672,7 +680,7 @@ function install(data, user, auditSource, callback) {
|
||||
};
|
||||
|
||||
appdb.add(appId, appStoreId, manifest, location, domain, ownerId, translatePortBindings(portBindings, manifest), data, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error, location, domainObject, portBindings));
|
||||
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));
|
||||
|
||||
@@ -832,7 +840,7 @@ function configure(appId, data, user, auditSource, callback) {
|
||||
debug(`configure: id:${appId}`);
|
||||
|
||||
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_CONFIGURE, values, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error, location, domainObject, portBindings));
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error, location, domainObject, portBindings, data.alternateDomains));
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
@@ -1121,7 +1129,7 @@ function clone(appId, data, user, auditSource, callback) {
|
||||
};
|
||||
|
||||
appdb.add(newAppId, app.appStoreId, manifest, location, domain, ownerId, translatePortBindings(portBindings, manifest), data, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error, location, domainObject, portBindings));
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error, location, domainObject, portBindings, []));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
purchaseApp({ appId: newAppId, appstoreId: app.appStoreId, manifestId: manifest.id }, function (error) {
|
||||
@@ -1210,6 +1218,8 @@ function stop(appId, callback) {
|
||||
function checkManifestConstraints(manifest) {
|
||||
assert(manifest && typeof manifest === 'object');
|
||||
|
||||
if (manifest.manifestVersion > 2) return new AppsError(AppsError.BAD_FIELD, 'Manifest version must be <= 2');
|
||||
|
||||
if (!manifest.dockerImage) return new AppsError(AppsError.BAD_FIELD, 'Missing dockerImage'); // dockerImage is optional in manifest
|
||||
|
||||
if (semver.valid(manifest.maxBoxVersion) && semver.gt(config.version(), manifest.maxBoxVersion)) {
|
||||
@@ -1454,7 +1464,7 @@ function downloadFile(appId, filePath, callback) {
|
||||
transform: function (chunk, ignoredEncoding, callback) {
|
||||
this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk;
|
||||
|
||||
while (true) {
|
||||
for (;;) {
|
||||
if (this._buffer.length < 8) break; // header is 8 bytes
|
||||
|
||||
var type = this._buffer.readUInt8(0);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
API_LOCATION: 'api', // this is unused but reserved for future use (#403)
|
||||
SMTP_LOCATION: 'smtp',
|
||||
IMAP_LOCATION: 'imap',
|
||||
|
||||
@@ -18,7 +17,6 @@ exports = module.exports = {
|
||||
],
|
||||
|
||||
ADMIN_LOCATION: 'my',
|
||||
DKIM_SELECTOR: 'cloudron',
|
||||
|
||||
NGINX_DEFAULT_CONFIG_FILE_NAME: 'default.conf',
|
||||
|
||||
|
||||
@@ -13,6 +13,10 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
const DEFAULT_SPEC = {
|
||||
appstore: {
|
||||
blacklist: [],
|
||||
whitelist: null // null imples, not set. this is an object and not an array
|
||||
},
|
||||
backups: {
|
||||
configurable: true
|
||||
},
|
||||
|
||||
+4
-4
@@ -131,7 +131,7 @@ function getInternal(dnsConfig, zoneName, name, type, callback) {
|
||||
|
||||
result.body.records.forEach(function (r) {
|
||||
// name.com api simply strips empty properties
|
||||
r.host = r.host || '@';
|
||||
r.host = r.host || '';
|
||||
});
|
||||
|
||||
var results = result.body.records.filter(function (r) {
|
||||
@@ -153,7 +153,7 @@ function upsert(domainObject, location, type, values, callback) {
|
||||
|
||||
const dnsConfig = domainObject.config,
|
||||
zoneName = domainObject.zoneName,
|
||||
name = domains.getName(domainObject, location, type) || '@';
|
||||
name = domains.getName(domainObject, location, type) || '';
|
||||
|
||||
debug(`upsert: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
||||
|
||||
@@ -174,7 +174,7 @@ function get(domainObject, location, type, callback) {
|
||||
|
||||
const dnsConfig = domainObject.config,
|
||||
zoneName = domainObject.zoneName,
|
||||
name = domains.getName(domainObject, location, type) || '@';
|
||||
name = domains.getName(domainObject, location, type) || '';
|
||||
|
||||
getInternal(dnsConfig, zoneName, name, type, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
@@ -196,7 +196,7 @@ function del(domainObject, location, type, values, callback) {
|
||||
|
||||
const dnsConfig = domainObject.config,
|
||||
zoneName = domainObject.zoneName,
|
||||
name = domains.getName(domainObject, location, type) || '@';
|
||||
name = domains.getName(domainObject, location, type) || '';
|
||||
|
||||
debug(`del: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
||||
|
||||
|
||||
+19
-9
@@ -181,14 +181,17 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
var manifest = app.manifest;
|
||||
var exposedPorts = {}, dockerPortBindings = { };
|
||||
var domain = app.fqdn;
|
||||
// TODO: these should all have the CLOUDRON_ prefix
|
||||
var stdEnv = [
|
||||
|
||||
const envPrefix = manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
|
||||
|
||||
let stdEnv = [
|
||||
'CLOUDRON=1',
|
||||
'CLOUDRON_PROXY_IP=172.18.0.1',
|
||||
'WEBADMIN_ORIGIN=' + config.adminOrigin(),
|
||||
'API_ORIGIN=' + config.adminOrigin(),
|
||||
'APP_ORIGIN=https://' + domain,
|
||||
'APP_DOMAIN=' + domain
|
||||
`CLOUDRON_APP_HOSTNAME=${name}`,
|
||||
`${envPrefix}WEBADMIN_ORIGIN=${config.adminOrigin()}`,
|
||||
`${envPrefix}API_ORIGIN=${config.adminOrigin()}`,
|
||||
`${envPrefix}APP_ORIGIN=https://${domain}`,
|
||||
`${envPrefix}APP_DOMAIN=${domain}`
|
||||
];
|
||||
|
||||
// docker portBindings requires ports to be exposed
|
||||
@@ -235,9 +238,9 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
// Note that Hostname has no effect on DNS. We have to use the --net-alias for dns.
|
||||
// Hostname cannot be set with container NetworkMode
|
||||
var containerOptions = {
|
||||
name: name, // used for filtering logs
|
||||
name: name, // for referencing containers
|
||||
Tty: isAppContainer,
|
||||
Hostname: app.id, // set to something 'constant' so app containers can use this to communicate (across app updates)
|
||||
Hostname: name,
|
||||
Image: app.manifest.dockerImage,
|
||||
Cmd: (isAppContainer && app.debugMode && app.debugMode.cmd) ? app.debugMode.cmd : cmd,
|
||||
Env: stdEnv.concat(addonEnv).concat(portEnv).concat(appEnv),
|
||||
@@ -273,10 +276,17 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
},
|
||||
CpuShares: 512, // relative to 1024 for system processes
|
||||
VolumesFrom: isAppContainer ? null : [ app.containerId + ':rw' ],
|
||||
NetworkMode: 'cloudron',
|
||||
NetworkMode: 'cloudron', // user defined bridge network
|
||||
Dns: ['172.18.0.1'], // use internal dns
|
||||
DnsSearch: ['.'], // use internal dns
|
||||
SecurityOpt: [ 'apparmor=docker-cloudron-app' ]
|
||||
},
|
||||
NetworkingConfig: {
|
||||
EndpointsConfig: {
|
||||
cloudron: {
|
||||
Aliases: [ name ] // this allows sub-containers reach app containers by name
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -145,7 +145,6 @@ function validateHostname(location, domainObject) {
|
||||
const hostname = fqdn(location, domainObject);
|
||||
|
||||
const RESERVED_LOCATIONS = [
|
||||
constants.API_LOCATION,
|
||||
constants.SMTP_LOCATION,
|
||||
constants.IMAP_LOCATION
|
||||
];
|
||||
|
||||
@@ -19,6 +19,7 @@ function startGraphite(existingInfra, callback) {
|
||||
if (existingInfra.version === infra.version && infra.images.graphite.tag === existingInfra.images.graphite.tag) return callback();
|
||||
|
||||
const cmd = `docker run --restart=always -d --name="graphite" \
|
||||
--hostname graphite \
|
||||
--net cloudron \
|
||||
--net-alias graphite \
|
||||
--log-driver syslog \
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
exports = module.exports = {
|
||||
// a version change recreates all containers with latest docker config
|
||||
'version': '48.14.0',
|
||||
'version': '48.15.0',
|
||||
|
||||
'baseImages': [
|
||||
{ repo: 'cloudron/base', tag: 'cloudron/base:1.0.0@sha256:147a648a068a2e746644746bbfb42eb7a50d682437cead3c67c933c546357617' }
|
||||
@@ -19,7 +19,7 @@ exports = module.exports = {
|
||||
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:2.0.2@sha256:6dcee0731dfb9b013ed94d56205eee219040ee806c7e251db3b3886eaa4947ff' },
|
||||
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:2.0.2@sha256:95e006390ddce7db637e1672eb6f3c257d3c2652747424f529b1dee3cbe6728c' },
|
||||
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.0.0@sha256:8a88dd334b62b578530a014ca1a2425a54cb9df1e475f5d3a36806e5cfa22121' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.3.0@sha256:5118cd2cc755a53661485a1c7507cbb546f765642fc83a82f0351d646221342f' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.3.1@sha256:9693e3ae42a12a7ac8cf5df94d828d46f5b22b4e2e1c7d1bc614d6ee2a22c365' },
|
||||
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.0.2@sha256:454f035d60b768153d4f31210380271b5ba1c09367c9d95c7fa37f9e39d2f59c' },
|
||||
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:0.1.0@sha256:e177c5bf5f38c84ce1dea35649c22a1b05f96eec67a54a812c5a35e585670f0f' }
|
||||
}
|
||||
|
||||
+28
-18
@@ -210,10 +210,14 @@ function verifyRelay(relay, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function checkDkim(domain, callback) {
|
||||
var dkim = {
|
||||
domain: `${constants.DKIM_SELECTOR}._domainkey.${domain}`,
|
||||
name: `${constants.DKIM_SELECTOR}._domainkey`,
|
||||
function checkDkim(mailDomain, callback) {
|
||||
assert.strictEqual(typeof mailDomain, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const domain = mailDomain.domain;
|
||||
let dkim = {
|
||||
domain: `${mailDomain.dkimSelector}._domainkey.${domain}`,
|
||||
name: `${mailDomain.dkimSelector}._domainkey`,
|
||||
type: 'TXT',
|
||||
expected: null,
|
||||
value: null,
|
||||
@@ -495,28 +499,28 @@ function getStatus(domain, callback) {
|
||||
|
||||
const mailFqdn = config.mailFqdn();
|
||||
|
||||
getDomain(domain, function (error, result) {
|
||||
getDomain(domain, function (error, mailDomain) {
|
||||
if (error) return callback(error);
|
||||
|
||||
let checks = [];
|
||||
if (result.enabled) {
|
||||
if (mailDomain.enabled) {
|
||||
checks.push(
|
||||
recordResult('dns.mx', checkMx.bind(null, domain, mailFqdn)),
|
||||
recordResult('dns.dmarc', checkDmarc.bind(null, domain))
|
||||
);
|
||||
}
|
||||
|
||||
if (result.relay.provider === 'cloudron-smtp') {
|
||||
if (mailDomain.relay.provider === 'cloudron-smtp') {
|
||||
// these tests currently only make sense when using Cloudron's SMTP server at this point
|
||||
checks.push(
|
||||
recordResult('dns.spf', checkSpf.bind(null, domain, mailFqdn)),
|
||||
recordResult('dns.dkim', checkDkim.bind(null, domain)),
|
||||
recordResult('dns.dkim', checkDkim.bind(null, mailDomain)),
|
||||
recordResult('dns.ptr', checkPtr.bind(null, mailFqdn)),
|
||||
recordResult('relay', checkOutboundPort25),
|
||||
recordResult('rbl', checkRblStatus.bind(null, domain))
|
||||
);
|
||||
} else if (result.relay.provider !== 'noop') {
|
||||
checks.push(recordResult('relay', checkSmtpRelay.bind(null, result.relay)));
|
||||
} else if (mailDomain.relay.provider !== 'noop') {
|
||||
checks.push(recordResult('relay', checkSmtpRelay.bind(null, mailDomain.relay)));
|
||||
}
|
||||
|
||||
async.parallel(checks, function () {
|
||||
@@ -770,9 +774,10 @@ function txtRecordsWithSpf(domain, mailFqdn, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function ensureDkimKeySync(domain) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
function ensureDkimKeySync(mailDomain) {
|
||||
assert.strictEqual(typeof mailDomain, 'object');
|
||||
|
||||
const domain = mailDomain.domain;
|
||||
const dkimPath = path.join(paths.MAIL_DATA_DIR, `dkim/${domain}`);
|
||||
const dkimPrivateKeyFile = path.join(dkimPath, 'private');
|
||||
const dkimPublicKeyFile = path.join(dkimPath, 'public');
|
||||
@@ -795,7 +800,10 @@ function ensureDkimKeySync(domain) {
|
||||
if (!safe.child_process.execSync('openssl genrsa -out ' + dkimPrivateKeyFile + ' 1024')) return new MailError(MailError.INTERNAL_ERROR, safe.error);
|
||||
if (!safe.child_process.execSync('openssl rsa -in ' + dkimPrivateKeyFile + ' -out ' + dkimPublicKeyFile + ' -pubout -outform PEM')) return new MailError(MailError.INTERNAL_ERROR, safe.error);
|
||||
|
||||
if (!safe.fs.writeFileSync(dkimSelectorFile, constants.DKIM_SELECTOR, 'utf8')) return new MailError(MailError.INTERNAL_ERROR, safe.error);
|
||||
if (!safe.fs.writeFileSync(dkimSelectorFile, mailDomain.dkimSelector, 'utf8')) return new MailError(MailError.INTERNAL_ERROR, safe.error);
|
||||
|
||||
// if the 'yellowtent' user of OS and the 'cloudron' user of mail container don't match, the keys become inaccessible by mail code
|
||||
if (!safe.fs.chmodSync(dkimPrivateKeyFile, 0o644)) return new MailError(MailError.INTERNAL_ERROR, safe.error);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -826,11 +834,11 @@ function upsertDnsRecords(domain, mailFqdn, callback) {
|
||||
|
||||
debug(`upsertDnsRecords: updating mail dns records of domain ${domain} and mail fqdn ${mailFqdn}`);
|
||||
|
||||
maildb.get(domain, function (error, result) {
|
||||
maildb.get(domain, function (error, mailDomain) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND));
|
||||
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
|
||||
|
||||
error = ensureDkimKeySync(domain);
|
||||
error = ensureDkimKeySync(mailDomain);
|
||||
if (error) return callback(error);
|
||||
|
||||
if (process.env.BOX_ENV === 'test') return callback();
|
||||
@@ -839,11 +847,11 @@ function upsertDnsRecords(domain, mailFqdn, callback) {
|
||||
if (!dkimKey) return callback(new MailError(MailError.INTERNAL_ERROR, new Error('Failed to read dkim public key')));
|
||||
|
||||
// t=s limits the domainkey to this domain and not it's subdomains
|
||||
var dkimRecord = { subdomain: `${constants.DKIM_SELECTOR}._domainkey`, domain: domain, type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] };
|
||||
var dkimRecord = { subdomain: `${mailDomain.dkimSelector}._domainkey`, domain: domain, type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] };
|
||||
|
||||
var records = [ ];
|
||||
records.push(dkimRecord);
|
||||
if (result.enabled) {
|
||||
if (mailDomain.enabled) {
|
||||
records.push({ subdomain: '_dmarc', domain: domain, type: 'TXT', values: [ '"v=DMARC1; p=reject; pct=100"' ] });
|
||||
records.push({ subdomain: '', domain: domain, type: 'MX', values: [ '10 ' + mailFqdn + '.' ] });
|
||||
}
|
||||
@@ -901,7 +909,9 @@ function addDomain(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
maildb.add(domain, function (error) {
|
||||
const dkimSelector = domain === config.adminDomain() ? 'cloudron' : ('cloudron-' + config.adminDomain().replace(/\./g, ''));
|
||||
|
||||
maildb.add(domain, { dkimSelector }, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new MailError(MailError.ALREADY_EXISTS, 'Domain already exists'));
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'No such domain'));
|
||||
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
|
||||
|
||||
@@ -4,10 +4,37 @@ Dear Cloudron Admin,
|
||||
|
||||
The application '<%= title %>' installed at <%= appFqdn %> was updated to app package version <%= version %>.
|
||||
|
||||
Changes:
|
||||
<%= changelog %>
|
||||
|
||||
Powered by https://cloudron.io
|
||||
|
||||
Sent at: <%= new Date().toUTCString() %>
|
||||
|
||||
<% } else { %>
|
||||
|
||||
<center>
|
||||
|
||||
<img src="<%= cloudronAvatarUrl %>" width="128px" height="128px"/>
|
||||
|
||||
<h3>Dear <%= cloudronName %> Admin,</h3>
|
||||
|
||||
<br/>
|
||||
|
||||
<div style="width: 650px; text-align: left;">
|
||||
The application '<%= title %>' installed at <%= appFqdn %> was updated to app package version <%= version %>.
|
||||
|
||||
<h5>Changelog:</h5>
|
||||
<%- changelogHTML %>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<div style="font-size: 10px; color: #333333; background: #ffffff;">
|
||||
Powered by <a href="https://cloudron.io">Cloudron</a>.<br/>
|
||||
Sent at: <%= new Date().toUTCString() %>
|
||||
</div>
|
||||
|
||||
</center>
|
||||
<% } %>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Dear Cloudron Admin,
|
||||
|
||||
<% for (var i = 0; i < apps.length; i++) { -%>
|
||||
A new version <%= apps[i].updateInfo.manifest.version %> of the app '<%= apps[i].app.manifest.title %>' installed at <%= apps[i].app.fqdn %> is available!
|
||||
A new version <%= apps[i].updateInfo.manifest.version %> of the app '<%= apps[i].app.manifest.title %>' installed at <%= apps[i].app.fqdn %> is available.
|
||||
|
||||
Changes:
|
||||
<%= apps[i].updateInfo.manifest.changelog %>
|
||||
@@ -28,10 +28,12 @@ Sent at: <%= new Date().toUTCString() %>
|
||||
|
||||
<h3>Dear <%= cloudronName %> Admin,</h3>
|
||||
|
||||
<br/>
|
||||
|
||||
<div style="width: 650px; text-align: left;">
|
||||
<% for (var i = 0; i < apps.length; i++) { -%>
|
||||
<p>
|
||||
A new version <%= apps[i].updateInfo.manifest.version %> of the app '<%= apps[i].app.manifest.title %>' installed at <a href="https://<%= apps[i].app.fqdn %>"><%= apps[i].app.fqdn %></a> is available!
|
||||
A new version <%= apps[i].updateInfo.manifest.version %> of the app '<%= apps[i].app.manifest.title %>' installed at <a href="https://<%= apps[i].app.fqdn %>"><%= apps[i].app.fqdn %></a> is available.
|
||||
</p>
|
||||
|
||||
<h5>Changelog:</h5>
|
||||
@@ -40,21 +42,20 @@ Sent at: <%= new Date().toUTCString() %>
|
||||
<br/>
|
||||
<% } -%>
|
||||
|
||||
<% if (!hasSubscription) { %>
|
||||
<% if (!hasSubscription) { -%>
|
||||
<p>Keep your Cloudron automatically up-to-date and secure by upgrading to a <a href="<%= webadminUrl %>/#/settings">paid plan</a>.</p>
|
||||
<% } else { -%>
|
||||
<% } else { -%>
|
||||
<p>
|
||||
<br/>
|
||||
<center><a href="<%= webadminUrl %>">Update now</a></center>
|
||||
<br/>
|
||||
</p>
|
||||
<% } -%>
|
||||
|
||||
<br/>
|
||||
<% } -%>
|
||||
</div>
|
||||
|
||||
<div style="font-size: 10px; color: #333333; background: #ffffff;">
|
||||
Powered by <a href="https://cloudron.io">Cloudron</a>.
|
||||
Powered by <a href="https://cloudron.io">Cloudron</a>.<br/>
|
||||
Sent at: <%= new Date().toUTCString() %>
|
||||
</div>
|
||||
|
||||
</center>
|
||||
|
||||
@@ -1,58 +1,48 @@
|
||||
{
|
||||
"app_updated.ejs": {
|
||||
"format": "html",
|
||||
"title": "WordPress",
|
||||
"appFqdn": "updated.smartserver.io",
|
||||
"version": "1.3.4",
|
||||
"changelog": "* This has changed\n * and that as well",
|
||||
"changelogHTML": "<ul><li>This has changed</li><li>and that as well</li></ul>",
|
||||
"cloudronName": "Smartserver",
|
||||
"cloudronAvatarUrl": "https://cloudron.io/img/logo.png"
|
||||
},
|
||||
"app_updates_available.ejs": {
|
||||
"format": "html",
|
||||
"webadminUrl": "https://my.cloudron.io",
|
||||
"cloudronName": "Smartserver",
|
||||
"cloudronAvatarUrl": "https://cloudron.io/img/logo.png",
|
||||
"info": {
|
||||
"pendingBoxUpdate": {
|
||||
"version": "1.3.7",
|
||||
"changelog": [
|
||||
"Feature one",
|
||||
"Feature two"
|
||||
]
|
||||
},
|
||||
"pendingAppUpdates": [{
|
||||
"manifest": {
|
||||
"title": "Wordpress",
|
||||
"version": "1.2.3",
|
||||
"changelog": "* This has changed\n * and that as well"
|
||||
}
|
||||
}],
|
||||
"finishedBoxUpdates": [{
|
||||
"boxUpdateInfo": {
|
||||
"version": "1.0.1",
|
||||
"changelog": [
|
||||
"Feature one",
|
||||
"Feature two"
|
||||
]
|
||||
}
|
||||
}, {
|
||||
"boxUpdateInfo": {
|
||||
"version": "1.0.2",
|
||||
"changelog": [
|
||||
"Feature one",
|
||||
"Feature two",
|
||||
"Feature three"
|
||||
]
|
||||
}
|
||||
}],
|
||||
"finishedAppUpdates": [{
|
||||
"toManifest": {
|
||||
"title": "Rocket.Chat",
|
||||
"version": "0.2.1",
|
||||
"changelog": "* This has changed\n * and that as well\n * some more"
|
||||
}
|
||||
}, {
|
||||
"toManifest": {
|
||||
"title": "Redmine",
|
||||
"version": "1.2.1",
|
||||
"changelog": "* This has changed\n * and that as well\n * some more"
|
||||
}
|
||||
}],
|
||||
"certRenewals": [],
|
||||
"finishedBackups": [],
|
||||
"usersAdded": [],
|
||||
"usersRemoved": [],
|
||||
"hasSubscription": false
|
||||
}
|
||||
"hasSubscription": true,
|
||||
"apps": [{
|
||||
"updateInfo": {
|
||||
"manifest": {
|
||||
"version": "1.4.3"
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"fqdn": "site.smartserver.io",
|
||||
"manifest": {
|
||||
"title": "WordPress"
|
||||
}
|
||||
},
|
||||
"changelog": "* This has changed\n * and that as well",
|
||||
"changelogHTML": "<ul><li>This has changed</li><li>and that as well</li></ul>"
|
||||
}, {
|
||||
"updateInfo": {
|
||||
"manifest": {
|
||||
"version": "0.1.3"
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"fqdn": "another.smartserver.io",
|
||||
"manifest": {
|
||||
"title": "RocketChat"
|
||||
}
|
||||
},
|
||||
"changelog": "* This has changed\n * and that as well",
|
||||
"changelogHTML": "<ul><li>This has changed</li><li>and that as well</li></ul>"
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
+5
-3
@@ -19,7 +19,7 @@ var assert = require('assert'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
safe = require('safetydance');
|
||||
|
||||
var MAILDB_FIELDS = [ 'domain', 'enabled', 'mailFromValidation', 'catchAllJson', 'relayJson' ].join(',');
|
||||
var MAILDB_FIELDS = [ 'domain', 'enabled', 'mailFromValidation', 'catchAllJson', 'relayJson', 'dkimSelector' ].join(',');
|
||||
|
||||
function postProcess(data) {
|
||||
data.enabled = !!data.enabled; // int to boolean
|
||||
@@ -34,10 +34,12 @@ function postProcess(data) {
|
||||
return data;
|
||||
}
|
||||
|
||||
function add(domain, callback) {
|
||||
function add(domain, data, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof data, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('INSERT INTO mail (domain) VALUES (?)', [ domain ], function (error) {
|
||||
database.query('INSERT INTO mail (domain, dkimSelector) VALUES (?, ?)', [ domain, data.dkimSelector || 'cloudron' ], function (error) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, 'mail domain already exists'));
|
||||
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND), 'no such domain');
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
+20
-2
@@ -306,11 +306,29 @@ function appUpdated(mailTo, app, callback) {
|
||||
getMailConfig(function (error, mailConfig) {
|
||||
if (error) return debug('Error getting mail details:', error);
|
||||
|
||||
var converter = new showdown.Converter();
|
||||
var templateData = {
|
||||
title: app.manifest.title,
|
||||
appFqdn: app.fqdn,
|
||||
version: app.manifest.version,
|
||||
changelog: app.manifest.changelog,
|
||||
changelogHTML: converter.makeHtml(app.manifest.changelog),
|
||||
cloudronName: mailConfig.cloudronName,
|
||||
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
|
||||
};
|
||||
|
||||
var templateDataText = JSON.parse(JSON.stringify(templateData));
|
||||
templateDataText.format = 'text';
|
||||
|
||||
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
|
||||
templateDataHTML.format = 'html';
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig.notificationFrom,
|
||||
to: mailTo,
|
||||
subject: util.format('[%s] App %s was updated', mailConfig.cloudronName, app.fqdn),
|
||||
text: render('app_updated.ejs', { title: app.manifest.title, appFqdn: app.fqdn, version: app.manifest.version, format: 'text' })
|
||||
subject: `[${mailConfig.cloudronName}] App ${app.fqdn} was updated`,
|
||||
text: render('app_updated.ejs', templateDataText),
|
||||
html: render('app_updated.ejs', templateDataHTML)
|
||||
};
|
||||
|
||||
sendMail(mailOptions, callback);
|
||||
|
||||
+5
-2
@@ -119,10 +119,13 @@ function pruneInfraImages(callback) {
|
||||
if (!line) continue;
|
||||
let parts = line.split(' '); // [ ID, Repo:Tag@Digest ]
|
||||
if (image.tag === parts[1]) continue; // keep
|
||||
debug(`pruneInfraImages: removing unused image of ${image.repo}: ${line}`);
|
||||
debug(`pruneInfraImages: removing unused image of ${image.repo}: tag: ${parts[1]} id: ${parts[0]}`);
|
||||
|
||||
shell.exec('pruneInfraImages', `docker rmi ${parts[0]}`, iteratorCallback);
|
||||
let result = safe.child_process.execSync(`docker rmi ${parts[0]}`, { encoding: 'utf8' });
|
||||
if (result === null) debug(`Erroring removing image ${parts[0]}: ${safe.error.mesage}`);
|
||||
}
|
||||
|
||||
iteratorCallback();
|
||||
}, callback);
|
||||
}
|
||||
|
||||
|
||||
+5
-30
@@ -87,29 +87,6 @@ function setProgress(task, message, callback) {
|
||||
callback();
|
||||
}
|
||||
|
||||
// some conf that can be setup during provision time since the post-activation API requires user tokens
|
||||
function autoprovision(autoconf, callback) {
|
||||
assert.strictEqual(typeof autoconf, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
async.eachSeries(Object.keys(autoconf), function (key, iteratorDone) {
|
||||
debug(`autoprovision: ${key}`);
|
||||
|
||||
switch (key) {
|
||||
case 'backupConfig':
|
||||
settings.setBackupConfig(autoconf[key], iteratorDone);
|
||||
break;
|
||||
default:
|
||||
debug(`autoprovision: ${key} ignored`);
|
||||
return iteratorDone();
|
||||
}
|
||||
}, function (error) {
|
||||
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function autoRegister(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -147,9 +124,9 @@ function unprovision(callback) {
|
||||
}
|
||||
|
||||
|
||||
function setup(dnsConfig, autoconf, auditSource, callback) {
|
||||
function setup(dnsConfig, backupConfig, auditSource, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof autoconf, 'object');
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -194,9 +171,9 @@ function setup(dnsConfig, autoconf, auditSource, callback) {
|
||||
autoRegister.bind(null, domain),
|
||||
domains.prepareDashboardDomain.bind(null, domain, auditSource, (progress) => setProgress('setup', progress.message, NOOP_CALLBACK)),
|
||||
cloudron.setDashboardDomain.bind(null, domain, auditSource), // this sets up the config.fqdn()
|
||||
mail.addDomain.bind(null, domain), // this relies on config.mailFqdn()
|
||||
mail.addDomain.bind(null, domain), // this relies on config.mailFqdn() and config.adminDomain()
|
||||
setProgress.bind(null, 'setup', 'Applying auto-configuration'),
|
||||
autoprovision.bind(null, autoconf),
|
||||
(next) => { if (!backupConfig) return next(); settings.setBackupConfig(backupConfig, next); },
|
||||
setProgress.bind(null, 'setup', 'Done'),
|
||||
eventlog.add.bind(null, eventlog.ACTION_PROVISION, auditSource, { })
|
||||
], function (error) {
|
||||
@@ -267,11 +244,10 @@ function activate(username, password, email, displayName, ip, auditSource, callb
|
||||
});
|
||||
}
|
||||
|
||||
function restore(backupConfig, backupId, version, autoconf, auditSource, callback) {
|
||||
function restore(backupConfig, backupId, version, auditSource, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
assert.strictEqual(typeof version, 'string');
|
||||
assert.strictEqual(typeof autoconf, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -305,7 +281,6 @@ function restore(backupConfig, backupId, version, autoconf, auditSource, callbac
|
||||
setProgress.bind(null, 'restore', 'Downloading backup'),
|
||||
backups.restore.bind(null, backupConfig, backupId, (progress) => setProgress('restore', progress.message, NOOP_CALLBACK)),
|
||||
setProgress.bind(null, 'restore', 'Applying auto-configuration'),
|
||||
autoprovision.bind(null, autoconf),
|
||||
// currently, our suggested restore flow is after a dnsSetup. The dnSetup creates DKIM keys and updates the DNS
|
||||
// for this reason, we have to re-setup DNS after a restore so it has DKIm from the backup
|
||||
// Once we have a 100% IP based restore, we can skip this
|
||||
|
||||
+18
-1
@@ -12,9 +12,19 @@ exports = module.exports = {
|
||||
var appstore = require('../appstore.js'),
|
||||
AppstoreError = appstore.AppstoreError,
|
||||
assert = require('assert'),
|
||||
custom = require('../custom.js'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess;
|
||||
|
||||
function isAppAllowed(appstoreId) {
|
||||
if (custom.spec().appstore.blacklist.includes(appstoreId)) return false;
|
||||
|
||||
if (!custom.spec().appstore.whitelist) return true;
|
||||
if (!custom.spec().appstore.whitelist[appstoreId]) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getApps(req, res, next) {
|
||||
appstore.getApps(function (error, apps) {
|
||||
if (error && error.reason === AppstoreError.INVALID_TOKEN) return next(new HttpError(402, error.message));
|
||||
@@ -22,13 +32,18 @@ function getApps(req, res, next) {
|
||||
if (error && error.reason === AppstoreError.NOT_REGISTERED) return next(new HttpError(412, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, { apps: apps }));
|
||||
let filteredApps = apps.filter((app) => !custom.spec().appstore.blacklist.includes(app.id));
|
||||
if (custom.spec().appstore.whitelist) filteredApps = filteredApps.filter((app) => app.id in custom.spec().appstore.whitelist);
|
||||
|
||||
next(new HttpSuccess(200, { apps: filteredApps }));
|
||||
});
|
||||
}
|
||||
|
||||
function getApp(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.appstoreId, 'string');
|
||||
|
||||
if (!isAppAllowed(req.params.appstoreId)) return next(new HttpError(405, 'feature disabled by admin'));
|
||||
|
||||
appstore.getApp(req.params.appstoreId, function (error, app) {
|
||||
if (error && error.reason === AppstoreError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error && error.reason === AppstoreError.INVALID_TOKEN) return next(new HttpError(402, error.message));
|
||||
@@ -44,6 +59,8 @@ function getAppVersion(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.appstoreId, 'string');
|
||||
assert.strictEqual(typeof req.params.versionId, 'string');
|
||||
|
||||
if (!isAppAllowed(req.params.appstoreId)) return next(new HttpError(405, 'feature disabled by admin'));
|
||||
|
||||
appstore.getAppVersion(req.params.appstoreId, req.params.versionId, function (error, manifest) {
|
||||
if (error && error.reason === AppstoreError.NOT_FOUND) return next(new HttpError(404, 'No such app or version'));
|
||||
if (error && error.reason === AppstoreError.INVALID_TOKEN) return next(new HttpError(402, error.message));
|
||||
|
||||
@@ -53,13 +53,12 @@ function setup(req, res, next) {
|
||||
if ('tlsConfig' in dnsConfig && typeof dnsConfig.tlsConfig !== 'object') return next(new HttpError(400, 'tlsConfig must be an object'));
|
||||
if (dnsConfig.tlsConfig && (!dnsConfig.tlsConfig.provider || typeof dnsConfig.tlsConfig.provider !== 'string')) return next(new HttpError(400, 'tlsConfig.provider must be a string'));
|
||||
|
||||
// TODO: validate subfields of these objects
|
||||
if (req.body.autoconf && typeof req.body.autoconf !== 'object') return next(new HttpError(400, 'autoconf must be an object'));
|
||||
if ('backupConfig' in req.body && typeof req.body.backupConfig !== 'object') return next(new HttpError(400, 'backupConfig must be an object'));
|
||||
|
||||
// it can take sometime to setup DNS, register cloudron
|
||||
req.clearTimeout();
|
||||
|
||||
provision.setup(dnsConfig, req.body.autoconf || {}, auditSource.fromRequest(req), function (error) {
|
||||
provision.setup(dnsConfig, req.body.backupConfig || null, auditSource.fromRequest(req), function (error) {
|
||||
if (error && error.reason === ProvisionError.ALREADY_SETUP) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === ProvisionError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === ProvisionError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
@@ -108,10 +107,7 @@ function restore(req, res, next) {
|
||||
if (typeof req.body.backupId !== 'string') return next(new HttpError(400, 'backupId must be a string or null'));
|
||||
if (typeof req.body.version !== 'string') return next(new HttpError(400, 'version must be a string'));
|
||||
|
||||
// TODO: validate subfields of these objects
|
||||
if (req.body.autoconf && typeof req.body.autoconf !== 'object') return next(new HttpError(400, 'autoconf must be an object'));
|
||||
|
||||
provision.restore(backupConfig, req.body.backupId, req.body.version, req.body.autoconf || {}, auditSource.fromRequest(req), function (error) {
|
||||
provision.restore(backupConfig, req.body.backupId, req.body.version, auditSource.fromRequest(req), function (error) {
|
||||
if (error && error.reason === ProvisionError.ALREADY_SETUP) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === ProvisionError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === ProvisionError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
|
||||
@@ -17,6 +17,8 @@ var addons = require('../addons.js'),
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess;
|
||||
|
||||
function getAll(req, res, next) {
|
||||
req.clearTimeout(); // can take a while to get status of all services
|
||||
|
||||
addons.getServices(function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ function createTicket(req, res, next) {
|
||||
appstore.createTicket(_.extend({ }, req.body, { email: req.user.email, displayName: req.user.displayName }), function (error) {
|
||||
if (error) return next(new HttpError(503, `Error contacting cloudron.io: ${error.message}. Please email ${custom.spec().support.email}`));
|
||||
|
||||
next(new HttpSuccess(201, {}));
|
||||
next(new HttpSuccess(201, { message: `An email for sent to ${custom.spec().support.email}. We will get back shortly!` }));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -381,13 +381,13 @@ describe('App API', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('app install fails - reserved api location', function (done) {
|
||||
it('app install fails - reserved smtp location', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send({ manifest: APP_MANIFEST, location: constants.API_LOCATION, accessRestriction: null, domain: DOMAIN_0.domain })
|
||||
.send({ manifest: APP_MANIFEST, location: constants.SMTP_LOCATION, accessRestriction: null, domain: DOMAIN_0.domain })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(res.body.message).to.contain(constants.API_LOCATION + ' is reserved');
|
||||
expect(res.body.message).to.contain(constants.SMTP_LOCATION + ' is reserved');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -239,7 +239,7 @@ describe('Mail API', function () {
|
||||
callback(null, dnsAnswerQueue[hostname][type]);
|
||||
};
|
||||
|
||||
dkimDomain = 'cloudron._domainkey.' + DOMAIN_0.domain;
|
||||
dkimDomain = 'cloudron-admincom._domainkey.' + DOMAIN_0.domain;
|
||||
spfDomain = DOMAIN_0.domain;
|
||||
mxDomain = DOMAIN_0.domain;
|
||||
dmarcDomain = '_dmarc.' + DOMAIN_0.domain;
|
||||
|
||||
@@ -19,6 +19,7 @@ function startSftp(existingInfra, callback) {
|
||||
if (existingInfra.version === infra.version && infra.images.graphite.tag === existingInfra.images.graphite.tag) return callback();
|
||||
|
||||
const cmd = `docker run --restart=always -d --name="sftp" \
|
||||
--hostname sftp \
|
||||
--net cloudron \
|
||||
--net-alias sftp \
|
||||
--log-driver syslog \
|
||||
|
||||
@@ -2044,7 +2044,7 @@ describe('database', function () {
|
||||
before(function (done) {
|
||||
async.series([
|
||||
domaindb.add.bind(null, DOMAIN_0.domain, { zoneName: DOMAIN_0.zoneName, provider: DOMAIN_0.provider, config: DOMAIN_0.config, tlsConfig: DOMAIN_0.tlsConfig }),
|
||||
maildb.add.bind(null, DOMAIN_0.domain)
|
||||
maildb.add.bind(null, DOMAIN_0.domain, {})
|
||||
], done);
|
||||
});
|
||||
|
||||
@@ -2206,7 +2206,8 @@ describe('database', function () {
|
||||
enabled: false,
|
||||
relay: { provider: 'cloudron-smtp' },
|
||||
catchAll: [ ],
|
||||
mailFromValidation: true
|
||||
mailFromValidation: true,
|
||||
dkimSelector: 'cloudron'
|
||||
};
|
||||
|
||||
before(function (done) {
|
||||
@@ -2218,7 +2219,7 @@ describe('database', function () {
|
||||
});
|
||||
|
||||
it('cannot add non-existing domain', function (done) {
|
||||
maildb.add(MAIL_DOMAIN_0.domain + 'nope', function (error) {
|
||||
maildb.add(MAIL_DOMAIN_0.domain + 'nope', {}, function (error) {
|
||||
expect(error).to.be.ok();
|
||||
expect(error.reason).to.be(DatabaseError.NOT_FOUND);
|
||||
|
||||
@@ -2227,7 +2228,7 @@ describe('database', function () {
|
||||
});
|
||||
|
||||
it('can add domain', function (done) {
|
||||
maildb.add(MAIL_DOMAIN_0.domain, function (error) {
|
||||
maildb.add(MAIL_DOMAIN_0.domain, {}, function (error) {
|
||||
expect(error).to.equal(null);
|
||||
|
||||
done();
|
||||
|
||||
@@ -100,7 +100,7 @@ function setup(done) {
|
||||
database._clear.bind(null),
|
||||
ldapServer.start.bind(null),
|
||||
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE),
|
||||
maildb.add.bind(null, DOMAIN_0.domain),
|
||||
maildb.add.bind(null, DOMAIN_0.domain, {}),
|
||||
function (callback) {
|
||||
users.createOwner(USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
Reference in New Issue
Block a user