Compare commits

..

1 Commits

Author SHA1 Message Date
Girish Ramakrishnan a3ae73d48f 3.1.4 changes 2018-09-12 20:29:55 -07:00
214 changed files with 7289 additions and 13599 deletions
-29
View File
@@ -1,29 +0,0 @@
{
"env": {
"es6": true,
"node": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 2017
},
"rules": {
"indent": [
"error",
4
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
],
"no-console": "off"
}
}
+1
View File
@@ -6,3 +6,4 @@ installer/src/certs/server.key
# vim swap files
*.swp
+9
View File
@@ -0,0 +1,9 @@
{
"node": true,
"browser": true,
"unused": true,
"multistr": true,
"globalstrict": true,
"predef": [ "angular", "$" ],
"esnext": true
}
-152
View File
@@ -1376,159 +1376,7 @@
[3.1.3]
* Prevent dashboard domain from being deleted
* Add alternateDomains to app install route
* cloudflare: Fix crash when access denied
[3.1.4]
* Fix issue where support tab was redirecting
[3.2.0]
* Add DO Spaces SFO2 region
* Wildcard DNS now validates the config
* Add ACMEv2 support
* Add Wildcard Let's Encrypt provider
[3.2.1]
* Add acme2 support. This provides DNS based validation removing inbound port 80 requirement
* Add support for wildcard certificates
* Allow mailbox name to be reset to the buit-in '.app' name
* Fix permission issue when restoring a Cloudron
* Fix a crash when restoring Cloudron
* Allow alternate domains to be set in app installation REST API
* Add SFO2 region for DigitalOcean Spaces
* Show the title in port bindings instead of the long description
[3.2.2]
* Update Haraka to 2.8.20
* (mail) Fix issue where LDAP connections where not cleaned up
[3.3.0]
* Use new addons with REST APIs
* Ubuntu 18.04 LTS support
* Custom env vars can be set per application
* Add a button to renew certs
* Add better support for private builds
* cloudflare: Fix crash when using bad email
* cloudflare: HTTP proxying works now
* add new exoscale-sos regions
* Add UI to toggle dynamic DNS
* Add support for hyphenated subdomains
[3.3.1]
* Use new addons with REST APIs
* Ubuntu 18.04 LTS support
* Custom env vars can be set per application
* Add a button to renew certs
* Add better support for private builds
* cloudflare: Fix crash when using bad email
* cloudflare: HTTP proxying works now
* add new exoscale-sos regions
* Add UI to toggle dynamic DNS
* Add support for hyphenated subdomains
[3.3.2]
* Use new addons with REST APIs
* Ubuntu 18.04 LTS support
* Custom env vars can be set per application
* Add a button to renew certs
* Add better support for private builds
* cloudflare: Fix crash when using bad email
* cloudflare: HTTP proxying works now
* add new exoscale-sos regions
* Add UI to toggle dynamic DNS
* Add support for hyphenated subdomains
* Add domain, mail events to eventlog
[3.3.3]
* Use new addons with REST APIs
* Ubuntu 18.04 LTS support
* Custom env vars can be set per application
* Add a button to renew certs
* Add better support for private builds
* cloudflare: Fix crash when using bad email
* cloudflare: HTTP proxying works now
* add new exoscale-sos regions
* Add UI to toggle dynamic DNS
* Add support for hyphenated subdomains
* Add domain, mail events to eventlog
[3.3.4]
* Use new addons with REST APIs
* Ubuntu 18.04 LTS support
* Custom env vars can be set per application
* Add a button to renew certs
* Add better support for private builds
* cloudflare: Fix crash when using bad email
* cloudflare: HTTP proxying works now
* add new exoscale-sos regions
* Add UI to toggle dynamic DNS
* Add support for hyphenated subdomains
* Add domain, mail events to eventlog
[3.4.0]
* Improve error page
* Add system view to manage addons and view their status
* Fix iconset regression for account and Cloudron name edits
* Add server reboot button and warn if reboot is required for security updates
* Backup and update tasks are now cancelable
* Move graphite away from port 3000 (reserved by ESXi)
* Flexible mailbox management
* Automatic updates can be toggled per app
[3.4.1]
* Improve error page
* Add system view to manage addons and view their status
* Fix iconset regression for account and Cloudron name edits
* Add server reboot button and warn if reboot is required for security updates
* Backup and update tasks are now cancelable
* Move graphite away from port 3000 (reserved by ESXi)
* Flexible mailbox management
* Automatic updates can be toggled per app
[3.4.2]
* Improve error page
* Add system view to manage addons and view their status
* Fix iconset regression for account and Cloudron name edits
* Add server reboot button and warn if reboot is required for security updates
* Backup and update tasks are now cancelable
* Move graphite away from port 3000 (reserved by ESXi)
* Flexible mailbox management
* Automatic updates can be toggled per app
[3.4.3]
* Improve error page
* Add system view to manage addons and view their status
* Fix iconset regression for account and Cloudron name edits
* Add server reboot button and warn if reboot is required for security updates
* Backup and update tasks are now cancelable
* Move graphite away from port 3000 (reserved by ESXi)
* Flexible mailbox management
* Automatic updates can be toggled per app
* Fix issue where OOM mails are sent out without a rate limit
[3.5.0]
* Add UI to switch dashboard domain
* Fix remote support button to not remove misparsed ssh keys
* cloudflare: preseve domain proxying status
* Fix issue where oom killer might kill the box code or the updater
* Add contabo and netcup as supported providers
* Allow full logs to be downloaded
* Update Haraka to 2.8.22
* Log events in the mail container
* Fix issue where SpamAssassin and SPF checks were run for outbound email
* Improve various eventlog messages
* Track dyndns change events
* Add new S3 regions - Paris/Stockholm/Osaka
* Retry errored downloads during restore
* Add user pagination UI
* Add namecheap as supported DNS provider
[3.5.1]
* Add dashboard domain change event
* Fix issue where notification email were sent from incorrect domain
* Alert about configuration issues in the notification UI
* Switching dashboard domain now updates MX, SPF records
* Mailbox and lists UI is now always visible (but disabled) when incoming email is disabled
* Fix issue where long passwords were not accepted
* DNS and backup credential secrets are not returned in API calls anymore
* Send notification when an app that went down, came back up
+1 -1
View File
@@ -630,7 +630,7 @@ state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
box
Copyright (C) 2016-2019 Cloudron UG
Copyright (C) 2016,2017,2018 Cloudron UG
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
-1
View File
@@ -1 +0,0 @@
# release version. do not edit manually
+3 -24
View File
@@ -26,25 +26,20 @@ debconf-set-selections <<< 'mysql-server mysql-server/root_password password pas
debconf-set-selections <<< 'mysql-server mysql-server/root_password_again password password'
# this enables automatic security upgrades (https://help.ubuntu.com/community/AutomaticSecurityUpdates)
# resolvconf is needed for unbound to work property after disabling systemd-resolved in 18.04
ubuntu_version=$(lsb_release -rs)
gpg_package=$([[ "${ubuntu_version}" == "16.04" ]] && echo "gnupg" || echo "gpg")
apt-get -y install \
acl \
awscli \
build-essential \
cron \
curl \
dmsetup \
$gpg_package \
iptables \
libpython2.7 \
logrotate \
mysql-server-5.7 \
nginx-full \
openssh-server \
pwgen \
resolvconf \
sudo \
rcconf \
swaks \
unattended-upgrades \
unbound \
@@ -92,12 +87,11 @@ if [ ! -f "${arg_infraversionpath}/infra_version.js" ]; then
exit 1
fi
images=$(node -e "var i = require('${arg_infraversionpath}/infra_version.js'); console.log(i.baseImages.map(function (x) { return x.tag; }).join(' '), Object.keys(i.images).map(function (x) { return i.images[x].tag; }).join(' '));")
images=$(node -e "var i = require('${arg_infraversionpath}/infra_version.js'); console.log(i.baseImages.join(' '), Object.keys(i.images).map(function (x) { return i.images[x].tag; }).join(' '));")
echo -e "\tPulling docker images: ${images}"
for image in ${images}; do
docker pull "${image}"
docker pull "${image%@sha256:*}" # this will tag the image for readability
done
echo "==> Install collectd"
@@ -107,11 +101,6 @@ if ! apt-get install -y collectd collectd-utils; then
sed -e 's/^FQDNLookup true/FQDNLookup false/' -i /etc/collectd/collectd.conf
fi
echo "==> Configuring host"
sed -e 's/^#NTP=/NTP=0.ubuntu.pool.ntp.org 1.ubuntu.pool.ntp.org 2.ubuntu.pool.ntp.org 3.ubuntu.pool.ntp.org/' -i /etc/systemd/timesyncd.conf
timedatectl set-ntp 1
timedatectl set-timezone UTC
# Disable bind for good measure (on online.net, kimsufi servers these are pre-installed and conflicts with unbound)
systemctl stop bind9 || true
systemctl disable bind9 || true
@@ -124,13 +113,3 @@ systemctl disable dnsmasq || true
systemctl stop postfix || true
systemctl disable postfix || true
# on ubuntu 18.04, this is the default. this requires resolvconf for DNS to work further after the disable
systemctl stop systemd-resolved || true
systemctl disable systemd-resolved || true
# ubuntu's default config for unbound does not work if ipv6 is disabled. this config is overwritten in start.sh
# we need unbound to work as this is required for installer.sh to do any DNS requests
ip6=$([[ -s /proc/net/if_inet6 ]] && echo "yes" || echo "no")
echo -e "server:\n\tinterface: 127.0.0.1\n\tdo-ip6: ${ip6}" > /etc/unbound/unbound.conf.d/cloudron-network.conf
systemctl restart unbound
+9 -15
View File
@@ -2,18 +2,15 @@
'use strict';
// prefix all output with a timestamp
// debug() already prefixes and uses process.stderr NOT console.*
['log', 'info', 'warn', 'debug', 'error'].forEach(function (log) {
var orig = console[log];
console[log] = function () {
orig.apply(console, [new Date().toISOString()].concat(Array.prototype.slice.call(arguments)));
};
});
require('supererror')({ splatchError: true });
let async = require('async'),
// remove timestamp from debug() based output
require('debug').formatArgs = function formatArgs(args) {
args[0] = this.namespace + ' ' + args[0];
};
var appHealthMonitor = require('./src/apphealthmonitor.js'),
async = require('async'),
config = require('./src/config.js'),
ldap = require('./src/ldap.js'),
dockerProxy = require('./src/dockerproxy.js'),
@@ -39,7 +36,8 @@ console.log();
async.series([
server.start,
ldap.start,
dockerProxy.start
dockerProxy.start,
appHealthMonitor.start,
], function (error) {
if (error) {
console.error('Error starting server', error);
@@ -51,8 +49,6 @@ async.series([
var NOOP_CALLBACK = function () { };
process.on('SIGINT', function () {
console.log('Received SIGINT. Shutting down.');
server.stop(NOOP_CALLBACK);
ldap.stop(NOOP_CALLBACK);
dockerProxy.stop(NOOP_CALLBACK);
@@ -60,8 +56,6 @@ process.on('SIGINT', function () {
});
process.on('SIGTERM', function () {
console.log('Received SIGTERM. Shutting down.');
server.stop(NOOP_CALLBACK);
ldap.stop(NOOP_CALLBACK);
dockerProxy.stop(NOOP_CALLBACK);
+5 -6
View File
@@ -6,18 +6,17 @@ var database = require('./src/database.js');
var sendFailureLogs = require('./src/logcollector').sendFailureLogs;
// This is triggered by systemd with the crashed unit name as argument
function main() {
if (process.argv.length !== 3) return console.error('Usage: crashnotifier.js <unitName>');
if (process.argv.length !== 3) return console.error('Usage: crashnotifier.js <processName>');
var unitName = process.argv[2];
console.log('Started crash notifier for', unitName);
var processName = process.argv[2];
console.log('Started crash notifier for', processName);
// eventlog api needs the db
// mailer needs the db
database.initialize(function (error) {
if (error) return console.error('Cannot connect to database. Unable to send crash log.', error);
sendFailureLogs(unitName);
sendFailureLogs(processName, { unit: processName });
});
}
@@ -1,7 +1,7 @@
'use strict';
exports.up = function(db, callback) {
var cmd = "CREATE TABLE userGroups(" +
var cmd = "CREATE TABLE groups(" +
"id VARCHAR(128) NOT NULL UNIQUE," +
"name VARCHAR(128) NOT NULL UNIQUE," +
"PRIMARY KEY(id))";
@@ -13,7 +13,7 @@ exports.up = function(db, callback) {
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE userGroups', function (error) {
db.runSql('DROP TABLE groups', function (error) {
if (error) console.error(error);
callback(error);
});
@@ -4,7 +4,7 @@ exports.up = function(db, callback) {
var cmd = "CREATE TABLE IF NOT EXISTS groupMembers(" +
"groupId VARCHAR(128) NOT NULL," +
"userId VARCHAR(128) NOT NULL," +
"FOREIGN KEY(groupId) REFERENCES userGroups(id)," +
"FOREIGN KEY(groupId) REFERENCES groups(id)," +
"FOREIGN KEY(userId) REFERENCES users(id));";
db.runSql(cmd, function (error) {
@@ -7,7 +7,7 @@ var ADMIN_GROUP_ID = 'admin'; // see constants.js
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'INSERT INTO userGroups (id, name) VALUES (?, ?)', [ ADMIN_GROUP_ID, 'admin' ]),
db.runSql.bind(db, 'INSERT INTO groups (id, name) VALUES (?, ?)', [ ADMIN_GROUP_ID, 'admin' ]),
function migrateAdminFlag(done) {
db.all('SELECT * FROM users WHERE admin=1', function (error, results) {
if (error) return done(error);
@@ -10,7 +10,7 @@ exports.up = function(db, callback) {
function addGroupMailboxes(done) {
console.log('Importing group mailboxes');
db.all('SELECT id, name FROM userGroups', function (error, results) {
db.all('SELECT id, name FROM groups', function (error, results) {
if (error) return done(error);
async.eachSeries(results, function (g, next) {
@@ -16,7 +16,7 @@ exports.up = function(db, callback) {
db.runSql.bind(db, 'ALTER TABLE clients CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE eventlog CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE groupMembers CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE userGroups CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE groups CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE mailboxes CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE migrations CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE settings CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
+1 -1
View File
@@ -29,7 +29,7 @@ exports.up = function(db, callback) {
// this will be finally created once we have a domain when we create the owner in user.js
const ADMIN_GROUP_ID = 'admin'; // see constants.js
db.runSql('DELETE FROM userGroups WHERE id = ?', [ ADMIN_GROUP_ID ], function (error) {
db.runSql('DELETE FROM groups WHERE id = ?', [ ADMIN_GROUP_ID ], function (error) {
if (error) return done(error);
db.runSql('DELETE FROM mailboxes WHERE ownerId = ?', [ ADMIN_GROUP_ID ], done);
@@ -19,8 +19,8 @@ exports.up = function(db, callback) {
},
function getGroups(done) {
db.all('SELECT id, name, GROUP_CONCAT(groupMembers.userId) AS userIds ' +
' FROM userGroups LEFT OUTER JOIN groupMembers ON userGroups.id = groupMembers.groupId ' +
' GROUP BY userGroups.id', [ ], function (error, results) {
' FROM groups LEFT OUTER JOIN groupMembers ON groups.id = groupMembers.groupId ' +
' GROUP BY groups.id', [ ], function (error, results) {
if (error) return done(error);
results.forEach(function (result) {
@@ -4,7 +4,7 @@ var async = require('async');
exports.up = function(db, callback) {
db.all('SELECT * from apps', [ ], function (error, results) {
if (error) return callback(error);
if (error) return done(error);
var queries = [
db.runSql.bind(db, 'START TRANSACTION;')
+1 -1
View File
@@ -18,7 +18,7 @@ exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'DELETE FROM groupMembers WHERE groupId=?', [ 'admin' ]),
db.runSql.bind(db, 'DELETE FROM userGroups WHERE id=?', [ 'admin' ])
db.runSql.bind(db, 'DELETE FROM groups WHERE id=?', [ 'admin' ])
], callback);
});
});
@@ -1,21 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
db.all('SELECT * from domains WHERE provider=?', [ 'manual' ], function (error, results) {
if (error) return callback(error);
async.eachSeries(results, function (result, iteratorDone) {
var config = JSON.parse(result.configJson || '{}');
if (!config.wildcard) return iteratorDone();
delete config.wildcard;
db.runSql('UPDATE domains SET provider=?, configJson=? WHERE domain=?', [ 'wildcard', JSON.stringify(config), result.domain ], iteratorDone);
}, callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -1,21 +0,0 @@
'use strict';
exports.up = function(db, callback) {
var cmd = 'CREATE TABLE IF NOT EXISTS appEnvVars(' +
'appId VARCHAR(128) NOT NULL,' +
'name TEXT NOT NULL,' +
'value TEXT NOT NULL,' +
'FOREIGN KEY(appId) REFERENCES apps(id)) CHARACTER SET utf8 COLLATE utf8_bin';
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE appEnvVars', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,27 +0,0 @@
'use strict';
exports.up = function(db, callback) {
var cmd = 'CREATE TABLE tasks(' +
'id int NOT NULL AUTO_INCREMENT,' +
'type VARCHAR(32) NOT NULL,' +
'argsJson TEXT,' +
'percent INTEGER DEFAULT 0,' +
'message TEXT,' +
'errorMessage TEXT,' +
'result TEXT,' +
'creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,' +
'ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,' +
'PRIMARY KEY (id))';
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE tasks', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,17 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('SELECT 1 FROM groups LIMIT 1', function (error) {
if (error) return callback(); // groups table does not exist
db.runSql('RENAME TABLE groups TO userGroups', function (error) {
if (error) console.error(error);
callback(error);
});
});
};
exports.down = function(db, callback) {
// this is a one way renaming since the previous migration steps have been already updated to match the new name
callback();
};
@@ -1,17 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE apps MODIFY creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'),
db.runSql.bind(db, 'ALTER TABLE apps MODIFY updateTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'),
db.runSql.bind(db, 'ALTER TABLE eventlog MODIFY creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'),
db.runSql.bind(db, 'ALTER TABLE backups MODIFY creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'),
db.runSql.bind(db, 'ALTER TABLE mailboxes MODIFY creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'),
], callback);
};
exports.down = function(db, callback) {
callback();
};
@@ -1,28 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE apps ADD COLUMN mailboxName VARCHAR(128)'),
db.runSql.bind(db, 'START TRANSACTION;'),
function migrateMailboxNames(done) {
db.all('SELECT * FROM mailboxes', function (error, mailboxes) {
if (error) return done(error);
async.eachSeries(mailboxes, function (mailbox, iteratorDone) {
if (mailbox.ownerType !== 'app') return iteratorDone();
db.runSql('UPDATE apps SET mailboxName = ? WHERE id = ?', [ mailbox.name, mailbox.ownerId ], iteratorDone);
}, done);
});
},
db.runSql.bind(db, 'COMMIT')
], callback);
};
exports.down = function(db, callback) {
callback();
};
@@ -1,28 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
function migrateMailboxNames(done) {
db.all('SELECT * FROM mailboxes', function (error, mailboxes) {
if (error) return done(error);
async.eachSeries(mailboxes, function (mailbox, iteratorDone) {
if (mailbox.ownerType !== 'app') return iteratorDone();
db.runSql('DELETE FROM mailboxes WHERE name = ?', [ mailbox.name ], iteratorDone);
}, done);
});
},
db.runSql.bind(db, 'COMMIT'),
db.runSql.bind(db, 'ALTER TABLE mailboxes DROP COLUMN ownerType')
], callback);
};
exports.down = function(db, callback) {
callback();
};
@@ -1,16 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN enableAutomaticUpdate BOOLEAN DEFAULT 1', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN enableAutomaticUpdate', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,27 +0,0 @@
'use strict';
exports.up = function(db, callback) {
var cmd = 'CREATE TABLE notifications(' +
'id int NOT NULL AUTO_INCREMENT,' +
'userId VARCHAR(128) NOT NULL,' +
'eventId VARCHAR(128) NOT NULL,' +
'title VARCHAR(512) NOT NULL,' +
'message TEXT,' +
'action VARCHAR(512) NOT NULL,' +
'acknowledged BOOLEAN DEFAULT false,' +
'creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,' +
'FOREIGN KEY(eventId) REFERENCES eventlog(id),' +
'PRIMARY KEY (id)) CHARACTER SET utf8 COLLATE utf8_bin';
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE notifications', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,16 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE tasks CHANGE result resultJson TEXT', [], function (error) {
if (error) console.error(error);
db.runSql('DELETE FROM tasks', callback); // empty tasks table since we have bad results format
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE tasks CHANGE resultJson result TEXT', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,15 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN dataDir VARCHAR(256) UNIQUE', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN dataDir', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,17 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE domains ADD COLUMN locked BOOLEAN DEFAULT 0', function (error) {
if (error) return callback(error);
callback();
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE domains DROP COLUMN locked', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,22 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
// WARNING in the future always give constraints proper names to not rely on automatic ones
db.runSql.bind(db, 'ALTER TABLE notifications DROP FOREIGN KEY notifications_ibfk_1'),
db.runSql.bind(db, 'ALTER TABLE notifications MODIFY eventId VARCHAR(128)'),
db.runSql.bind(db, 'COMMIT')
], callback);
};
exports.down = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'ALTER TABLE notifications MODIFY eventId VARCHAR(128) NOT NULL'),
db.runSql.bind(db, 'ALTER TABLE notifications ADD FOREIGN KEY(eventId) REFERENCES eventlog(id)'),
db.runSql.bind(db, 'COMMIT')
], callback);
};
@@ -1,23 +0,0 @@
'use strict';
let async = require('async');
exports.up = function(db, callback) {
db.runSql('SELECT * FROM domains', function (error, domains) {
if (error) return callback(error);
async.eachSeries(domains, function (domain, iteratorCallback) {
if (domain.provider !== 'namecheap') return iteratorCallback();
let config = JSON.parse(domain.configJson);
config.token = config.apiKey;
delete config.apiKey;
db.runSql('UPDATE domains SET configJson = ? WHERE domain = ?', [ JSON.stringify(config), domain.domain ], iteratorCallback);
}, callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -1,17 +0,0 @@
'use strict';
exports.up = function(db, callback) {
var cmd = 'ALTER TABLE apps ADD COLUMN healthTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP';
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN healthTime', function (error) {
if (error) console.error(error);
callback(error);
});
};
+11 -46
View File
@@ -29,7 +29,7 @@ CREATE TABLE IF NOT EXISTS users(
PRIMARY KEY(id));
CREATE TABLE IF NOT EXISTS userGroups(
CREATE TABLE IF NOT EXISTS groups(
id VARCHAR(128) NOT NULL UNIQUE,
name VARCHAR(254) NOT NULL UNIQUE,
PRIMARY KEY(id));
@@ -37,7 +37,7 @@ CREATE TABLE IF NOT EXISTS userGroups(
CREATE TABLE IF NOT EXISTS groupMembers(
groupId VARCHAR(128) NOT NULL,
userId VARCHAR(128) NOT NULL,
FOREIGN KEY(groupId) REFERENCES userGroups(id),
FOREIGN KEY(groupId) REFERENCES groups(id),
FOREIGN KEY(userId) REFERENCES users(id));
CREATE TABLE IF NOT EXISTS tokens(
@@ -65,15 +65,14 @@ CREATE TABLE IF NOT EXISTS apps(
installationProgress TEXT,
runState VARCHAR(512),
health VARCHAR(128),
healthTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the app last responded
containerId VARCHAR(128),
manifestJson TEXT,
httpPort INTEGER, // this is the nginx proxy port and not manifest.httpPort
location VARCHAR(128) NOT NULL,
domain VARCHAR(128) NOT NULL,
accessRestrictionJson TEXT, // { users: [ ], groups: [ ] }
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the app was installed
updateTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the last app update was done
creationTime TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the app was installed
updateTime TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the last app update was done
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, // when this db record was updated (useful for UI caching)
memoryLimit BIGINT DEFAULT 0,
xFrameOptions VARCHAR(512),
@@ -81,8 +80,6 @@ CREATE TABLE IF NOT EXISTS apps(
debugModeJson TEXT, // options for development mode
robotsTxt TEXT,
enableBackup BOOLEAN DEFAULT 1, // misnomer: controls automatic daily backups
enableAutomaticUpdate BOOLEAN DEFAULT 1,
mailboxName VARCHAR(128), // mailbox of this app
// the following fields do not belong here, they can be removed when we use a queue for apptask
restoreConfigJson VARCHAR(256), // used to pass backupId to restore from to apptask
@@ -121,15 +118,9 @@ CREATE TABLE IF NOT EXISTS appAddonConfigs(
value VARCHAR(512) NOT NULL,
FOREIGN KEY(appId) REFERENCES apps(id));
CREATE TABLE IF NOT EXISTS appEnvVars(
appId VARCHAR(128) NOT NULL,
name TEXT NOT NULL,
value TEXT NOT NULL,
FOREIGN KEY(appId) REFERENCES apps(id));
CREATE TABLE IF NOT EXISTS backups(
id VARCHAR(128) NOT NULL,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
creationTime TIMESTAMP,
version VARCHAR(128) NOT NULL, /* app version or box version */
type VARCHAR(16) NOT NULL, /* 'box' or 'app' */
dependsOn TEXT, /* comma separate list of objects this backup depends on */
@@ -144,7 +135,7 @@ CREATE TABLE IF NOT EXISTS eventlog(
action VARCHAR(128) NOT NULL,
source TEXT, /* { userId, username, ip }. userId can be null for cron,sysadmin */
data TEXT, /* free flowing json based on action */
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
createdAt TIMESTAMP(2) NOT NULL,
PRIMARY KEY (id));
@@ -176,17 +167,15 @@ CREATE TABLE IF NOT EXISTS mail(
/* Future fields:
* accessRestriction - to determine who can access it. So this has foreign keys
* quota - per mailbox quota
NOTE: this table exists only real mailboxes. And has unique constraint to handle
conflict with aliases and mailbox names
*/
CREATE TABLE IF NOT EXISTS mailboxes(
name VARCHAR(128) NOT NULL,
type VARCHAR(16) NOT NULL, /* 'mailbox', 'alias', 'list' */
ownerId VARCHAR(128) NOT NULL, /* user id */
ownerId VARCHAR(128) NOT NULL, /* app id or user id or group id */
ownerType VARCHAR(16) NOT NULL, /* 'app' or 'user' or 'group' */
aliasTarget VARCHAR(128), /* the target name type is an alias */
membersJson TEXT, /* members of a group */
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
creationTime TIMESTAMP,
domain VARCHAR(128),
FOREIGN KEY(domain) REFERENCES mail(domain),
@@ -200,30 +189,6 @@ CREATE TABLE IF NOT EXISTS subdomains(
FOREIGN KEY(domain) REFERENCES domains(domain),
FOREIGN KEY(appId) REFERENCES apps(id),
UNIQUE (subdomain, domain));
UNIQUE (subdomain, domain))
CREATE TABLE IF NOT EXISTS tasks(
id int NOT NULL AUTO_INCREMENT,
type VARCHAR(32) NOT NULL,
percent INTEGER DEFAULT 0,
message TEXT,
errorMessage TEXT,
result TEXT,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id));
CREATE TABLE IF NOT EXISTS notifications(
id int NOT NULL AUTO_INCREMENT,
userId VARCHAR(128) NOT NULL,
eventId VARCHAR(128),
title VARCHAR(512) NOT NULL,
message TEXT,
action VARCHAR(512) NOT NULL,
acknowledged BOOLEAN DEFAULT false,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);
CHARACTER SET utf8 COLLATE utf8_bin;
CHARACTER SET utf8 COLLATE utf8_bin;
+1485 -1928
View File
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -20,14 +20,14 @@
"async": "^2.6.1",
"aws-sdk": "^2.253.1",
"body-parser": "^1.18.3",
"cloudron-manifestformat": "^2.14.2",
"cloudron-manifestformat": "^2.13.1",
"connect": "^3.6.6",
"connect-ensure-login": "^0.1.1",
"connect-lastmile": "^1.0.2",
"connect-timeout": "^1.9.0",
"cookie-parser": "^1.3.5",
"cookie-session": "^1.3.2",
"cron": "^1.5.1",
"cron": "^1.3.0",
"csurf": "^1.6.6",
"db-migrate": "^0.11.1",
"db-migrate-mysql": "^1.1.10",
@@ -45,7 +45,6 @@
"morgan": "^1.9.0",
"multiparty": "^4.1.4",
"mysql": "^2.15.0",
"namecheap": "github:joshuakarjala/node-namecheap#464a952",
"nodemailer": "^4.6.5",
"nodemailer-smtp-transport": "^2.7.4",
"oauth2orize": "^1.11.0",
@@ -88,11 +87,12 @@
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
"nock": "^9.0.14",
"node-sass": "^4.6.1",
"recursive-readdir": "^2.2.2",
"sinon": "^7.2.2"
"recursive-readdir": "^2.2.2"
},
"scripts": {
"test": "src/test/setupTest && BOX_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- --no-timeouts --exit -R spec ./src/test ./src/routes/test/[^a]*js",
"migrate_local": "DATABASE_URL=mysql://root:@localhost/box node_modules/.bin/db-migrate up",
"migrate_test": "BOX_ENV=test DATABASE_URL=mysql://root:@localhost/boxtest node_modules/.bin/db-migrate up",
"test": "npm run migrate_test && src/test/setupTest && BOX_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- --exit -R spec ./src/test ./src/routes/test",
"postmerge": "/bin/true",
"precommit": "/bin/true",
"prepush": "npm test",
+122
View File
@@ -0,0 +1,122 @@
#!/bin/bash
set -eu -o pipefail
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
function get_status() {
key="$1"
if status=$($curl -q -f "http://localhost:3000/api/v1/cloudron/status" 2>/dev/null); then
currentValue=$(echo "${status}" | python3 -c 'import sys, json; print(json.dumps(json.load(sys.stdin)[sys.argv[1]]))' "${key}")
echo "${currentValue}"
return 0
fi
return 1
}
function wait_for_status() {
key="$1"
expectedValue="$2"
echo "wait_for_status: $key to be $expectedValue"
while true; do
if currentValue=$(get_status "${key}"); then
echo "wait_for_status: $key is current: $currentValue expecting: $expectedValue"
if [[ "${currentValue}" == $expectedValue ]]; then
break
fi
fi
sleep 3
done
}
domain=""
domainProvider=""
domainConfigJson="{}"
domainTlsProvider="letsencrypt-prod"
adminUsername="superadmin"
adminPassword="Secret123#"
adminEmail="admin@server.local"
appstoreUserId=""
appstoreToken=""
backupDir="/var/backups"
args=$(getopt -o "" -l "domain:,domain-provider:,domain-tls-provider:,admin-username:,admin-password:,admin-email:,appstore-user:,appstore-token:,backup-dir:" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--domain) domain="$2"; shift 2;;
--domain-provider) domainProvider="$2"; shift 2;;
--domain-tls-provider) domainTlsProvider="$2"; shift 2;;
--admin-username) adminUsername="$2"; shift 2;;
--admin-password) adminPassword="$2"; shift 2;;
--admin-email) adminEmail="$2"; shift 2;;
--appstore-user) appstoreUser="$2"; shift 2;;
--appstore-token) appstoreToken="$2"; shift 2;;
--backup-dir) backupDir="$2"; shift 2;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
done
echo "=> Waiting for cloudron to be ready"
wait_for_status "version" '*'
if [[ $(get_status "webadminStatus") != *'"tls": true'* ]]; then
echo "=> Domain setup"
dnsSetupData=$(printf '{ "domain": "%s", "adminFqdn": "%s", "provider": "%s", "config": %s, "tlsConfig": { "provider": "%s" } }' "${domain}" "my.${domain}" "${domainProvider}" "$domainConfigJson" "${domainTlsProvider}")
if ! $curl -X POST -H "Content-Type: application/json" -d "${dnsSetupData}" http://localhost:3000/api/v1/cloudron/dns_setup; then
echo "DNS Setup Failed"
exit 1
fi
wait_for_status "webadminStatus" '*"tls": true*'
else
echo "=> Skipping Domain setup"
fi
activationData=$(printf '{"username": "%s", "password":"%s", "email": "%s" }' "${adminUsername}" "${adminPassword}" "${adminEmail}")
if [[ $(get_status "activated") == "false" ]]; then
echo "=> Activating"
if ! activationResult=$($curl -X POST -H "Content-Type: application/json" -d "${activationData}" http://localhost:3000/api/v1/cloudron/activate); then
echo "Failed to activate with ${activationData}: ${activationResult}"
exit 1
fi
wait_for_status "activated" "true"
else
echo "=> Skipping Activation"
fi
echo "=> Getting token"
if ! activationResult=$($curl -X POST -H "Content-Type: application/json" -d "${activationData}" http://localhost:3000/api/v1/developer/login); then
echo "Failed to login with ${activationData}: ${activationResult}"
exit 1
fi
accessToken=$(echo "${activationResult}" | python3 -c 'import sys, json; print(json.load(sys.stdin)[sys.argv[1]])' "accessToken")
echo "=> Setting up App Store account with accessToken ${accessToken}"
appstoreData=$(printf '{"userId":"%s", "token":"%s" }' "${appstoreUser}" "${appstoreToken}")
if ! appstoreResult=$($curl -X POST -H "Content-Type: application/json" -d "${appstoreData}" "http://localhost:3000/api/v1/settings/appstore_config?access_token=${accessToken}"); then
echo "Failed to setup Appstore account with ${appstoreData}: ${appstoreResult}"
exit 1
fi
echo "=> Setting up Backup Directory with accessToken ${accessToken}"
backupData=$(printf '{"provider":"filesystem", "key":"", "backupFolder":"%s", "retentionSecs": 864000, "format": "tgz", "externalDisk": true}' "${backupDir}")
chown -R yellowtent:yellowtent "${backupDir}"
if ! backupResult=$($curl -X POST -H "Content-Type: application/json" -d "${backupData}" "http://localhost:3000/api/v1/settings/backup_config?access_token=${accessToken}"); then
echo "Failed to setup backup configuration with ${backupDir}: ${backupResult}"
exit 1
fi
echo "=> Done!"
-106
View File
@@ -1,106 +0,0 @@
#!/bin/bash
set -eu -o pipefail
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400 --http1.1"
ip=""
dns_config=""
tls_cert_file=""
tls_key_file=""
license_file=""
backup_config=""
args=$(getopt -o "" -l "ip:,backup-config:,license:,dns-config:,tls-cert:,tls-key:" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--ip) ip="$2"; shift 2;;
--dns-config) dns_config="$2"; shift 2;;
--tls-cert) tls_cert_file="$2"; shift 2;;
--tls-key) tls_key_file="$2"; shift 2;;
--license) license_file="$2"; shift 2;;
--backup-config) backup_config="$2"; shift 2;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
done
# validate arguments in the absence of data
if [[ -z "${ip}" ]]; then
echo "--ip is required"
exit 1
fi
if [[ -z "${dns_config}" ]]; then
echo "--dns-config is required"
exit 1
fi
if [[ ! -f "${license_file}" ]]; then
echo "--license must be a valid license file"
exit 1
fi
function get_status() {
key="$1"
if status=$($curl -q -f -k "https://${ip}/api/v1/cloudron/status" 2>/dev/null); then
currentValue=$(echo "${status}" | python3 -c 'import sys, json; print(json.dumps(json.load(sys.stdin)[sys.argv[1]]))' "${key}")
echo "${currentValue}"
return 0
fi
return 1
}
function wait_for_status() {
key="$1"
expectedValue="$2"
echo "wait_for_status: $key to be $expectedValue"
while true; do
if currentValue=$(get_status "${key}"); then
echo "wait_for_status: $key is current: $currentValue expecting: $expectedValue"
if [[ "${currentValue}" == $expectedValue ]]; then
break
fi
fi
sleep 3
done
}
echo "=> Waiting for cloudron to be ready"
wait_for_status "version" '*'
domain=$(echo "${dns_config}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["domain"])')
echo "Provisioning Cloudron ${domain}"
if [[ -n "${tls_cert_file}" && -n "${tls_key_file}" ]]; then
tls_cert=$(cat "${tls_cert_file}" | awk '{printf "%s\\n", $0}')
tls_key=$(cat "${tls_key_file}" | awk '{printf "%s\\n", $0}')
fallback_cert=$(printf '{ "cert": "%s", "key": "%s", "provider": "fallback", "restricted": true }' "${tls_cert}" "${tls_key}")
else
fallback_cert=None
fi
tls_config='{ "provider": "fallback" }'
dns_config=$(echo "${dns_config}" | python3 -c "import json,sys;obj=json.load(sys.stdin);obj.update(tlsConfig=${tls_config});obj.update(fallbackCertficate=${fallback_cert});print(json.dumps(obj))")
license=$(cat "${license_file}")
if [[ -z "${backup_config:-}" ]]; then
backup_config='{ "provider": "filesystem", "backupFolder": "/var/backups", "format": "tgz" }'
fi
setupData=$(printf '{ "dnsConfig": %s, "autoconf": { "appstoreConfig": %s, "backupConfig": %s } }' "${dns_config}" "${license}" "${backup_config}")
if ! setupResult=$($curl -kq -X POST -H "Content-Type: application/json" -d "${setupData}" https://${ip}/api/v1/cloudron/setup); then
echo "Failed to setup with ${setupData} ${setupResult}"
exit 1
fi
wait_for_status "webadminStatus" '*"tls": true*'
echo "Cloudron is ready at https://my-${domain}"
+36 -71
View File
@@ -4,6 +4,7 @@ set -eu -o pipefail
# change this to a hash when we make a upgrade release
readonly LOG_FILE="/var/log/cloudron-setup.log"
readonly DATA_FILE="/root/cloudron-install-data.json"
readonly MINIMUM_DISK_SIZE_GB="18" # this is the size of "/" and required to fit in docker images 18 is a safe bet for different reporting on 20GB min
readonly MINIMUM_MEMORY="974" # this is mostly reported for 1GB main memory (DO 992, EC2 990, Linode 989, Serverdiscounter.com 974)
@@ -47,11 +48,14 @@ edition=""
requestedVersion=""
apiServerOrigin="https://api.cloudron.io"
webServerOrigin="https://cloudron.io"
prerelease="false"
sourceTarballUrl=""
rebootServer="true"
baseDataDir=""
args=$(getopt -o "" -l "help,skip-baseimage-init,provider:,version:,env:,edition:,skip-reboot" -n "$0" -- "$@")
echo "Running cloudron-setup with args : $@" > "${LOG_FILE}"
args=$(getopt -o "" -l "help,skip-baseimage-init,data-dir:,provider:,version:,env:,prerelease,edition:,skip-reboot" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
@@ -64,13 +68,17 @@ while true; do
if [[ "$2" == "dev" ]]; then
apiServerOrigin="https://api.dev.cloudron.io"
webServerOrigin="https://dev.cloudron.io"
prerelease="true"
elif [[ "$2" == "staging" ]]; then
apiServerOrigin="https://api.staging.cloudron.io"
webServerOrigin="https://staging.cloudron.io"
prerelease="true"
fi
shift 2;;
--skip-baseimage-init) initBaseImage="false"; shift;;
--skip-reboot) rebootServer="false"; shift;;
--prerelease) prerelease="true"; shift;;
--data-dir) baseDataDir=$(realpath "$2"); shift 2;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
@@ -83,27 +91,21 @@ if [[ ${EUID} -ne 0 ]]; then
fi
# Only --help works with mismatched ubuntu
ubuntu_version=$(lsb_release -rs)
if [[ "${ubuntu_version}" != "16.04" && "${ubuntu_version}" != "18.04" ]]; then
echo "Cloudron requires Ubuntu 16.04 or 18.04" > /dev/stderr
if [[ $(lsb_release -rs) != "16.04" ]]; then
echo "Cloudron requires Ubuntu 16.04" > /dev/stderr
exit 1
fi
# Can only write after we have confirmed script has root access
echo "Running cloudron-setup with args : $@" > "${LOG_FILE}"
# validate arguments in the absence of data
if [[ -z "${provider}" ]]; then
echo "--provider is required (azure, digitalocean, ec2, exoscale, gce, hetzner, lightsail, linode, netcup, ovh, rosehosting, scaleway, vultr or generic)"
echo "--provider is required (azure, digitalocean, ec2, exoscale, gce, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic)"
exit 1
elif [[ \
"${provider}" != "ami" && \
"${provider}" != "azure" && \
"${provider}" != "caas" && \
"${provider}" != "cloudscale" && \
"${provider}" != "contabo" && \
"${provider}" != "digitalocean" && \
"${provider}" != "digitalocean-mp" && \
"${provider}" != "ec2" && \
"${provider}" != "exoscale" && \
"${provider}" != "galaxygate" && \
@@ -112,15 +114,13 @@ elif [[ \
"${provider}" != "hetzner" && \
"${provider}" != "lightsail" && \
"${provider}" != "linode" && \
"${provider}" != "netcup" && \
"${provider}" != "netcup-image" && \
"${provider}" != "ovh" && \
"${provider}" != "rosehosting" && \
"${provider}" != "scaleway" && \
"${provider}" != "vultr" && \
"${provider}" != "generic" \
]]; then
echo "--provider must be one of: azure, cloudscale.ch, contabo, digitalocean, ec2, exoscale, galaxygate, gce, hetzner, lightsail, linode, netcup, ovh, rosehosting, scaleway, vultr or generic"
echo "--provider must be one of: azure, cloudscale.ch, digitalocean, ec2, exoscale, galaxygate, gce, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic"
exit 1
fi
@@ -141,18 +141,6 @@ echo " Join us at https://forum.cloudron.io for any questions."
echo ""
if [[ "${initBaseImage}" == "true" ]]; then
echo "=> Installing software-properties-common"
if ! apt-get install -y software-properties-common &>> "${LOG_FILE}"; then
echo "Could not install software-properties-common (for add-apt-repository below). See ${LOG_FILE}"
exit 1
fi
echo "=> Ensure required apt sources"
if ! add-apt-repository universe &>> "${LOG_FILE}"; then
echo "Could not add required apt sources (for nginx-full). See ${LOG_FILE}"
exit 1
fi
echo "=> Updating apt and installing script dependencies"
if ! apt-get update &>> "${LOG_FILE}"; then
echo "Could not update package repositories. See ${LOG_FILE}"
@@ -166,7 +154,7 @@ if [[ "${initBaseImage}" == "true" ]]; then
fi
echo "=> Checking version"
if ! releaseJson=$($curl -s "${apiServerOrigin}/api/v1/releases?boxVersion=${requestedVersion}"); then
if ! releaseJson=$($curl -s "${apiServerOrigin}/api/v1/releases?prerelease=${prerelease}&boxVersion=${requestedVersion}"); then
echo "Failed to get release information"
exit 1
fi
@@ -182,6 +170,19 @@ if ! sourceTarballUrl=$(echo "${releaseJson}" | python3 -c 'import json,sys;obj=
exit 1
fi
# Build data
# from 1.9, we use autoprovision.json
data=$(cat <<EOF
{
"provider": "${provider}",
"edition": "${edition}",
"apiServerOrigin": "${apiServerOrigin}",
"webServerOrigin": "${webServerOrigin}",
"version": "${version}"
}
EOF
)
echo "=> Downloading version ${version} ..."
box_src_tmp_dir=$(mktemp -dt box-src-XXXXXX)
@@ -199,44 +200,13 @@ if [[ "${initBaseImage}" == "true" ]]; then
echo ""
fi
# NOTE: this install script only supports 3.x and above
echo "=> Installing version ${version} (this takes some time) ..."
if [[ "${version}" =~ 3\.[0-2]+\.[0-9]+ ]]; then
readonly DATA_FILE="/root/cloudron-install-data.json"
data=$(cat <<EOF
{
"provider": "${provider}",
"edition": "${edition}",
"apiServerOrigin": "${apiServerOrigin}",
"webServerOrigin": "${webServerOrigin}",
"version": "${version}"
}
EOF
)
echo "${data}" > "${DATA_FILE}"
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" --data-file "${DATA_FILE}" &>> "${LOG_FILE}"; then
echo "Failed to install cloudron. See ${LOG_FILE} for details"
exit 1
fi
rm "${DATA_FILE}"
else
mkdir -p /etc/cloudron
cat > "/etc/cloudron/cloudron.conf" <<CONF_END
{
"apiServerOrigin": "${apiServerOrigin}",
"webServerOrigin": "${webServerOrigin}",
"provider": "${provider}",
"edition": "${edition}"
}
CONF_END
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" &>> "${LOG_FILE}"; then
echo "Failed to install cloudron. See ${LOG_FILE} for details"
exit 1
fi
echo "${data}" > "${DATA_FILE}"
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" --data-file "${DATA_FILE}" --data-dir "${baseDataDir}" &>> "${LOG_FILE}"; then
echo "Failed to install cloudron. See ${LOG_FILE} for details"
exit 1
fi
rm "${DATA_FILE}"
echo -n "=> Waiting for cloudron to be ready (this takes some time) ..."
while true; do
@@ -247,15 +217,10 @@ while true; do
sleep 10
done
echo -e "\n\n${GREEN}Visit https://<IP> and accept the self-signed certificate to finish setup.${DONE}\n"
echo -e "\n\n${GREEN}Visit https://<IP> and accept the self-signed certificate to finish setup.${DONE}"
if [[ "${rebootServer}" == "true" ]]; then
systemctl stop box mysql # sometimes mysql ends up having corrupt privilege tables
read -p "The server has to be rebooted to apply all the settings. Reboot now ? [Y/n] " yn
yn=${yn:-y}
case $yn in
[Yy]* ) systemctl reboot;;
* ) exit;;
esac
echo -e "\n${RED}Rebooting this server now to let changes take effect.${DONE}\n"
systemctl stop mysql # sometimes mysql ends up having corrupt privilege tables
systemctl reboot
fi
-115
View File
@@ -1,115 +0,0 @@
#!/bin/bash
# This script collects diagnostic information to help debug server related issues
# It also enables SSH access for the cloudron support team
PASTEBIN="https://paste.cloudron.io"
OUT="/tmp/cloudron-support.log"
LINE="\n========================================================\n"
CLOUDRON_SUPPORT_PUBLIC_KEY="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQVilclYAIu+ioDp/sgzzFz6YU0hPcRYY7ze/LiF/lC7uQqK062O54BFXTvQ3ehtFZCx3bNckjlT2e6gB8Qq07OM66De4/S/g+HJW4TReY2ppSPMVNag0TNGxDzVH8pPHOysAm33LqT2b6L/wEXwC6zWFXhOhHjcMqXvi8Ejaj20H1HVVcf/j8qs5Thkp9nAaFTgQTPu8pgwD8wDeYX1hc9d0PYGesTADvo6HF4hLEoEnefLw7PaStEbzk2fD3j7/g5r5HcgQQXBe74xYZ/1gWOX2pFNuRYOBSEIrNfJEjFJsqk3NR1+ZoMGK7j+AZBR4k0xbrmncQLcQzl6MMDzkp support@cloudron.io"
HELP_MESSAGE="
This script collects diagnostic information to help debug server related issues
Options:
--enable-ssh Enable SSH access for the Cloudron support team
--help Show this message
"
# We require root
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root." > /dev/stderr
exit 1
fi
enableSSH="false"
args=$(getopt -o "" -l "help,enable-ssh" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--help) echo -e "${HELP_MESSAGE}"; exit 0;;
--enable-ssh) enableSSH="true"; shift;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
done
# check if at least 10mb root partition space is available
if [[ "`df --output="avail" / | sed -n 2p`" -lt "10240" ]]; then
echo "No more space left on /"
echo "This is likely the root case of the issue. Free up some space and also check other partitions below:"
echo ""
df -h
echo ""
echo "To recover from a full disk, follow the guide at https://cloudron.io/documentation/server/#recovery-after-disk-full"
exit 1
fi
# check for at least 5mb free /tmp space for the log file
if [[ "`df --output="avail" /tmp | sed -n 2p`" -lt "5120" ]]; then
echo "Not enough space left on /tmp"
echo "Free up some space first by deleting files from /tmp"
exit 1
fi
echo -n "Generating Cloudron Support stats..."
# clear file
rm -rf $OUT
ssh_port=$(cat /etc/ssh/sshd_config | grep "Port " | sed -e "s/.*Port //")
if [[ $SUDO_USER == "" ]]; then
ssh_user="root"
ssh_folder="/root/.ssh/"
authorized_key_file="${ssh_folder}/authorized_keys"
else
ssh_user="$SUDO_USER"
ssh_folder="/home/$SUDO_USER/.ssh/"
authorized_key_file="${ssh_folder}/authorized_keys"
fi
echo -e $LINE"SSH"$LINE >> $OUT
echo "Username: ${ssh_user}" >> $OUT
echo "Port: ${ssh_port}" >> $OUT
echo -e $LINE"cloudron.conf"$LINE >> $OUT
cat /etc/cloudron/cloudron.conf &>> $OUT
echo -e $LINE"Docker container"$LINE >> $OUT
if ! timeout --kill-after 10s 15s docker ps -a &>> $OUT 2>&1; then
echo -e "Docker is not responding" >> $OUT
fi
echo -e $LINE"Filesystem stats"$LINE >> $OUT
df -h &>> $OUT
echo -e $LINE"System daemon status"$LINE >> $OUT
systemctl status --lines=100 cloudron.target box mysql unbound cloudron-syslog nginx collectd docker &>> $OUT
echo -e $LINE"Box logs"$LINE >> $OUT
tail -n 100 /home/yellowtent/platformdata/logs/box.log &>> $OUT
echo -e $LINE"Firewall chains"$LINE >> $OUT
iptables -L &>> $OUT
echo "Done"
echo -n "Uploading information..."
# for some reason not using $(cat $OUT) will not contain newlines!?
paste_key=$(curl -X POST ${PASTEBIN}/documents --silent -d "$(cat $OUT)" | python3 -c "import sys, json; print(json.load(sys.stdin)['key'])")
echo "Done"
if [[ "${enableSSH}" == "true" ]]; then
echo -n "Enabling ssh access for the Cloudron support team..."
mkdir -p "${ssh_folder}"
echo "${CLOUDRON_SUPPORT_PUBLIC_KEY}" >> ${authorized_key_file}
chown -R ${ssh_user} "${ssh_folder}"
chmod 600 "${authorized_key_file}"
echo "Done"
fi
echo ""
echo "Please email the following link to support@cloudron.io"
echo ""
echo "${PASTEBIN}/${paste_key}"
+7 -11
View File
@@ -7,28 +7,21 @@ set -eu
[[ $(uname -s) == "Darwin" ]] && GNU_GETOPT="/usr/local/opt/gnu-getopt/bin/getopt" || GNU_GETOPT="getopt"
readonly GNU_GETOPT
args=$(${GNU_GETOPT} -o "" -l "output:,version:" -n "$0" -- "$@")
args=$(${GNU_GETOPT} -o "" -l "output:" -n "$0" -- "$@")
eval set -- "${args}"
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
bundle_file=""
version=""
while true; do
case "$1" in
--output) bundle_file="$2"; shift 2;;
--version) version="$2"; shift 2;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
done
if [[ -z "${version}" ]]; then
echo "--version is required"
exit 1
fi
readonly TMPDIR=${TMPDIR:-/tmp} # why is this not set on mint?
if ! $(cd "${SOURCE_DIR}" && git diff --exit-code >/dev/null); then
@@ -48,16 +41,19 @@ fi
box_version=$(cd "${SOURCE_DIR}" && git rev-parse "HEAD")
branch=$(git rev-parse --abbrev-ref HEAD)
dashboard_version=$(cd "${SOURCE_DIR}/../dashboard" && git fetch && git rev-parse "${branch}")
if [[ "${branch}" == "master" ]]; then
dashboard_version=$(cd "${SOURCE_DIR}/../dashboard" && git rev-parse "${branch}")
else
dashboard_version=$(cd "${SOURCE_DIR}/../dashboard" && git fetch && git rev-parse "origin/${branch}")
fi
bundle_dir=$(mktemp -d -t box 2>/dev/null || mktemp -d box-XXXXXXXXXX --tmpdir=$TMPDIR)
[[ -z "$bundle_file" ]] && bundle_file="${TMPDIR}/box-${box_version:0:10}-${dashboard_version:0:10}-${version}.tar.gz"
[[ -z "$bundle_file" ]] && bundle_file="${TMPDIR}/box-${box_version:0:10}-${dashboard_version:0:10}.tar.gz"
chmod "o+rx,g+rx" "${bundle_dir}" # otherwise extracted tarball director won't be readable by others/group
echo "==> Checking out code box version [${box_version}] and dashboard version [${dashboard_version}] into ${bundle_dir}"
(cd "${SOURCE_DIR}" && git archive --format=tar ${box_version} | (cd "${bundle_dir}" && tar xf -))
(cd "${SOURCE_DIR}/../dashboard" && git archive --format=tar ${dashboard_version} | (mkdir -p "${bundle_dir}/dashboard.build" && cd "${bundle_dir}/dashboard.build" && tar xf -))
(cp "${SOURCE_DIR}/../dashboard/LICENSE" "${bundle_dir}")
echo "${version}" > "${bundle_dir}/VERSION"
echo "==> Installing modules for dashboard asset generation"
(cd "${bundle_dir}/dashboard.build" && npm install --production)
+28 -21
View File
@@ -1,9 +1,5 @@
#!/bin/bash
# This script is run before the box code is switched. This means that we can
# put network related/curl downloads here. If the script fails, the old code
# will continue to run
set -eu -o pipefail
if [[ ${EUID} -ne 0 ]]; then
@@ -14,12 +10,29 @@ fi
readonly USER=yellowtent
readonly BOX_SRC_DIR=/home/${USER}/box
readonly BASE_DATA_DIR=/home/${USER}
readonly CLOUDRON_CONF=/home/yellowtent/configs/cloudron.conf
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly box_src_tmp_dir="$(realpath ${script_dir}/..)"
readonly is_update=$(systemctl is-active box && echo "yes" || echo "no")
readonly is_update=$([[ -f "${CLOUDRON_CONF}" ]] && echo "yes" || echo "no")
arg_data=""
arg_data_dir=""
args=$(getopt -o "" -l "data:,data-file:,data-dir:" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--data) arg_data="$2"; shift 2;;
--data-file) arg_data=$(cat $2); shift 2;;
--data-dir) arg_data_dir="$2"; shift 2;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
done
echo "==> installer: updating docker"
if [[ $(docker version --format {{.Client.Version}}) != "18.03.1-ce" ]]; then
@@ -81,21 +94,6 @@ if [[ ${try} -eq 10 ]]; then
exit 4
fi
echo "==> installer: downloading new addon images"
images=$(node -e "var i = require('${box_src_tmp_dir}/src/infra_version.js'); console.log(i.baseImages.map(function (x) { return x.tag; }).join(' '), Object.keys(i.images).map(function (x) { return i.images[x].tag; }).join(' '));")
echo -e "\tPulling docker images: ${images}"
for image in ${images}; do
if ! docker pull "${image}"; then # this pulls the image using the sha256
echo "==> installer: Could not pull ${image}"
exit 5
fi
if ! docker pull "${image%@sha256:*}"; then # this will tag the image for readability
echo "==> installer: Could not pull ${image%@sha256:*}"
exit 6
fi
done
echo "==> installer: update cloudron-syslog"
CLOUDRON_SYSLOG_DIR=/usr/local/cloudron-syslog
CLOUDRON_SYSLOG="${CLOUDRON_SYSLOG_DIR}/bin/cloudron-syslog"
@@ -117,6 +115,15 @@ if [[ "${is_update}" == "yes" ]]; then
${BOX_SRC_DIR}/setup/stop.sh
fi
# setup links to data directory
if [[ -n "${arg_data_dir}" ]]; then
echo "==> installer: setting up links to data directory"
mkdir "${arg_data_dir}/appsdata"
ln -s "${arg_data_dir}/appsdata" "${BASE_DATA_DIR}/appsdata"
mkdir "${arg_data_dir}/platformdata"
ln -s "${arg_data_dir}/platformdata" "${BASE_DATA_DIR}/platformdata"
fi
# ensure we are not inside the source directory, which we will remove now
cd /root
@@ -126,4 +133,4 @@ mv "${box_src_tmp_dir}" "${BOX_SRC_DIR}"
chown -R "${USER}:${USER}" "${BOX_SRC_DIR}"
echo "==> installer: calling box setup script"
"${BOX_SRC_DIR}/setup/start.sh"
"${BOX_SRC_DIR}/setup/start.sh" --data "${arg_data}"
+76
View File
@@ -0,0 +1,76 @@
#!/bin/bash
source_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
json="${source_dir}/../node_modules/.bin/json"
arg_api_server_origin=""
arg_fqdn="" # remove after 1.10
arg_admin_domain=""
arg_admin_location=""
arg_admin_fqdn=""
arg_retire_reason=""
arg_retire_info=""
arg_version=""
arg_web_server_origin=""
arg_provider=""
arg_is_demo="false"
arg_edition=""
args=$(getopt -o "" -l "data:,retire-reason:,retire-info:" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--retire-reason)
arg_retire_reason="$2"
shift 2
;;
--retire-info)
arg_retire_info="$2"
shift 2
;;
--data)
# these params must be valid in all cases
arg_fqdn=$(echo "$2" | $json fqdn)
arg_admin_fqdn=$(echo "$2" | $json adminFqdn)
arg_admin_location=$(echo "$2" | $json adminLocation)
[[ "${arg_admin_location}" == "" ]] && arg_admin_location="my"
arg_admin_domain=$(echo "$2" | $json adminDomain)
[[ "${arg_admin_domain}" == "" ]] && arg_admin_domain="${arg_fqdn}"
# only update/restore have this valid (but not migrate)
arg_api_server_origin=$(echo "$2" | $json apiServerOrigin)
[[ "${arg_api_server_origin}" == "" ]] && arg_api_server_origin="https://api.cloudron.io"
arg_web_server_origin=$(echo "$2" | $json webServerOrigin)
[[ "${arg_web_server_origin}" == "" ]] && arg_web_server_origin="https://cloudron.io"
# TODO check if and where this is used
arg_version=$(echo "$2" | $json version)
# read possibly empty parameters here
arg_is_demo=$(echo "$2" | $json isDemo)
[[ "${arg_is_demo}" == "" ]] && arg_is_demo="false"
arg_provider=$(echo "$2" | $json provider)
[[ "${arg_provider}" == "" ]] && arg_provider="generic"
arg_edition=$(echo "$2" | $json edition)
[[ "${arg_edition}" == "" ]] && arg_edition=""
shift 2
;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
done
echo "Parsed arguments:"
echo "api server: ${arg_api_server_origin}"
echo "admin fqdn: ${arg_admin_fqdn}"
echo "fqdn: ${arg_fqdn}"
echo "version: ${arg_version}"
echo "web server: ${arg_web_server_origin}"
echo "provider: ${arg_provider}"
echo "edition: ${arg_edition}"
+65 -18
View File
@@ -2,9 +2,6 @@
set -eu -o pipefail
# This script is run after the box code is switched. This means that this script
# should pretty much always succeed. No network logic/download code here.
echo "==> Cloudron Start"
readonly USER="yellowtent"
@@ -13,12 +10,19 @@ readonly BOX_SRC_DIR="${HOME_DIR}/box"
readonly PLATFORM_DATA_DIR="${HOME_DIR}/platformdata" # platform data
readonly APPS_DATA_DIR="${HOME_DIR}/appsdata" # app data
readonly BOX_DATA_DIR="${HOME_DIR}/boxdata" # box data
readonly CONFIG_DIR="${HOME_DIR}/configs"
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly json="$(realpath ${script_dir}/../node_modules/.bin/json)"
readonly ubuntu_version=$(lsb_release -rs)
cp -f "${script_dir}/../scripts/cloudron-support" /usr/bin/cloudron-support
source "${script_dir}/argparser.sh" "$@" # this injects the arg_* variables used below
echo "==> Configuring host"
sed -e 's/^#NTP=/NTP=0.ubuntu.pool.ntp.org 1.ubuntu.pool.ntp.org 2.ubuntu.pool.ntp.org 3.ubuntu.pool.ntp.org/' -i /etc/systemd/timesyncd.conf
timedatectl set-ntp 1
timedatectl set-timezone UTC
hostnamectl set-hostname "${arg_admin_fqdn}"
echo "==> Configuring docker"
cp "${script_dir}/start/docker-cloudron-app.apparmor" /etc/apparmor.d/docker-cloudron-app
@@ -44,6 +48,19 @@ if [[ ! -f /etc/systemd/system/docker.service.d/cloudron.conf ]] || ! diff -q /e
fi
docker network create --subnet=172.18.0.0/16 cloudron || true
# caas has ssh on port 202 and we disable password login
if [[ "${arg_provider}" == "caas" ]]; then
# https://stackoverflow.com/questions/4348166/using-with-sed on why ? must be escaped
sed -e 's/^#\?PermitRootLogin .*/PermitRootLogin without-password/g' \
-e 's/^#\?PermitEmptyPasswords .*/PermitEmptyPasswords no/g' \
-e 's/^#\?PasswordAuthentication .*/PasswordAuthentication no/g' \
-e 's/^#\?Port .*/Port 202/g' \
-i /etc/ssh/sshd_config
# required so we can connect to this machine since port 22 is blocked by iptables by now
systemctl reload sshd
fi
mkdir -p "${BOX_DATA_DIR}"
mkdir -p "${APPS_DATA_DIR}"
@@ -54,13 +71,12 @@ mkdir -p "${PLATFORM_DATA_DIR}/graphite"
mkdir -p "${PLATFORM_DATA_DIR}/mysql"
mkdir -p "${PLATFORM_DATA_DIR}/postgresql"
mkdir -p "${PLATFORM_DATA_DIR}/mongodb"
mkdir -p "${PLATFORM_DATA_DIR}/redis"
mkdir -p "${PLATFORM_DATA_DIR}/addons/mail"
mkdir -p "${PLATFORM_DATA_DIR}/collectd/collectd.conf.d"
mkdir -p "${PLATFORM_DATA_DIR}/logrotate.d"
mkdir -p "${PLATFORM_DATA_DIR}/acme"
mkdir -p "${PLATFORM_DATA_DIR}/backup"
mkdir -p "${PLATFORM_DATA_DIR}/logs/backup" "${PLATFORM_DATA_DIR}/logs/updater" "${PLATFORM_DATA_DIR}/logs/tasks"
mkdir -p "${PLATFORM_DATA_DIR}/logs/backup"
mkdir -p "${PLATFORM_DATA_DIR}/update"
mkdir -p "${BOX_DATA_DIR}/appicons"
@@ -90,19 +106,20 @@ systemctl daemon-reload
systemctl restart systemd-journald
setfacl -n -m u:${USER}:r /var/log/journal/*/system.journal
echo "==> Creating config directory"
mkdir -p "${CONFIG_DIR}"
echo "==> Setting up unbound"
# DO uses Google nameservers by default. This causes RBL queries to fail (host 2.0.0.127.zen.spamhaus.org)
# We do not use dnsmasq because it is not a recursive resolver and defaults to the value in the interfaces file (which is Google DNS!)
# We listen on 0.0.0.0 because there is no way control ordering of docker (which creates the 172.18.0.0/16) and unbound
# If IP6 is not enabled, dns queries seem to fail on some hosts. -s returns false if file missing or 0 size
ip6=$([[ -s /proc/net/if_inet6 ]] && echo "yes" || echo "no")
echo -e "server:\n\tinterface: 0.0.0.0\n\tdo-ip6: ${ip6}\n\taccess-control: 127.0.0.1 allow\n\taccess-control: 172.18.0.1/16 allow\n\tcache-max-negative-ttl: 30\n\tcache-max-ttl: 300\n\t#logfile: /var/log/unbound.log\n\t#verbosity: 10" > /etc/unbound/unbound.conf.d/cloudron-network.conf
# If IP6 is not enabled, dns queries seem to fail on some hosts
echo -e "server:\n\tinterface: 0.0.0.0\n\tdo-ip6: yes\n\taccess-control: 127.0.0.1 allow\n\taccess-control: 172.18.0.1/16 allow\n\tcache-max-negative-ttl: 30\n\tcache-max-ttl: 300\n\t#logfile: /var/log/unbound.log\n\t#verbosity: 10" > /etc/unbound/unbound.conf.d/cloudron-network.conf
# update the root anchor after a out-of-disk-space situation (see #269)
unbound-anchor -a /var/lib/unbound/root.key
echo "==> Adding systemd services"
cp -r "${script_dir}/start/systemd/." /etc/systemd/system/
[[ "${ubuntu_version}" == "16.04" ]] && sed -e 's/MemoryMax/MemoryLimit/g' -i /etc/systemd/system/box.service
systemctl daemon-reload
systemctl enable unbound
systemctl enable cloudron-syslog
@@ -135,8 +152,8 @@ echo "==> Configuring logrotate"
if ! grep -q "^include ${PLATFORM_DATA_DIR}/logrotate.d" /etc/logrotate.conf; then
echo -e "\ninclude ${PLATFORM_DATA_DIR}/logrotate.d\n" >> /etc/logrotate.conf
fi
cp "${script_dir}/start/box-logrotate" "${script_dir}/start/app-logrotate" "${PLATFORM_DATA_DIR}/logrotate.d/"
chown root:root "${PLATFORM_DATA_DIR}/logrotate.d/box-logrotate" "${PLATFORM_DATA_DIR}/logrotate.d/app-logrotate"
cp "${script_dir}/start/app-logrotate" "${PLATFORM_DATA_DIR}/logrotate.d/app-logrotate"
chown root:root "${PLATFORM_DATA_DIR}/logrotate.d/app-logrotate"
echo "==> Adding motd message for admins"
cp "${script_dir}/start/cloudron-motd" /etc/update-motd.d/92-cloudron
@@ -154,8 +171,14 @@ if ! grep -q "^Restart=" /etc/systemd/system/multi-user.target.wants/nginx.servi
echo -e "\n[Service]\nRestart=always\n" >> /etc/systemd/system/multi-user.target.wants/nginx.service
systemctl daemon-reload
fi
# remove this migration after 1.10
[[ -f /etc/nginx/cert/host.cert ]] && cp /etc/nginx/cert/host.cert "/etc/nginx/cert/${arg_admin_domain}.host.cert"
[[ -f /etc/nginx/cert/host.key ]] && cp /etc/nginx/cert/host.key "/etc/nginx/cert/${arg_admin_domain}.host.key"
systemctl start nginx
# bookkeep the version as part of data
echo "{ \"version\": \"${arg_version}\", \"apiServerOrigin\": \"${arg_api_server_origin}\" }" > "${BOX_DATA_DIR}/version"
# restart mysql to make sure it has latest config
if [[ ! -f /etc/mysql/mysql.cnf ]] || ! diff -q "${script_dir}/start/mysql.cnf" /etc/mysql/mysql.cnf >/dev/null; then
# wait for all running mysql jobs
@@ -179,7 +202,33 @@ mysqladmin -u root -ppassword password password # reset default root password
mysql -u root -p${mysql_root_password} -e 'CREATE DATABASE IF NOT EXISTS box'
echo "==> Migrating data"
(cd "${BOX_SRC_DIR}" && BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@127.0.0.1/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up)
sudo -u "${USER}" -H bash <<EOF
set -eu
cd "${BOX_SRC_DIR}"
BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@127.0.0.1/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up
EOF
echo "==> Creating cloudron.conf"
cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
{
"version": "${arg_version}",
"apiServerOrigin": "${arg_api_server_origin}",
"webServerOrigin": "${arg_web_server_origin}",
"adminDomain": "${arg_admin_domain}",
"adminFqdn": "${arg_admin_fqdn}",
"adminLocation": "${arg_admin_location}",
"provider": "${arg_provider}",
"isDemo": ${arg_is_demo},
"edition": "${arg_edition}"
}
CONF_END
echo "==> Creating config.json for dashboard"
cat > "${BOX_SRC_DIR}/dashboard/dist/config.json" <<CONF_END
{
"webServerOrigin": "${arg_web_server_origin}"
}
CONF_END
if [[ ! -f "${BOX_DATA_DIR}/dhparams.pem" ]]; then
echo "==> Generating dhparams (takes forever)"
@@ -190,12 +239,10 @@ else
fi
echo "==> Changing ownership"
# be careful of what is chown'ed here. subdirs like mysql,redis etc are owned by the containers and will stop working if perms change
chown -R "${USER}" /etc/cloudron
chown "${USER}:${USER}" -R "${CONFIG_DIR}"
chown "${USER}:${USER}" -R "${PLATFORM_DATA_DIR}/nginx" "${PLATFORM_DATA_DIR}/collectd" "${PLATFORM_DATA_DIR}/addons" "${PLATFORM_DATA_DIR}/acme" "${PLATFORM_DATA_DIR}/backup" "${PLATFORM_DATA_DIR}/logs" "${PLATFORM_DATA_DIR}/update"
chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}/INFRA_VERSION" 2>/dev/null || true
chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}"
chown "${USER}:${USER}" "${APPS_DATA_DIR}"
# logrotate files have to be owned by root, this is here to fixup existing installations where we were resetting the owner to yellowtent
chown root:root -R "${PLATFORM_DATA_DIR}/logrotate.d"
-9
View File
@@ -1,9 +0,0 @@
# logrotate config for box logs
/home/yellowtent/platformdata/logs/box.log {
rotate 10
size 10M
# we never compress so we can simply tail the files
nocompress
copytruncate
}
+12 -26
View File
@@ -1,29 +1,15 @@
#!/bin/bash
#!/bin/sh
# motd hook to remind admins about updates
printf "\t\t\tNOTE TO CLOUDRON ADMINS\n"
printf "\t\t\t-----------------------\n"
printf "Please do not run apt upgrade manually as it will update packages that\n"
printf "Cloudron relies on and may break your installation. Ubuntu security updates\n"
printf "are automatically installed on this server every night.\n"
printf "\n"
printf "Read more at https://cloudron.io/documentation/security/#os-updates\n"
printf "**********************************************************************\n\n"
if [[ -z "$(ls -A /home/yellowtent/boxdata/mail/dkim)" ]]; then
printf "\t\t\tWELCOME TO CLOUDRON\n"
printf "\t\t\t-------------------\n"
printf '\n\e[1;32m%-6s\e[m\n\n' "Visit https://<IP> on your browser and accept the self-signed certificate to finish setup."
printf "Cloudron overview - https://cloudron.io/documentation/ \n"
printf "Cloudron setup - https://cloudron.io/documentation/installation/#setup \n"
else
printf "\t\t\tNOTE TO CLOUDRON ADMINS\n"
printf "\t\t\t-----------------------\n"
printf "Please do not run apt upgrade manually as it will update packages that\n"
printf "Cloudron relies on and may break your installation. Ubuntu security updates\n"
printf "are automatically installed on this server every night.\n"
printf "\n"
printf "Read more at https://cloudron.io/documentation/security/#os-updates\n"
if grep -q "^PasswordAuthentication yes" /etc/ssh/sshd_config; then
printf "\nPlease disable password based SSH access to secure your server. Read more at\n"
printf "https://cloudron.io/documentation/security/#securing-ssh-access\n"
fi
if grep -q "^PasswordAuthentication yes" /etc/ssh/sshd_config; then
printf "\nPlease disable password based SSH access to secure your server. Read more at\n"
printf "https://cloudron.io/documentation/security/#securing-ssh-access\n"
fi
printf "\nFor help and more information, visit https://forum.cloudron.io\n\n"
printf "**********************************************************************\n"
@@ -162,7 +162,7 @@ server {
# graphite paths (uncomment block below and visit /graphite/index.html)
# remember to comment out the CSP policy as well to access the graphite dashboard
# location ~ ^/(graphite|content|metrics|dashboard|render|browser|composer)/ {
# proxy_pass http://127.0.0.1:8417;
# proxy_pass http://127.0.0.1:8000;
# client_max_body_size 1m;
# }
+8 -20
View File
@@ -1,17 +1,11 @@
# sudo logging breaks journalctl output with very long urls (systemd bug)
Defaults !syslog
Defaults!/home/yellowtent/box/src/scripts/clearvolume.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/clearvolume.sh
Defaults!/home/yellowtent/box/src/scripts/createappdir.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/createappdir.sh
Defaults!/home/yellowtent/box/src/scripts/mvvolume.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/mvvolume.sh
Defaults!/home/yellowtent/box/src/scripts/mkdirvolume.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/mkdirvolume.sh
Defaults!/home/yellowtent/box/src/scripts/rmaddondir.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmaddondir.sh
Defaults!/home/yellowtent/box/src/scripts/rmappdir.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmappdir.sh
Defaults!/home/yellowtent/box/src/scripts/reloadnginx.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/reloadnginx.sh
@@ -31,21 +25,15 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/retire.sh
Defaults!/home/yellowtent/box/src/scripts/update.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/update.sh
Defaults!/home/yellowtent/box/src/scripts/remotesupport.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/remotesupport.sh
Defaults!/home/yellowtent/box/src/scripts/authorized_keys.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/authorized_keys.sh
Defaults!/home/yellowtent/box/src/scripts/configurelogrotate.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/configurelogrotate.sh
Defaults!/home/yellowtent/box/src/scripts/backupupload.js env_keep="HOME BOX_ENV"
Defaults!/home/yellowtent/box/src/scripts/backupupload.js closefrom_override
yellowtent ALL=(root) NOPASSWD:SETENV: /home/yellowtent/box/src/scripts/backupupload.js
Defaults!/home/yellowtent/box/src/backuptask.js env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD:SETENV: /home/yellowtent/box/src/backuptask.js
Defaults!/home/yellowtent/box/src/scripts/restart.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restart.sh
Defaults!/home/yellowtent/box/src/scripts/restartdocker.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restartdocker.sh
Defaults!/home/yellowtent/box/src/scripts/restartunbound.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restartunbound.sh
+2 -6
View File
@@ -12,17 +12,13 @@ Wants=cloudron-resize-fs.service
Type=idle
WorkingDirectory=/home/yellowtent/box
Restart=always
; Systemd does not append logs when logging to files, we spawn a shell first and exec to replace it after setting up the pipes
ExecStart=/bin/sh -c 'echo "Logging to /home/yellowtent/platformdata/logs/box.log"; exec /usr/bin/node --max_old_space_size=150 /home/yellowtent/box/box.js >> /home/yellowtent/platformdata/logs/box.log 2>&1'
ExecStart=/usr/bin/node --max_old_space_size=150 /home/yellowtent/box/box.js
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmile" "BOX_ENV=cloudron" "NODE_ENV=production"
; kill apptask processes as well
KillMode=control-group
; Do not kill this process on OOM. Children inherit this score. Do not set it to -1000 so that MemoryMax can keep working
OOMScoreAdjust=-999
User=yellowtent
Group=yellowtent
; OOM killer is invoked in this unit beyond this. The start script replaces this with MemoryLimit for Ubuntu 16
MemoryMax=400M
MemoryLimit=200M
TimeoutStopSec=5s
StartLimitInterval=1
StartLimitBurst=60
+238 -1103
View File
File diff suppressed because it is too large Load Diff
+26 -72
View File
@@ -18,7 +18,6 @@ exports = module.exports = {
getAddonConfigByName: getAddonConfigByName,
unsetAddonConfig: unsetAddonConfig,
unsetAddonConfigByAppId: unsetAddonConfigByAppId,
getAppIdByAddonConfigValue: getAppIdByAddonConfigValue,
setHealth: setHealth,
setInstallationCommand: setInstallationCommand,
@@ -43,7 +42,7 @@ exports = module.exports = {
RSTATE_RUNNING: 'running',
RSTATE_PENDING_START: 'pending_start',
RSTATE_PENDING_STOP: 'pending_stop',
RSTATE_STOPPED: 'stopped', // app stopped by us
RSTATE_STOPPED: 'stopped', // app stopped by use
// run codes (keep in sync in UI)
HEALTH_HEALTHY: 'healthy',
@@ -62,6 +61,7 @@ var assert = require('assert'),
async = require('async'),
database = require('./database.js'),
DatabaseError = require('./databaseerror'),
mailboxdb = require('./mailboxdb.js'),
safe = require('safetydance'),
util = require('util');
@@ -69,8 +69,7 @@ var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationSta
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'subdomains.subdomain AS location', 'subdomains.domain',
'apps.accessRestrictionJson', 'apps.restoreConfigJson', 'apps.oldConfigJson', 'apps.updateConfigJson', 'apps.memoryLimit',
'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
'apps.creationTime', 'apps.updateTime', 'apps.ownerId', 'apps.mailboxName', 'apps.enableAutomaticUpdate',
'apps.dataDir', 'apps.ts', 'apps.healthTime' ].join(',');
'apps.creationTime', 'apps.updateTime', 'apps.ownerId', 'apps.ts' ].join(',');
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId' ].join(',');
@@ -107,7 +106,7 @@ function postProcess(result) {
delete result.environmentVariables;
delete result.portTypes;
for (let i = 0; i < environmentVariables.length; i++) {
for (var i = 0; i < environmentVariables.length; i++) {
result.portBindings[environmentVariables[i]] = { hostPort: parseInt(hostPorts[i], 10), type: portTypes[i] };
}
@@ -121,7 +120,6 @@ function postProcess(result) {
result.sso = !!result.sso; // make it bool
result.enableBackup = !!result.enableBackup; // make it bool
result.enableAutomaticUpdate = !!result.enableAutomaticUpdate; // make it bool
assert(result.debugModeJson === null || typeof result.debugModeJson === 'string');
result.debugMode = safe.JSON.parse(result.debugModeJson);
@@ -132,17 +130,6 @@ function postProcess(result) {
delete d.appId;
delete d.type;
});
let envNames = JSON.parse(result.envNames), envValues = JSON.parse(result.envValues);
delete result.envNames;
delete result.envValues;
result.env = {};
for (let i = 0; i < envNames.length; i++) { // NOTE: envNames is [ null ] when env of an app is empty
if (envNames[i]) result.env[envNames[i]] = envValues[i];
}
// in the db, we store dataDir as unique/nullable
result.dataDir = result.dataDir || '';
}
function get(id, callback) {
@@ -150,11 +137,9 @@ function get(id, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes, '
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes'
+ ' FROM apps'
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
+ ' WHERE apps.id = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, id ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
@@ -168,7 +153,7 @@ function get(id, callback) {
postProcess(result[0]);
callback(null, result[0]);
});
})
});
}
@@ -177,11 +162,9 @@ function getByHttpPort(httpPort, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes,'
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes'
+ ' FROM apps'
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
+ ' WHERE httpPort = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, httpPort ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
@@ -203,11 +186,9 @@ function getByContainerId(containerId, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes,'
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes'
+ ' FROM apps'
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
+ ' WHERE containerId = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, containerId ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
@@ -228,11 +209,9 @@ function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes,'
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes'
+ ' FROM apps'
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
+ ' GROUP BY apps.id ORDER BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
@@ -280,15 +259,13 @@ function add(id, appStoreId, manifest, location, domain, ownerId, portBindings,
var sso = 'sso' in data ? data.sso : null;
var robotsTxt = 'robotsTxt' in data ? data.robotsTxt : null;
var debugModeJson = data.debugMode ? JSON.stringify(data.debugMode) : null;
var env = data.env || {};
const mailboxName = data.mailboxName || null;
var queries = [];
queries.push({
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId, mailboxName) ' +
' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId, mailboxName ]
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId) ' +
' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId ]
});
queries.push({
@@ -303,12 +280,13 @@ function add(id, appStoreId, manifest, location, domain, ownerId, portBindings,
});
});
Object.keys(env).forEach(function (name) {
// only allocate a mailbox if mailboxName is set
if (data.mailboxName) {
queries.push({
query: 'INSERT INTO appEnvVars (appId, name, value) VALUES (?, ?, ?)',
args: [ id, name, env[name] ]
query: 'INSERT INTO mailboxes (name, type, domain, ownerId, ownerType) VALUES (?, ?, ?, ?, ?)',
args: [ data.mailboxName, mailboxdb.TYPE_MAILBOX, domain, id, mailboxdb.OWNER_TYPE_APP ]
});
});
}
if (data.alternateDomains) {
data.alternateDomains.forEach(function (d) {
@@ -374,8 +352,8 @@ function del(id, callback) {
var queries = [
{ query: 'DELETE FROM subdomains WHERE appId = ?', args: [ id ] },
{ query: 'DELETE FROM mailboxes WHERE ownerId=?', args: [ id ] },
{ query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] },
{ query: 'DELETE FROM appEnvVars WHERE appId = ?', args: [ id ] },
{ query: 'DELETE FROM apps WHERE id = ?', args: [ id ] }
];
@@ -394,7 +372,6 @@ function clear(callback) {
database.query.bind(null, 'DELETE FROM subdomains'),
database.query.bind(null, 'DELETE FROM appPortBindings'),
database.query.bind(null, 'DELETE FROM appAddonConfigs'),
database.query.bind(null, 'DELETE FROM appEnvVars'),
database.query.bind(null, 'DELETE FROM apps')
], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
@@ -414,7 +391,6 @@ function updateWithConstraints(id, app, constraints, callback) {
assert(!('portBindings' in app) || typeof app.portBindings === 'object');
assert(!('accessRestriction' in app) || typeof app.accessRestriction === 'object' || app.accessRestriction === '');
assert(!('alternateDomains' in app) || Array.isArray(app.alternateDomains));
assert(!('env' in app) || typeof app.env === 'object');
var queries = [ ];
@@ -428,19 +404,12 @@ function updateWithConstraints(id, app, constraints, callback) {
});
}
if ('env' in app) {
queries.push({ query: 'DELETE FROM appEnvVars WHERE appId = ?', args: [ id ] });
Object.keys(app.env).forEach(function (name) {
queries.push({
query: 'INSERT INTO appEnvVars (appId, name, value) VALUES (?, ?, ?)',
args: [ id, name, app.env[name] ]
});
});
if ('location' in app) {
queries.push({ query: 'UPDATE subdomains SET subdomain = ? WHERE appId = ? AND type = ?', args: [ app.location, id, exports.SUBDOMAIN_TYPE_PRIMARY ]});
}
if ('location' in app && 'domain' in app) { // must be updated together as they are unique together
queries.push({ query: 'UPDATE subdomains SET subdomain = ?, domain = ? WHERE appId = ? AND type = ?', args: [ app.location, app.domain, id, exports.SUBDOMAIN_TYPE_PRIMARY ]});
if ('domain' in app) {
queries.push({ query: 'UPDATE subdomains SET domain = ? WHERE appId = ? AND type = ?', args: [ app.domain, id, exports.SUBDOMAIN_TYPE_PRIMARY ]});
}
if ('alternateDomains' in app) {
@@ -455,7 +424,7 @@ function updateWithConstraints(id, app, constraints, callback) {
if (p === 'manifest' || p === 'oldConfig' || p === 'updateConfig' || p === 'restoreConfig' || p === 'accessRestriction' || p === 'debugMode') {
fields.push(`${p}Json = ?`);
values.push(JSON.stringify(app[p]));
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains' && p !== 'env') {
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains') {
fields.push(p + ' = ?');
values.push(app[p]);
}
@@ -476,13 +445,12 @@ function updateWithConstraints(id, app, constraints, callback) {
}
// not sure if health should influence runState
function setHealth(appId, health, healthTime, callback) {
function setHealth(appId, health, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof health, 'string');
assert(util.isDate(healthTime));
assert.strictEqual(typeof callback, 'function');
var values = { health, healthTime };
var values = { health: health };
var constraints = 'AND runState NOT LIKE "pending_%" AND installationState = "installed"';
@@ -615,20 +583,6 @@ function getAddonConfigByAppId(appId, callback) {
});
}
function getAppIdByAddonConfigValue(addonId, name, value, callback) {
assert.strictEqual(typeof addonId, 'string');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof value, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT appId FROM appAddonConfigs WHERE addonId = ? AND name = ? AND value = ?', [ addonId, name, value ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null, results[0].appId);
});
}
function getAddonConfigByName(appId, addonId, name, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof addonId, 'string');
@@ -663,7 +617,7 @@ function transferOwnership(oldOwnerId, newOwnerId, callback) {
assert.strictEqual(typeof newOwnerId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('UPDATE apps SET ownerId=? WHERE ownerId=?', [ newOwnerId, oldOwnerId ], function (error) {
database.query('UPDATE apps SET ownerId=? WHERE ownerId=?', [ newOwnerId, oldOwnerId ], function (error, results) {
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'No such user'));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
+95 -91
View File
@@ -7,21 +7,20 @@ var appdb = require('./appdb.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:apphealthmonitor'),
docker = require('./docker.js').connection,
eventlog = require('./eventlog.js'),
mailer = require('./mailer.js'),
superagent = require('superagent'),
util = require('util');
exports = module.exports = {
run: run
start: start,
stop: stop
};
const HEALTHCHECK_INTERVAL = 10 * 1000; // every 10 seconds. this needs to be small since the UI makes only healthy apps clickable
const UNHEALTHY_THRESHOLD = 10 * 60 * 1000; // 10 minutes
const OOM_MAIL_LIMIT = 60 * 60 * 1000; // 60 minutes
let gLastOomMailTime = Date.now() - (5 * 60 * 1000); // pretend we sent email 5 minutes ago
const AUDIT_SOURCE = { userId: null, username: 'healthmonitor' };
var HEALTHCHECK_INTERVAL = 10 * 1000; // every 10 seconds. this needs to be small since the UI makes only healthy apps clickable
var UNHEALTHY_THRESHOLD = 10 * 60 * 1000; // 10 minutes
var gHealthInfo = { }; // { time, emailSent }
var gRunTimeout = null;
var gDockerEventStream = null;
function debugApp(app) {
assert(typeof app === 'object');
@@ -34,29 +33,27 @@ function setHealth(app, health, callback) {
assert.strictEqual(typeof health, 'string');
assert.strictEqual(typeof callback, 'function');
let now = new Date(), healthTime = app.healthTime, curHealth = app.health;
var now = new Date();
if (!(app.id in gHealthInfo)) { // add new apps to list
gHealthInfo[app.id] = { time: now, emailSent: false };
}
if (health === appdb.HEALTH_HEALTHY) {
healthTime = now;
if (curHealth && curHealth !== appdb.HEALTH_HEALTHY) { // app starts out with null health
debugApp(app, 'app switched from %s to healthy', curHealth);
gHealthInfo[app.id].time = now;
} else if (Math.abs(now - gHealthInfo[app.id].time) > UNHEALTHY_THRESHOLD) {
if (gHealthInfo[app.id].emailSent) return callback(null);
// do not send mails for dev apps
if (!app.debugMode) eventlog.add(eventlog.ACTION_APP_UP, AUDIT_SOURCE, { app: app });
}
} else if (Math.abs(now - healthTime) > UNHEALTHY_THRESHOLD) {
if (curHealth === appdb.HEALTH_HEALTHY) {
debugApp(app, 'marking as unhealthy since not seen for more than %s minutes', UNHEALTHY_THRESHOLD/(60 * 1000));
debugApp(app, 'marking as unhealthy since not seen for more than %s minutes', UNHEALTHY_THRESHOLD/(60 * 1000));
// do not send mails for dev apps
if (!app.debugMode) eventlog.add(eventlog.ACTION_APP_DOWN, AUDIT_SOURCE, { app: app });
}
if (!app.debugMode) mailer.appDied(app); // do not send mails for dev apps
gHealthInfo[app.id].emailSent = true;
} else {
debugApp(app, 'waiting for %s seconds to update the app health', (UNHEALTHY_THRESHOLD - Math.abs(now - healthTime))/1000);
debugApp(app, 'waiting for sometime to update the app health');
return callback(null);
}
appdb.setHealth(app.id, health, healthTime, function (error) {
appdb.setHealth(app.id, health, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null); // app uninstalled?
if (error) return callback(error);
@@ -91,9 +88,6 @@ function checkAppHealth(app, callback) {
return setHealth(app, appdb.HEALTH_DEAD, callback);
}
// non-appstore apps may not have healthCheckPath
if (!manifest.healthCheckPath) return setHealth(app, appdb.HEALTH_HEALTHY, callback);
// poll through docker network instead of nginx to bypass any potential oauth proxy
var healthCheckUrl = 'http://127.0.0.1:' + app.httpPort + manifest.healthCheckPath;
superagent
@@ -116,60 +110,7 @@ function checkAppHealth(app, callback) {
});
}
/*
OOM can be tested using stress tool like so:
docker run -ti -m 100M cloudron/base:0.10.0 /bin/bash
apt-get update && apt-get install stress
stress --vm 1 --vm-bytes 200M --vm-hang 0
*/
function processDockerEvents(intervalSecs, callback) {
assert.strictEqual(typeof intervalSecs, 'number');
assert.strictEqual(typeof callback, 'function');
const since = ((new Date().getTime() / 1000) - intervalSecs).toFixed(0);
const until = ((new Date().getTime() / 1000) - 1).toFixed(0);
docker.getEvents({ since: since, until: until, filters: JSON.stringify({ event: [ 'oom' ] }) }, function (error, stream) {
if (error) return callback(error);
stream.setEncoding('utf8');
stream.on('data', function (data) {
var ev = JSON.parse(data);
var containerId = ev.id;
appdb.getByContainerId(containerId, function (error, app) { // this can error for addons
var program = error || !app.id ? containerId : `app-${app.id}`;
var now = Date.now();
const notifyUser = (!app || !app.debugMode) && (now - gLastOomMailTime > OOM_MAIL_LIMIT);
debug('OOM %s notifyUser: %s. lastOomTime: %s (now: %s)', program, notifyUser, gLastOomMailTime, now, ev);
// do not send mails for dev apps
if (notifyUser) {
// app can be null for addon containers
eventlog.add(eventlog.ACTION_APP_OOM, AUDIT_SOURCE, { ev: ev, containerId: containerId, app: app || null });
gLastOomMailTime = now;
}
});
});
stream.on('error', function (error) {
debug('Error reading docker events', error);
callback();
});
stream.on('end', callback);
// safety hatch if 'until' doesn't work (there are cases where docker is working with a different time)
setTimeout(stream.destroy.bind(stream), 3000); // https://github.com/apocas/dockerode/issues/179
});
}
function processApp(callback) {
assert.strictEqual(typeof callback, 'function');
function processApps(callback) {
apps.getAll(function (error, result) {
if (error) return callback(error);
@@ -187,16 +128,79 @@ function processApp(callback) {
});
}
function run(intervalSecs, callback) {
assert.strictEqual(typeof intervalSecs, 'number');
assert.strictEqual(typeof callback, 'function');
function run() {
processApps(function (error) {
if (error) console.error(error);
async.series([
processApp, // this is first because docker.getEvents seems to get 'stuck' sometimes
processDockerEvents.bind(null, intervalSecs)
], function (error) {
if (error) debug(error);
callback();
gRunTimeout = setTimeout(run, HEALTHCHECK_INTERVAL);
});
}
/*
OOM can be tested using stress tool like so:
docker run -ti -m 100M cloudron/base:0.10.0 /bin/bash
apt-get update && apt-get install stress
stress --vm 1 --vm-bytes 200M --vm-hang 0
*/
function processDockerEvents() {
// note that for some reason, the callback is called only on the first event
debug('Listening for docker events');
const OOM_MAIL_LIMIT = 60 * 60 * 1000; // 60 minutes
var lastOomMailTime = new Date(new Date() - OOM_MAIL_LIMIT);
docker.getEvents({ filters: JSON.stringify({ event: [ 'oom' ] }) }, function (error, stream) {
if (error) return console.error(error);
gDockerEventStream = stream;
stream.setEncoding('utf8');
stream.on('data', function (data) {
var ev = JSON.parse(data);
appdb.getByContainerId(ev.id, function (error, app) { // this can error for addons
var program = error || !app.appStoreId ? ev.id : app.appStoreId;
var context = JSON.stringify(ev);
var now = new Date();
if (app) context = context + '\n\n' + JSON.stringify(app, null, 4) + '\n';
debug('OOM Context: %s', context);
// do not send mails for dev apps
if ((!app || !app.debugMode) && (now - lastOomMailTime > OOM_MAIL_LIMIT)) {
mailer.oomEvent(program, context); // app can be null if it's an addon crash
lastOomMailTime = now;
}
});
});
stream.on('error', function (error) {
console.error('Error reading docker events', error);
gDockerEventStream = null; // TODO: reconnect?
});
stream.on('end', function () {
console.error('Docker event stream ended');
gDockerEventStream = null; // TODO: reconnect?
});
});
}
function start(callback) {
assert.strictEqual(typeof callback, 'function');
debug('Starting apphealthmonitor');
processDockerEvents();
run();
callback();
}
function stop(callback) {
assert.strictEqual(typeof callback, 'function');
clearTimeout(gRunTimeout);
if (gDockerEventStream) gDockerEventStream.end();
callback();
}
+134 -222
View File
@@ -38,7 +38,6 @@ exports = module.exports = {
configureInstalledApps: configureInstalledApps,
getAppConfig: getAppConfig,
getDataDir: getDataDir,
downloadFile: downloadFile,
uploadFile: uploadFile,
@@ -73,6 +72,7 @@ var appdb = require('./appdb.js'),
eventlog = require('./eventlog.js'),
fs = require('fs'),
mail = require('./mail.js'),
mailboxdb = require('./mailboxdb.js'),
manifestFormat = require('cloudron-manifestformat'),
os = require('os'),
path = require('path'),
@@ -154,8 +154,7 @@ function validatePortBindings(portBindings, manifest) {
config.get('ldapPort'), /* ldap server (lo) */
3306, /* mysql (lo) */
4190, /* managesieve */
8000, /* ESXi monitoring */
8417, /* graphite (lo) */
8000, /* graphite (lo) */
];
if (!portBindings) return null;
@@ -292,60 +291,26 @@ function validateBackupFormat(format) {
return new AppsError(AppsError.BAD_FIELD, 'Invalid backup format');
}
function validateEnv(env) {
for (let key in env) {
if (key.length > 512) return new AppsError(AppsError.BAD_FIELD, 'Max env var key length is 512');
// http://pubs.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap08.html
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) return new AppsError(AppsError.BAD_FIELD, `"${key}" is not a valid environment variable`);
}
return null;
}
function validateDataDir(dataDir) {
if (dataDir === '') return null; // revert back to default dataDir
if (path.resolve(dataDir) !== dataDir) return new AppsError(AppsError.BAD_FIELD, 'dataDir must be an absolute path');
// nfs shares will have the directory mounted already
let stat = safe.fs.lstatSync(dataDir);
if (stat) {
if (!stat.isDirectory()) return new AppsError(AppsError.BAD_FIELD, `dataDir ${dataDir} is not a directory`);
let entries = safe.fs.readdirSync(dataDir);
if (!entries) return new AppsError(AppsError.BAD_FIELD, `dataDir ${dataDir} could not be listed`);
if (entries.length !== 0) return new AppsError(AppsError.BAD_FIELD, `dataDir ${dataDir} is not empty`);
}
// backup logic relies on paths not overlapping (because it recurses)
if (dataDir.startsWith(paths.APPS_DATA_DIR)) return new AppsError(AppsError.BAD_FIELD, `dataDir ${dataDir} cannot be inside apps data`);
// if we made it this far, it cannot start with any of these realistically
const fhs = [ '/bin', '/boot', '/etc', '/lib', '/lib32', '/lib64', '/proc', '/run', '/sbin', '/tmp', '/usr' ];
if (fhs.some((p) => dataDir.startsWith(p))) return new AppsError(AppsError.BAD_FIELD, `dataDir ${dataDir} cannot be placed inside this location`);
return null;
}
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 '(.*)'/);
var match = error.message.match(/ER_DUP_ENTRY: Duplicate entry '(.*)' for key/);
if (!match) {
debug('Unexpected SQL error message.', error);
return new AppsError(AppsError.INTERNAL_ERROR, error);
return new AppsError(AppsError.INTERNAL_ERROR);
}
// check if the location conflicts
if (match[1] === location) return new AppsError(AppsError.ALREADY_EXISTS, `${match[2]} '${match[1]}' is in use`);
if (match[1] === location) return new AppsError(AppsError.ALREADY_EXISTS);
// check if any of the port bindings conflict
for (let portName in portBindings) {
if (portBindings[portName] === parseInt(match[1])) return new AppsError(AppsError.PORT_CONFLICT, match[1]);
}
return new AppsError(AppsError.ALREADY_EXISTS, `${match[2]} '${match[1]}' is in use`);
return new AppsError(AppsError.ALREADY_EXISTS);
}
// app configs that is useful for 'archival' into the app backup config.json
@@ -354,33 +319,25 @@ function getAppConfig(app) {
manifest: app.manifest,
location: app.location,
domain: app.domain,
fqdn: app.fqdn,
accessRestriction: app.accessRestriction,
portBindings: app.portBindings,
memoryLimit: app.memoryLimit,
xFrameOptions: app.xFrameOptions || 'SAMEORIGIN',
robotsTxt: app.robotsTxt,
sso: app.sso,
alternateDomains: app.alternateDomains || [],
env: app.env,
dataDir: app.dataDir
alternateDomains: app.alternateDomains || []
};
}
function getDataDir(app, dataDir) {
return dataDir || path.join(paths.APPS_DATA_DIR, app.id, 'data');
}
function removeInternalFields(app) {
return _.pick(app,
'id', 'appStoreId', 'installationState', 'installationProgress', 'runState', 'health',
'location', 'domain', 'fqdn', 'mailboxName',
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'xFrameOptions',
'sso', 'debugMode', 'robotsTxt', 'enableBackup', 'creationTime', 'updateTime', 'ts',
'alternateDomains', 'ownerId', 'env', 'enableAutomaticUpdate', 'dataDir');
'alternateDomains', 'ownerId');
}
// non-admins can only see these
function removeRestrictedFields(app) {
return _.pick(app,
'id', 'appStoreId', 'installationState', 'installationProgress', 'runState', 'health', 'ownerId',
@@ -415,23 +372,25 @@ function get(appId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
domaindb.getAll(function (error, domainObjects) {
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));
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
postProcess(app);
domaindb.get(app.domain, function (error, domainObject) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
postProcess(app);
let domainObjectMap = {};
for (let d of domainObjects) { domainObjectMap[d.domain] = d; }
app.iconUrl = getIconUrlSync(app);
app.fqdn = domains.fqdn(app.location, domainObjectMap[app.domain]);
app.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
app.fqdn = domains.fqdn(app.location, domainObject);
callback(null, app);
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (!error) app.mailboxName = mailboxes[0].name;
callback(null, app);
});
});
});
}
@@ -440,30 +399,25 @@ function getByIpAddress(ip, callback) {
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
domaindb.getAll(function (error, domainObjects) {
docker.getContainerIdByIp(ip, function (error, containerId) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
let domainObjectMap = {};
for (let d of domainObjects) { domainObjectMap[d.domain] = d; }
docker.getContainerIdByIp(ip, function (error, containerId) {
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));
appdb.getByContainerId(containerId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
postProcess(app);
domaindb.get(app.domain, function (error, domainObject) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
postProcess(app);
app.iconUrl = getIconUrlSync(app);
app.fqdn = domains.fqdn(app.location, domainObject);
domaindb.getAll(function (error, domainObjects) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
let domainObjectMap = {};
for (let d of domainObjects) { domainObjectMap[d.domain] = d; }
app.iconUrl = getIconUrlSync(app);
app.fqdn = domains.fqdn(app.location, domainObjectMap[app.domain]);
app.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
if (!error) app.mailboxName = mailboxes[0].name;
callback(null, app);
});
@@ -475,28 +429,30 @@ function getByIpAddress(ip, callback) {
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
domaindb.getAll(function (error, domainObjects) {
appdb.getAll(function (error, apps) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
let domainObjectMap = {};
for (let d of domainObjects) { domainObjectMap[d.domain] = d; }
apps.forEach(postProcess);
appdb.getAll(function (error, apps) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
async.eachSeries(apps, function (app, iteratorDone) {
domaindb.get(app.domain, function (error, domainObject) {
if (error) return iteratorDone(new AppsError(AppsError.INTERNAL_ERROR, error));
apps.forEach(postProcess);
async.eachSeries(apps, function (app, iteratorDone) {
app.iconUrl = getIconUrlSync(app);
app.fqdn = domains.fqdn(app.location, domainObjectMap[app.domain]);
app.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
app.fqdn = domains.fqdn(app.location, domainObject);
iteratorDone(null, app);
}, function (error) {
if (error) return callback(error);
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
callback(null, apps);
if (!error) app.mailboxName = mailboxes[0].name;
iteratorDone(null, app);
});
});
}, function (error) {
if (error) return callback(error);
callback(null, apps);
});
});
}
@@ -557,13 +513,10 @@ function install(data, user, auditSource, callback) {
debugMode = data.debugMode || null,
robotsTxt = data.robotsTxt || null,
enableBackup = 'enableBackup' in data ? data.enableBackup : true,
enableAutomaticUpdate = 'enableAutomaticUpdate' in data ? data.enableAutomaticUpdate : true,
backupId = data.backupId || null,
backupFormat = data.backupFormat || 'tgz',
ownerId = data.ownerId,
alternateDomains = data.alternateDomains || [],
env = data.env || {},
mailboxName = data.mailboxName || '';
alternateDomains = data.alternateDomains || [];
assert(data.appStoreId || data.manifest); // atleast one of them is required
@@ -601,16 +554,6 @@ function install(data, user, auditSource, callback) {
// if sso was unspecified, enable it by default if possible
if (sso === null) sso = !!manifest.addons['ldap'] || !!manifest.addons['oauth'];
error = validateEnv(env);
if (error) return callback(error);
if (mailboxName) {
error = mail.validateName(mailboxName);
if (error) return callback(error);
} else {
mailboxName = mailboxNameForLocation(location, manifest);
}
var appId = uuid.v4();
if (icon) {
@@ -632,7 +575,8 @@ function install(data, user, auditSource, callback) {
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Bad location: ' + error.message));
if (cert && key) {
error = reverseProxy.validateCertificate(location, domainObject, { cert, key });
let fqdn = domains.fqdn(location, domain, domainObject);
error = reverseProxy.validateCertificate(fqdn, cert, key);
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
}
@@ -644,13 +588,11 @@ function install(data, user, auditSource, callback) {
xFrameOptions: xFrameOptions,
sso: sso,
debugMode: debugMode,
mailboxName: mailboxName,
mailboxName: mailboxNameForLocation(location, manifest),
restoreConfig: backupId ? { backupId: backupId, backupFormat: backupFormat } : null,
enableBackup: enableBackup,
enableAutomaticUpdate: enableAutomaticUpdate,
robotsTxt: robotsTxt,
alternateDomains: alternateDomains,
env: env
alternateDomains: alternateDomains
};
appdb.add(appId, appStoreId, manifest, location, domain, ownerId, translatePortBindings(portBindings, manifest), data, function (error) {
@@ -658,11 +600,11 @@ function install(data, user, auditSource, callback) {
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));
appstore.purchase(appId, { appstoreId: appStoreId, manifestId: manifest.id }, function (appstoreError) {
appstore.purchase(appId, appStoreId, function (appstoreError) {
// if purchase failed, rollback the appdb record
if (appstoreError) {
appdb.del(appId, function (error) {
if (error) debug('install: Failed to rollback app installation.', error);
if (error) console.error('Failed to rollback app installation.', error);
if (appstoreError.reason === AppstoreError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, appstoreError.message));
if (appstoreError && appstoreError.reason === AppstoreError.BILLING_REQUIRED) return callback(new AppsError(AppsError.BILLING_REQUIRED, appstoreError.message));
@@ -676,8 +618,9 @@ function install(data, user, auditSource, callback) {
// save cert to boxdata/certs
if (cert && key) {
let error = reverseProxy.setAppCertificateSync(location, domainObject, { cert, key });
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error setting cert: ' + error.message));
let fqdn = domains.fqdn(location, domainObject);
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, fqdn + '.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, fqdn + '.user.key'), key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
}
taskmanager.restartAppTask(appId);
@@ -706,14 +649,12 @@ function configure(appId, data, user, auditSource, callback) {
get(appId, function (error, app) {
if (error) return callback(error);
let domain, location, portBindings, values = { };
if ('location' in data && 'domain' in data) {
location = values.location = data.location.toLowerCase();
domain = values.domain = data.domain.toLowerCase();
} else {
location = app.location;
domain = app.domain;
}
var domain, location, portBindings, values = { };
if ('location' in data) location = values.location = data.location.toLowerCase();
else location = app.location;
if ('domain' in data) domain = values.domain = data.domain.toLowerCase();
else domain = app.domain;
if ('accessRestriction' in data) {
values.accessRestriction = data.accessRestriction;
@@ -755,15 +696,8 @@ function configure(appId, data, user, auditSource, callback) {
}
if ('mailboxName' in data) {
if (data.mailboxName) {
error = mail.validateName(data.mailboxName);
if (error) return callback(error);
values.mailboxName = data.mailboxName;
} else {
values.mailboxName = mailboxNameForLocation(location, app.manifest);
}
} else { // keep existing name or follow the new location
values.mailboxName = app.mailboxName.endsWith('.app') ? mailboxNameForLocation(location, app.manifest) : app.mailboxName;
error = mail.validateName(data.mailboxName);
if (error) return callback(error);
}
if ('alternateDomains' in data) {
@@ -772,18 +706,6 @@ function configure(appId, data, user, auditSource, callback) {
values.alternateDomains.forEach(function (ad) { ad.subdomain = addSpacesSuffix(ad.subdomain, user); }); // TODO: validate these
}
if ('env' in data) {
values.env = data.env;
error = validateEnv(data.env);
if (error) return callback(error);
}
if ('dataDir' in data && data.dataDir !== app.dataDir) {
error = validateDataDir(data.dataDir);
if (error) return callback(error);
values.dataDir = data.dataDir;
}
domains.get(domain, function (error, domainObject) {
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
@@ -795,36 +717,51 @@ function configure(appId, data, user, auditSource, callback) {
// save cert to boxdata/certs. TODO: move this to apptask when we have a real task queue
if ('cert' in data && 'key' in data) {
if (data.cert && data.key) {
error = reverseProxy.validateCertificate(location, domainObject, { cert: data.cert, key: data.key });
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
}
let fqdn = domains.fqdn(location, domainObject);
error = reverseProxy.setAppCertificateSync(location, domainObject, { cert: data.cert, key: data.key });
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error setting cert: ' + error.message));
if (data.cert && data.key) {
error = reverseProxy.validateCertificate(fqdn, data.cert, data.key);
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.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, `${fqdn}.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, `${fqdn}.user.cert`))) debug('Error removing cert: ' + safe.error.message);
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.key`))) debug('Error removing key: ' + safe.error.message);
}
}
if ('enableBackup' in data) values.enableBackup = data.enableBackup;
if ('enableAutomaticUpdate' in data) values.enableAutomaticUpdate = data.enableAutomaticUpdate;
values.oldConfig = getAppConfig(app);
debug('Will configure app with id:%s values:%j', appId, values);
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_CONFIGURE, values, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
// make the mailbox name follow the apps new location, if the user did not set it explicitly
var oldName = app.mailboxName;
var newName = data.mailboxName || (app.mailboxName.endsWith('.app') ? mailboxNameForLocation(location, app.manifest) : app.mailboxName);
mailboxdb.updateName(oldName, values.oldConfig.domain, newName, domain, function (error) {
if (newName.endsWith('.app')) error = null; // ignore internal mailbox conflict errors since we want to show location conflict errors in the UI
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));
taskmanager.restartAppTask(appId);
// fetch fresh app object for eventlog
get(appId, function (error, result) {
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));
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: result });
taskmanager.restartAppTask(appId);
callback(null);
// fetch fresh app object for eventlog
get(appId, function (error, result) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: result });
callback(null);
});
});
});
});
@@ -839,22 +776,34 @@ function update(appId, data, auditSource, callback) {
debug('Will update app with id:%s', appId);
get(appId, function (error, app) {
downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) {
if (error) return callback(error);
downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) {
var updateConfig = { };
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);
updateConfig.manifest = manifest;
if ('icon' in data) {
if (data.icon) {
if (!validator.isBase64(data.icon)) return callback(new AppsError(AppsError.BAD_FIELD, 'icon is not base64'));
if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.png'), new Buffer(data.icon, 'base64'))) {
return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving icon:' + safe.error.message));
}
} else {
safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, appId + '.png'));
}
}
get(appId, function (error, app) {
if (error) return callback(error);
var updateConfig = { };
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);
updateConfig.manifest = manifest;
// prevent user from installing a app with different manifest id over an existing app
// this allows cloudron install -f --app <appid> for an app installed from the appStore
if (app.manifest.id !== updateConfig.manifest.id) {
@@ -863,25 +812,6 @@ function update(appId, data, auditSource, callback) {
updateConfig.appStoreId = '';
}
// suffix '0' if prerelease is missing for semver.lte to work as expected
const currentVersion = semver.prerelease(app.manifest.version) ? app.manifest.version : `${app.manifest.version}-0`;
const updateVersion = semver.prerelease(updateConfig.manifest.version) ? updateConfig.manifest.version : `${updateConfig.manifest.version}-0`;
if (app.appStoreId !== '' && semver.lte(updateVersion, currentVersion)) {
if (!data.force) return callback(new AppsError(AppsError.BAD_FIELD, 'Downgrades are not permitted for apps installed from AppStore. force to override'));
}
if ('icon' in data) {
if (data.icon) {
if (!validator.isBase64(data.icon)) return callback(new AppsError(AppsError.BAD_FIELD, 'icon is not base64'));
if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.png'), new Buffer(data.icon, 'base64'))) {
return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving icon:' + safe.error.message));
}
} else {
safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, appId + '.png'));
}
}
// 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'));
@@ -913,26 +843,22 @@ function getLogs(appId, options, callback) {
assert(options && typeof options === 'object');
assert.strictEqual(typeof callback, 'function');
assert.strictEqual(typeof options.lines, 'number');
assert.strictEqual(typeof options.format, 'string');
assert.strictEqual(typeof options.follow, 'boolean');
debug('Getting logs for %s', appId);
get(appId, function (error, app) {
get(appId, function (error /*, app */) {
if (error) return callback(error);
var lines = options.lines === -1 ? '+1' : options.lines,
var lines = options.lines || 100,
format = options.format || 'json',
follow = options.follow;
follow = !!options.follow;
assert.strictEqual(typeof lines, 'number');
assert.strictEqual(typeof format, 'string');
var args = [ '--lines=' + lines ];
if (follow) args.push('--follow', '--retry', '--quiet'); // same as -F. to make it work if file doesn't exist, --quiet to not output file headers, which are no logs
args.push(path.join(paths.LOG_DIR, appId, 'apptask.log'));
args.push(path.join(paths.LOG_DIR, appId, 'app.log'));
if (app.manifest.addons && app.manifest.addons.redis) args.push(path.join(paths.LOG_DIR, `redis-${appId}/app.log`));
var cp = spawn('/usr/bin/tail', args);
@@ -1000,7 +926,7 @@ function restore(appId, data, auditSource, callback) {
taskmanager.restartAppTask(appId);
eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { app: app, backupId: backupInfo.id, fromManifest: app.manifest, toManifest: backupInfo.manifest });
eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { appId: appId, app: app });
callback(null);
});
@@ -1021,8 +947,7 @@ function clone(appId, data, user, auditSource, callback) {
domain = data.domain.toLowerCase(),
portBindings = data.portBindings || null,
backupId = data.backupId,
ownerId = data.ownerId,
mailboxName = data.mailboxName || '';
ownerId = data.ownerId;
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof location, 'string');
@@ -1040,22 +965,13 @@ function clone(appId, data, user, auditSource, callback) {
if (!backupInfo.manifest) callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Could not get restore config'));
const manifest = backupInfo.manifest;
// re-validate because this new box version may not accept old configs
error = checkManifestConstraints(manifest);
error = checkManifestConstraints(backupInfo.manifest);
if (error) return callback(error);
error = validatePortBindings(portBindings, manifest);
error = validatePortBindings(portBindings, backupInfo.manifest);
if (error) return callback(error);
if (mailboxName) {
error = mail.validateName(mailboxName);
if (error) return callback(error);
} else {
mailboxName = mailboxNameForLocation(location, manifest);
}
domains.get(domain, function (error, domainObject) {
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new AppsError(AppsError.EXTERNAL_ERROR, 'No such domain'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
@@ -1064,7 +980,7 @@ function clone(appId, data, user, auditSource, callback) {
error = domains.validateHostname(location, domainObject);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Bad location: ' + error.message));
var newAppId = uuid.v4();
var newAppId = uuid.v4(), manifest = backupInfo.manifest;
var data = {
installationState: appdb.ISTATE_PENDING_CLONE,
@@ -1073,21 +989,20 @@ function clone(appId, data, user, auditSource, callback) {
xFrameOptions: app.xFrameOptions,
restoreConfig: { backupId: backupId, backupFormat: backupInfo.format },
sso: !!app.sso,
mailboxName: mailboxName,
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app',
enableBackup: app.enableBackup,
robotsTxt: app.robotsTxt,
env: app.env
robotsTxt: app.robotsTxt
};
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(location, portBindings, error));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
appstore.purchase(newAppId, { appstoreId: app.appStoreId, manifestId: manifest.id }, function (appstoreError) {
appstore.purchase(newAppId, app.appStoreId, function (appstoreError) {
// if purchase failed, rollback the appdb record
if (appstoreError) {
appdb.del(newAppId, function (error) {
if (error) debug('install: Failed to rollback app installation.', error);
if (error) console.error('Failed to rollback app installation.', error);
if (appstoreError.reason === AppstoreError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, appstoreError.message));
if (appstoreError && appstoreError.reason === AppstoreError.BILLING_REQUIRED) return callback(new AppsError(AppsError.BILLING_REQUIRED, appstoreError.message));
@@ -1126,7 +1041,7 @@ function uninstall(appId, auditSource, callback) {
get(appId, function (error, app) {
if (error) return callback(error);
appstore.unpurchase(appId, { appstoreId: app.appStoreId, manifestId: app.manifest.id }, function (error) {
appstore.unpurchase(appId, app.appStoreId, function (error) {
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));
@@ -1224,8 +1139,6 @@ function exec(appId, options, callback) {
};
container.exec(execOptions, function (error, exec) {
if (error && error.statusCode === 409) return callback(new AppsError(AppsError.BAD_STATE, error.message)); // container restarting/not running
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
var startOptions = {
Detach: false,
@@ -1264,7 +1177,6 @@ function autoupdateApps(updateInfo, auditSource, callback) { // updateInfo is {
assert.strictEqual(typeof callback, 'function');
function canAutoupdateApp(app, newManifest) {
if (!app.enableAutomaticUpdate) return new Error('Automatic update disabled');
if ((semver.major(app.manifest.version) !== 0) && (semver.major(app.manifest.version) !== semver.major(newManifest.version))) return new Error('Major version change'); // major changes are blocking
const newTcpPorts = newManifest.tcpPorts || { };
+12 -8
View File
@@ -19,7 +19,8 @@ exports = module.exports = {
AppstoreError: AppstoreError
};
var apps = require('./apps.js'),
var appdb = require('./appdb.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
config = require('./config.js'),
@@ -95,16 +96,18 @@ function isFreePlan(subscription) {
}
// See app.js install it will create a db record first but remove it again if appstore purchase fails
function purchase(appId, data, callback) {
function purchase(appId, appstoreId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof data, 'object');
assert(data.appstoreId || data.manifestId);
assert.strictEqual(typeof appstoreId, 'string');
assert.strictEqual(typeof callback, 'function');
if (appstoreId === '') return callback(null);
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/apps/' + appId;
var data = { appstoreId: appstoreId };
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
@@ -118,12 +121,13 @@ function purchase(appId, data, callback) {
});
}
function unpurchase(appId, data, callback) {
function unpurchase(appId, appstoreId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof data, 'object');
assert(data.appstoreId || data.manifestId);
assert.strictEqual(typeof appstoreId, 'string');
assert.strictEqual(typeof callback, 'function');
if (appstoreId === '') return callback(null);
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
@@ -135,7 +139,7 @@ function unpurchase(appId, data, callback) {
if (result.statusCode === 404) return callback(null); // was never purchased
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
superagent.del(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
superagent.del(url).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
if (result.statusCode !== 204) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
+60 -112
View File
@@ -10,8 +10,8 @@ exports = module.exports = {
_reserveHttpPort: reserveHttpPort,
_configureReverseProxy: configureReverseProxy,
_unconfigureReverseProxy: unconfigureReverseProxy,
_createAppDir: createAppDir,
_deleteAppDir: deleteAppDir,
_createVolume: createVolume,
_deleteVolume: deleteVolume,
_verifyManifest: verifyManifest,
_registerSubdomain: registerSubdomain,
_unregisterSubdomain: unregisterSubdomain,
@@ -36,7 +36,6 @@ var addons = require('./addons.js'),
ejs = require('ejs'),
fs = require('fs'),
manifestFormat = require('cloudron-manifestformat'),
mkdirp = require('mkdirp'),
net = require('net'),
os = require('os'),
path = require('path'),
@@ -52,11 +51,10 @@ var addons = require('./addons.js'),
var COLLECTD_CONFIG_EJS = fs.readFileSync(__dirname + '/collectd.config.ejs', { encoding: 'utf8' }),
CONFIGURE_COLLECTD_CMD = path.join(__dirname, 'scripts/configurecollectd.sh'),
MV_VOLUME_CMD = path.join(__dirname, 'scripts/mvvolume.sh'),
LOGROTATE_CONFIG_EJS = fs.readFileSync(__dirname + '/logrotate.ejs', { encoding: 'utf8' }),
CONFIGURE_LOGROTATE_CMD = path.join(__dirname, 'scripts/configurelogrotate.sh');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
CONFIGURE_LOGROTATE_CMD = path.join(__dirname, 'scripts/configurelogrotate.sh'),
RMAPPDIR_CMD = path.join(__dirname, 'scripts/rmappdir.sh'),
CREATEAPPDIR_CMD = path.join(__dirname, 'scripts/createappdir.sh');
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -136,62 +134,46 @@ function createContainer(app, callback) {
});
}
function deleteContainers(app, options, callback) {
// Only delete the main container of the app, not destroy any docker addon created ones
function deleteMainContainer(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'deleting app containers (app, scheduler)');
debugApp(app, 'deleting main app container');
docker.deleteContainers(app.id, options, function (error) {
docker.deleteContainer(app.containerId, function (error) {
if (error) return callback(new Error('Error deleting container: ' + error));
updateApp(app, { containerId: null }, callback);
});
}
function createAppDir(app, callback) {
function deleteContainers(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
mkdirp(path.join(paths.APPS_DATA_DIR, app.id), callback);
debugApp(app, 'deleting app containers (app, scheduler)');
docker.deleteContainers(app.id, function (error) {
if (error) return callback(new Error('Error deleting container: ' + error));
updateApp(app, { containerId: null }, callback);
});
}
function deleteAppDir(app, options, callback) {
function createVolume(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
shell.sudo('createVolume', [ CREATEAPPDIR_CMD, app.id ], callback);
}
function deleteVolume(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
const appDataDir = path.join(paths.APPS_DATA_DIR, app.id);
// resolve any symlinked data dir
const stat = safe.fs.lstatSync(appDataDir);
if (!stat) return callback(null);
const resolvedAppDataDir = stat.isSymbolicLink() ? safe.fs.readlinkSync(appDataDir) : appDataDir;
if (safe.fs.existsSync(resolvedAppDataDir)) {
const entries = safe.fs.readdirSync(resolvedAppDataDir);
if (!entries) return callback(`Error listing ${resolvedAppDataDir}: ${safe.error.message}`);
// remove only files. directories inside app dir are currently volumes managed by the addons
// we cannot delete those dirs anyway because of perms
entries.forEach(function (entry) {
let stat = safe.fs.statSync(path.join(resolvedAppDataDir, entry));
if (stat && !stat.isDirectory()) safe.fs.unlinkSync(path.join(resolvedAppDataDir, entry));
});
}
// if this fails, it's probably because the localstorage/redis addons have not cleaned up properly
if (options.removeDirectory) {
if (stat.isSymbolicLink()) {
if (!safe.fs.unlinkSync(appDataDir)) return callback(safe.error.code === 'ENOENT' ? null : safe.error);
} else {
if (!safe.fs.rmdirSync(appDataDir)) return callback(safe.error.code === 'ENOENT' ? null : safe.error);
}
}
callback(null);
shell.sudo('deleteVolume', [ RMAPPDIR_CMD, app.id, !!options.removeDirectory ], callback);
}
function addCollectdProfile(app, callback) {
@@ -201,7 +183,7 @@ function addCollectdProfile(app, callback) {
var collectdConf = ejs.render(COLLECTD_CONFIG_EJS, { appId: app.id, containerId: app.containerId });
fs.writeFile(path.join(paths.COLLECTD_APPCONFIG_DIR, app.id + '.conf'), collectdConf, function (error) {
if (error) return callback(error);
shell.sudo('addCollectdProfile', [ CONFIGURE_COLLECTD_CMD, 'add', app.id ], {}, callback);
shell.sudo('addCollectdProfile', [ CONFIGURE_COLLECTD_CMD, 'add', app.id ], callback);
});
}
@@ -211,7 +193,7 @@ function removeCollectdProfile(app, callback) {
fs.unlink(path.join(paths.COLLECTD_APPCONFIG_DIR, app.id + '.conf'), function (error) {
if (error && error.code !== 'ENOENT') debugApp(app, 'Error removing collectd profile', error);
shell.sudo('removeCollectdProfile', [ CONFIGURE_COLLECTD_CMD, 'remove', app.id ], {}, callback);
shell.sudo('removeCollectdProfile', [ CONFIGURE_COLLECTD_CMD, 'remove', app.id ], callback);
});
}
@@ -230,7 +212,7 @@ function addLogrotateConfig(app, callback) {
var tmpFilePath = path.join(os.tmpdir(), app.id + '.logrotate');
fs.writeFile(tmpFilePath, logrotateConf, function (error) {
if (error) return callback(error);
shell.sudo('addLogrotateConfig', [ CONFIGURE_LOGROTATE_CMD, 'add', app.id, tmpFilePath ], {}, callback);
shell.sudo('addLogrotateConfig', [ CONFIGURE_LOGROTATE_CMD, 'add', app.id, tmpFilePath ], callback);
});
});
}
@@ -239,7 +221,7 @@ function removeLogrotateConfig(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
shell.sudo('removeLogrotateConfig', [ CONFIGURE_LOGROTATE_CMD, 'remove', app.id ], {}, callback);
shell.sudo('removeLogrotateConfig', [ CONFIGURE_LOGROTATE_CMD, 'remove', app.id ], callback);
}
function verifyManifest(manifest, callback) {
@@ -301,10 +283,7 @@ function registerSubdomain(app, overwrite, callback) {
if (values.length !== 0 && !overwrite) return retryCallback(null, new Error('DNS Record already exists'));
domains.upsertDnsRecords(app.location, app.domain, 'A', [ ip ], function (error) {
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) {
debug('Upsert error. Will retry.', error.message);
return retryCallback(error); // try again
}
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) return retryCallback(error); // try again
retryCallback(null, error);
});
@@ -361,10 +340,8 @@ function registerAlternateDomains(app, overwrite, callback) {
if (values.length !== 0 && !overwrite) return retryCallback(null, new Error('DNS Record already exists'));
domains.upsertDnsRecords(domain.subdomain, domain.domain, 'A', [ ip ], function (error) {
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) {
debug('Upsert error. Will retry.', error.message);
return retryCallback(error); // try again
}
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) return retryCallback(error); // try again
retryCallback(null, error);
});
});
@@ -381,14 +358,9 @@ function unregisterAlternateDomains(app, all, callback) {
assert.strictEqual(typeof all, 'boolean');
assert.strictEqual(typeof callback, 'function');
let obsoleteDomains = [];
if (all) {
obsoleteDomains = app.alternateDomains;
} else if (app.oldConfig) { // oldConfig can be null during an infra update
obsoleteDomains = app.oldConfig.alternateDomains.filter(function (o) {
return !app.alternateDomains.some(function (n) { return n.subdomain === o.subdomain && n.domain === o.domain; });
});
}
var obsoleteDomains
if (all) obsoleteDomains = app.alternateDomains;
else obsoleteDomains = app.oldConfig.alternateDomains.filter(function (o) { return !app.alternateDomains.some(function (n) { return n.subdomain === o.subdomain && n.domain === o.domain; }); });
if (obsoleteDomains.length === 0) return callback();
@@ -427,10 +399,8 @@ function cleanupLogs(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
// note that redis container logs are cleaned up by the addon
rimraf(path.join(paths.LOG_DIR, app.id), function (error) {
if (error) debugApp(app, 'cannot cleanup logs: %s', error);
callback(null);
});
}
@@ -447,30 +417,18 @@ function waitForDnsPropagation(app, callback) {
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
domains.waitForDnsRecord(app.location, app.domain, 'A', ip, { interval: 5000, times: 240 }, function (error) {
domains.waitForDnsRecord(app.fqdn, app.domain, ip, { interval: 5000, times: 240 }, function (error) {
if (error) return callback(error);
// now wait for alternateDomains, if any
async.eachSeries(app.alternateDomains, function (domain, iteratorCallback) {
domains.waitForDnsRecord(domain.subdomain, domain.domain, 'A', ip, { interval: 5000, times: 240 }, iteratorCallback);
async.eachSeries(app.alternateDomains, function (domain, callback) {
var fqdn = (domain.subdomain ? (domain.subdomain + '.') : '') + domain.domain;
domains.waitForDnsRecord(fqdn, domain.domain, ip, { interval: 5000, times: 240 }, callback);
}, callback);
});
});
}
function migrateDataDir(app, sourceDir, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof sourceDir, 'string');
assert.strictEqual(typeof callback, 'function');
let resolvedSourceDir = apps.getDataDir(app, sourceDir);
let resolvedTargetDir = apps.getDataDir(app, app.dataDir);
debug(`migrateDataDir: migrating data from ${resolvedSourceDir} to ${resolvedTargetDir}`);
shell.sudo('migrateDataDir', [ MV_VOLUME_CMD, resolvedSourceDir, resolvedTargetDir ], {}, callback);
}
// Ordering is based on the following rationale:
// - configure nginx, icon, oauth
// - register subdomain.
@@ -499,14 +457,16 @@ function install(app, callback) {
removeCollectdProfile.bind(null, app),
removeLogrotateConfig.bind(null, app),
stopApp.bind(null, app),
deleteContainers.bind(null, app, { managedOnly: true }),
deleteMainContainer.bind(null, app),
function teardownAddons(next) {
// when restoring, app does not require these addons anymore. remove carefully to preserve the db passwords
var addonsToRemove = !isRestoring ? app.manifest.addons : _.omit(app.oldConfig.manifest.addons, Object.keys(app.manifest.addons));
var addonsToRemove = !isRestoring
? app.manifest.addons
: _.omit(app.oldConfig.manifest.addons, Object.keys(app.manifest.addons));
addons.teardownAddons(app, addonsToRemove, next);
},
deleteAppDir.bind(null, app, { removeDirectory: false }), // do not remove any symlinked appdata dir
deleteVolume.bind(null, app, { removeDirectory: false }), // do not remove any symlinked volume
// for restore case
function deleteImageIfChanged(done) {
@@ -523,14 +483,11 @@ function install(app, callback) {
updateApp.bind(null, app, { installationProgress: '30, Registering subdomain' }),
registerSubdomain.bind(null, app, isRestoring /* overwrite */),
updateApp.bind(null, app, { installationProgress: '35, Registering alternate domains'}),
registerAlternateDomains.bind(null, app, isRestoring /* overwrite */),
updateApp.bind(null, app, { installationProgress: '40, Downloading image' }),
docker.downloadImage.bind(null, app.manifest),
updateApp.bind(null, app, { installationProgress: '50, Creating app data directory' }),
createAppDir.bind(null, app),
updateApp.bind(null, app, { installationProgress: '50, Creating volume' }),
createVolume.bind(null, app),
function restoreFromBackup(next) {
if (!restoreConfig) {
@@ -540,10 +497,8 @@ function install(app, callback) {
], next);
} else {
async.series([
updateApp.bind(null, app, { installationProgress: '65, Download backup and restoring addons' }),
addons.setupAddons.bind(null, app, app.manifest.addons),
addons.clearAddons.bind(null, app, app.manifest.addons),
backups.restoreApp.bind(null, app, app.manifest.addons, restoreConfig, (progress) => updateApp(app, { installationProgress: `65, Restore - ${progress.message}` }, NOOP_CALLBACK))
updateApp.bind(null, app, { installationProgress: '60, Download backup and restoring addons' }),
backups.restoreApp.bind(null, app, app.manifest.addons, restoreConfig),
], next);
}
},
@@ -585,7 +540,7 @@ function backup(app, callback) {
async.series([
updateApp.bind(null, app, { installationProgress: '10, Backing up' }),
backups.backupApp.bind(null, app, (progress) => updateApp(app, { installationProgress: `30, ${progress.message}` }, NOOP_CALLBACK)),
backups.backupApp.bind(null, app),
// done!
function (callback) {
@@ -607,8 +562,7 @@ function configure(app, callback) {
assert.strictEqual(typeof callback, 'function');
// oldConfig can be null during an infra update
const locationChanged = app.oldConfig && (app.oldConfig.fqdn !== app.fqdn);
const dataDirChanged = app.oldConfig && (app.oldConfig.dataDir !== app.dataDir);
var locationChanged = app.oldConfig && (app.oldConfig.fqdn !== app.fqdn);
async.series([
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
@@ -616,7 +570,7 @@ function configure(app, callback) {
removeCollectdProfile.bind(null, app),
removeLogrotateConfig.bind(null, app),
stopApp.bind(null, app),
deleteContainers.bind(null, app, { managedOnly: true }),
deleteMainContainer.bind(null, app),
unregisterAlternateDomains.bind(null, app, false /* all */),
function (next) {
if (!locationChanged) return next();
@@ -624,6 +578,7 @@ function configure(app, callback) {
unregisterSubdomain(app, app.oldConfig.location, app.oldConfig.domain, next);
},
reserveHttpPort.bind(null, app),
updateApp.bind(null, app, { installationProgress: '20, Downloading icon' }),
@@ -638,20 +593,13 @@ function configure(app, callback) {
updateApp.bind(null, app, { installationProgress: '40, Downloading image' }),
docker.downloadImage.bind(null, app.manifest),
updateApp.bind(null, app, { installationProgress: '45, Ensuring app data directory' }),
createAppDir.bind(null, app),
updateApp.bind(null, app, { installationProgress: '45, Ensuring volume' }),
createVolume.bind(null, app),
// re-setup addons since they rely on the app's fqdn (e.g oauth)
updateApp.bind(null, app, { installationProgress: '50, Setting up addons' }),
addons.setupAddons.bind(null, app, app.manifest.addons),
// migrate dataDir
function (next) {
if (!dataDirChanged) return next();
migrateDataDir(app, app.oldConfig.dataDir, next);
},
updateApp.bind(null, app, { installationProgress: '60, Creating container' }),
createContainer.bind(null, app),
@@ -705,7 +653,7 @@ function update(app, callback) {
async.series([
updateApp.bind(null, app, { installationProgress: '15, Backing up app' }),
backups.backupApp.bind(null, app, (progress) => updateApp(app, { installationProgress: `15, Backup - ${progress.message}` }, NOOP_CALLBACK))
backups.backupApp.bind(null, app)
], function (error) {
if (error) error.backupError = true;
next(error);
@@ -723,7 +671,7 @@ function update(app, callback) {
removeCollectdProfile.bind(null, app),
removeLogrotateConfig.bind(null, app),
stopApp.bind(null, app),
deleteContainers.bind(null, app, { managedOnly: true }),
deleteMainContainer.bind(null, app),
function deleteImageIfChanged(done) {
if (app.manifest.dockerImage === app.updateConfig.manifest.dockerImage) return done();
@@ -809,13 +757,13 @@ function uninstall(app, callback) {
stopApp.bind(null, app),
updateApp.bind(null, app, { installationProgress: '20, Deleting container' }),
deleteContainers.bind(null, app, {}),
deleteContainers.bind(null, app),
updateApp.bind(null, app, { installationProgress: '30, Teardown addons' }),
addons.teardownAddons.bind(null, app, app.manifest.addons),
updateApp.bind(null, app, { installationProgress: '40, Deleting app data directory' }),
deleteAppDir.bind(null, app, { removeDirectory: true }),
updateApp.bind(null, app, { installationProgress: '40, Deleting volume' }),
deleteVolume.bind(null, app, { removeDirectory: true }),
updateApp.bind(null, app, { installationProgress: '50, Deleting image' }),
docker.deleteImage.bind(null, app.manifest),
+250 -323
View File
File diff suppressed because it is too large Load Diff
@@ -9,10 +9,17 @@ if (process.argv[2] === '--check') return console.log('OK');
require('supererror')({ splatchError: true });
// remove timestamp from debug() based output
require('debug').formatArgs = function formatArgs(args) {
args[0] = this.namespace + ' ' + args[0];
};
var assert = require('assert'),
backups = require('../backups.js'),
database = require('../database.js'),
debug = require('debug')('box:backupupload');
backups = require('./backups.js'),
database = require('./database.js'),
debug = require('debug')('box:backuptask'),
paths = require('./paths.js'),
safe = require('safetydance');
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -21,31 +28,27 @@ function initialize(callback) {
}
// Main process starts here
const backupId = process.argv[2];
const format = process.argv[3];
const dataLayoutString = process.argv[4];
var backupId = process.argv[2];
var format = process.argv[3];
var dataDir = process.argv[4];
debug(`Backing up ${dataLayoutString} to ${backupId}`);
debug(`Backing up ${dataDir} to ${backupId}`);
process.on('SIGTERM', function () {
process.exit(0);
});
// this can happen when the backup task is terminated (not box code)
process.on('disconnect', function () {
debug('parent process died');
process.exit(0);
});
initialize(function (error) {
if (error) throw error;
backups.upload(backupId, format, dataLayoutString, (progress) => process.send(progress), function resultHandler(error) {
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, '');
backups.upload(backupId, format, dataDir, function resultHandler(error) {
if (error) debug('upload completed with error', error);
debug('upload completed');
process.send({ result: error ? error.message : '' });
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, error ? error.message : '');
// https://nodejs.org/api/process.html are exit codes used by node. apps.js uses the value below
// to check apptask crashes
+137 -1
View File
@@ -4,18 +4,29 @@ exports = module.exports = {
verifySetupToken: verifySetupToken,
setupDone: setupDone,
changePlan: changePlan,
upgrade: upgrade,
sendHeartbeat: sendHeartbeat,
getBoxAndUserDetails: getBoxAndUserDetails,
setPtrRecord: setPtrRecord,
CaasError: CaasError
};
var assert = require('assert'),
backups = require('./backups.js'),
config = require('./config.js'),
debug = require('debug')('box:caas'),
locker = require('./locker.js'),
path = require('path'),
progress = require('./progress.js'),
settings = require('./settings.js'),
shell = require('./shell.js'),
superagent = require('superagent'),
util = require('util');
util = require('util'),
_ = require('underscore');
const RETIRE_CMD = path.join(__dirname, 'scripts/retire.sh');
function CaasError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
@@ -42,6 +53,20 @@ CaasError.INVALID_TOKEN = 'Invalid Token';
CaasError.INTERNAL_ERROR = 'Internal Error';
CaasError.EXTERNAL_ERROR = 'External Error';
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function retire(reason, info, callback) {
assert(reason === 'migrate' || reason === 'upgrade');
info = info || { };
callback = callback || NOOP_CALLBACK;
var data = {
apiServerOrigin: config.apiServerOrigin(),
adminFqdn: config.adminFqdn()
};
shell.sudo('retire', [ RETIRE_CMD, reason, JSON.stringify(info), JSON.stringify(data) ], callback);
}
function getCaasConfig(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -92,6 +117,96 @@ function setupDone(setupToken, callback) {
});
});
}
function doMigrate(options, caasConfig, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof caasConfig, 'object');
assert.strictEqual(typeof callback, 'function');
var error = locker.lock(locker.OP_MIGRATE);
if (error) return callback(new CaasError(CaasError.BAD_STATE, error.message));
function unlock(error) {
debug('Failed to migrate', error);
locker.unlock(locker.OP_MIGRATE);
progress.set(progress.MIGRATE, -1, 'Backup failed: ' + error.message);
}
progress.set(progress.MIGRATE, 10, 'Backing up for migration');
// initiate the migration in the background
backups.backupBoxAndApps({ userId: null, username: 'migrator' }, function (error) {
if (error) return unlock(error);
debug('migrate: domain: %s size %s region %s', options.domain, options.size, options.region);
superagent
.post(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId + '/migrate')
.query({ token: caasConfig.token })
.send(options)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return unlock(error); // network error
if (result.statusCode === 409) return unlock(new CaasError(CaasError.BAD_STATE));
if (result.statusCode === 404) return unlock(new CaasError(CaasError.NOT_FOUND));
if (result.statusCode !== 202) return unlock(new CaasError(CaasError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
progress.set(progress.MIGRATE, 10, 'Migrating');
retire('migrate', _.pick(options, 'domain', 'size', 'region'));
});
});
callback(null);
}
function changePlan(options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
if (config.isDemo()) return callback(new CaasError(CaasError.BAD_FIELD, 'Not allowed in demo mode'));
getCaasConfig(function (error, result) {
if (error) return callback(error);
doMigrate(options, result, callback);
});
}
// this function expects a lock
function upgrade(boxUpdateInfo, callback) {
assert(boxUpdateInfo !== null && typeof boxUpdateInfo === 'object');
function upgradeError(e) {
progress.set(progress.UPDATE, -1, e.message);
callback(e);
}
progress.set(progress.UPDATE, 5, 'Backing up for upgrade');
backups.backupBoxAndApps({ userId: null, username: 'upgrader' }, function (error) {
if (error) return upgradeError(error);
getCaasConfig(function (error, result) {
if (error) return upgradeError(error);
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + result.boxId + '/upgrade')
.query({ token: result.token })
.send({ version: boxUpdateInfo.version })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return upgradeError(new Error('Network error making upgrade request: ' + error));
if (result.statusCode !== 202) return upgradeError(new Error(util.format('Server not ready to upgrade. statusCode: %s body: %j', result.status, result.body)));
progress.set(progress.UPDATE, 10, 'Updating base system');
// no need to unlock since this is the last thing we ever do on this box
callback();
retire('upgrade');
});
});
});
}
function sendHeartbeat() {
assert(config.provider() === 'caas', 'Heartbeat is only sent for managed cloudrons');
@@ -108,6 +223,27 @@ function sendHeartbeat() {
});
}
function getBoxAndUserDetails(callback) {
assert.strictEqual(typeof callback, 'function');
if (config.provider() !== 'caas') return callback(null, {});
getCaasConfig(function (error, caasConfig) {
if (error) return callback(error);
superagent
.get(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId)
.query({ token: caasConfig.token })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, 'Cannot reach appstore'));
if (result.statusCode !== 200) return callback(new CaasError(CaasError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
return callback(null, result.body);
});
});
}
function setPtrRecord(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
+479
View File
@@ -0,0 +1,479 @@
'use strict';
var assert = require('assert'),
async = require('async'),
crypto = require('crypto'),
debug = require('debug')('box:cert/acme'),
execSync = require('safetydance').child_process.execSync,
fs = require('fs'),
parseLinks = require('parse-links'),
path = require('path'),
paths = require('../paths.js'),
safe = require('safetydance'),
superagent = require('superagent'),
util = require('util'),
_ = require('underscore');
var CA_PROD = 'https://acme-v01.api.letsencrypt.org',
CA_STAGING = 'https://acme-staging.api.letsencrypt.org',
LE_AGREEMENT = 'https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf';
exports = module.exports = {
getCertificate: getCertificate,
// testing
_name: 'acme'
};
function AcmeError(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;
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(AcmeError, Error);
AcmeError.INTERNAL_ERROR = 'Internal Error';
AcmeError.EXTERNAL_ERROR = 'External Error';
AcmeError.ALREADY_EXISTS = 'Already Exists';
AcmeError.NOT_COMPLETED = 'Not Completed';
AcmeError.FORBIDDEN = 'Forbidden';
// http://jose.readthedocs.org/en/latest/
// https://www.ietf.org/proceedings/92/slides/slides-92-acme-1.pdf
// https://community.letsencrypt.org/t/list-of-client-implementations/2103
function Acme(options) {
assert.strictEqual(typeof options, 'object');
this.caOrigin = options.prod ? CA_PROD : CA_STAGING;
this.accountKeyPem = null; // Buffer
this.email = options.email;
}
Acme.prototype.getNonce = function (callback) {
superagent.get(this.caOrigin + '/directory').timeout(30 * 1000).end(function (error, response) {
if (error && !error.response) return callback(error);
if (response.statusCode !== 200) return callback(new Error('Invalid response code when fetching nonce : ' + response.statusCode));
return callback(null, response.headers['Replay-Nonce'.toLowerCase()]);
});
};
// urlsafe base64 encoding (jose)
function urlBase64Encode(string) {
return string.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
function b64(str) {
var buf = util.isBuffer(str) ? str : new Buffer(str);
return urlBase64Encode(buf.toString('base64'));
}
function getModulus(pem) {
assert(util.isBuffer(pem));
var stdout = execSync('openssl rsa -modulus -noout', { input: pem, encoding: 'utf8' });
if (!stdout) return null;
var match = stdout.match(/Modulus=([0-9a-fA-F]+)$/m);
if (!match) return null;
return Buffer.from(match[1], 'hex');
}
Acme.prototype.sendSignedRequest = function (url, payload, callback) {
assert.strictEqual(typeof url, 'string');
assert.strictEqual(typeof payload, 'string');
assert.strictEqual(typeof callback, 'function');
assert(util.isBuffer(this.accountKeyPem));
var that = this;
var header = {
alg: 'RS256',
jwk: {
e: b64(Buffer.from([0x01, 0x00, 0x01])), // exponent - 65537
kty: 'RSA',
n: b64(getModulus(this.accountKeyPem))
}
};
var payload64 = b64(payload);
this.getNonce(function (error, nonce) {
if (error) return callback(error);
debug('sendSignedRequest: using nonce %s for url %s', nonce, url);
var protected64 = b64(JSON.stringify(_.extend({ }, header, { nonce: nonce })));
var signer = crypto.createSign('RSA-SHA256');
signer.update(protected64 + '.' + payload64, 'utf8');
var signature64 = urlBase64Encode(signer.sign(that.accountKeyPem, 'base64'));
var data = {
header: header,
protected: protected64,
payload: payload64,
signature: signature64
};
superagent.post(url).set('Content-Type', 'application/x-www-form-urlencoded').send(JSON.stringify(data)).timeout(30 * 1000).end(function (error, res) {
if (error && !error.response) return callback(error); // network errors
callback(null, res);
});
});
};
Acme.prototype.updateContact = function (registrationUri, callback) {
assert.strictEqual(typeof registrationUri, 'string');
assert.strictEqual(typeof callback, 'function');
debug('updateContact: %s %s', registrationUri, this.email);
// https://github.com/ietf-wg-acme/acme/issues/30
var payload = {
resource: 'reg',
contact: [ 'mailto:' + this.email ],
agreement: LE_AGREEMENT
};
var that = this;
this.sendSignedRequest(registrationUri, JSON.stringify(payload), function (error, result) {
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
if (result.statusCode !== 202) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to update contact. Expecting 202, got %s %s', result.statusCode, result.text)));
debug('updateContact: contact of user updated to %s', that.email);
callback();
});
};
Acme.prototype.registerUser = function (callback) {
assert.strictEqual(typeof callback, 'function');
var payload = {
resource: 'new-reg',
contact: [ 'mailto:' + this.email ],
agreement: LE_AGREEMENT
};
debug('registerUser: %s', this.email);
var that = this;
this.sendSignedRequest(this.caOrigin + '/acme/new-reg', JSON.stringify(payload), function (error, result) {
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
if (result.statusCode === 409) return that.updateContact(result.headers.location, callback); // already exists
if (result.statusCode !== 201) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
debug('registerUser: registered user %s', that.email);
callback(null);
});
};
Acme.prototype.registerDomain = function (domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
var payload = {
resource: 'new-authz',
identifier: {
type: 'dns',
value: domain
}
};
debug('registerDomain: %s', domain);
this.sendSignedRequest(this.caOrigin + '/acme/new-authz', JSON.stringify(payload), function (error, result) {
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when registering domain: ' + error.message));
if (result.statusCode === 403) return callback(new AcmeError(AcmeError.FORBIDDEN, result.body.detail));
if (result.statusCode !== 201) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
debug('registerDomain: registered %s', domain);
callback(null, result.body);
});
};
Acme.prototype.prepareHttpChallenge = function (challenge, callback) {
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
debug('prepareHttpChallenge: preparing for challenge %j', challenge);
var token = challenge.token;
assert(util.isBuffer(this.accountKeyPem));
var jwk = {
e: b64(Buffer.from([0x01, 0x00, 0x01])), // Exponent - 65537
kty: 'RSA',
n: b64(getModulus(this.accountKeyPem))
};
var shasum = crypto.createHash('sha256');
shasum.update(JSON.stringify(jwk));
var thumbprint = urlBase64Encode(shasum.digest('base64'));
var keyAuthorization = token + '.' + thumbprint;
debug('prepareHttpChallenge: writing %s to %s', keyAuthorization, path.join(paths.ACME_CHALLENGES_DIR, token));
fs.writeFile(path.join(paths.ACME_CHALLENGES_DIR, token), token + '.' + thumbprint, function (error) {
if (error) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, error));
callback();
});
};
Acme.prototype.notifyChallengeReady = function (challenge, callback) {
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
debug('notifyChallengeReady: %s was met', challenge.uri);
var keyAuthorization = fs.readFileSync(path.join(paths.ACME_CHALLENGES_DIR, challenge.token), 'utf8');
var payload = {
resource: 'challenge',
keyAuthorization: keyAuthorization
};
this.sendSignedRequest(challenge.uri, JSON.stringify(payload), function (error, result) {
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when notifying challenge: ' + error.message));
if (result.statusCode !== 202) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to notify challenge. Expecting 202, got %s %s', result.statusCode, result.text)));
callback();
});
};
Acme.prototype.waitForChallenge = function (challenge, callback) {
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
debug('waitingForChallenge: %j', challenge);
async.retry({ times: 10, interval: 5000 }, function (retryCallback) {
debug('waitingForChallenge: getting status');
superagent.get(challenge.uri).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) {
debug('waitForChallenge: network error getting uri %s', challenge.uri);
return retryCallback(new AcmeError(AcmeError.EXTERNAL_ERROR, error.message)); // network error
}
if (result.statusCode !== 202) {
debug('waitForChallenge: invalid response code getting uri %s', result.statusCode);
return retryCallback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
}
debug('waitForChallenge: status is "%s %j', result.body.status, result.body);
if (result.body.status === 'pending') return retryCallback(new AcmeError(AcmeError.NOT_COMPLETED));
else if (result.body.status === 'valid') return retryCallback();
else return retryCallback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Unexpected status: ' + result.body.status));
});
}, function retryFinished(error) {
// async.retry will pass 'undefined' as second arg making it unusable with async.waterfall()
callback(error);
});
};
// https://community.letsencrypt.org/t/public-beta-rate-limits/4772 for rate limits
Acme.prototype.signCertificate = function (domain, csrDer, callback) {
assert.strictEqual(typeof domain, 'string');
assert(util.isBuffer(csrDer));
assert.strictEqual(typeof callback, 'function');
var outdir = paths.APP_CERTS_DIR;
var payload = {
resource: 'new-cert',
csr: b64(csrDer)
};
debug('signCertificate: sending new-cert request');
this.sendSignedRequest(this.caOrigin + '/acme/new-cert', JSON.stringify(payload), function (error, result) {
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when signing certificate: ' + error.message));
// 429 means we reached the cert limit for this domain
if (result.statusCode !== 201) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to sign certificate. Expecting 201, got %s %s', result.statusCode, result.text)));
var certUrl = result.headers.location;
if (!certUrl) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Missing location in downloadCertificate'));
safe.fs.writeFileSync(path.join(outdir, domain + '.url'), certUrl, 'utf8'); // maybe use for renewal
return callback(null, result.headers.location);
});
};
Acme.prototype.createKeyAndCsr = function (domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
var outdir = paths.APP_CERTS_DIR;
var csrFile = path.join(outdir, domain + '.csr');
var privateKeyFile = path.join(outdir, domain + '.key');
if (safe.fs.existsSync(privateKeyFile)) {
// in some old releases, csr file was corrupt. so always regenerate it
debug('createKeyAndCsr: reuse the key for renewal at %s', privateKeyFile);
} else {
var key = execSync('openssl genrsa 4096');
if (!key) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
debug('createKeyAndCsr: key file saved at %s', privateKeyFile);
}
var csrDer = execSync(util.format('openssl req -new -key %s -outform DER -subj /CN=%s', privateKeyFile, domain));
if (!csrDer) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(csrFile, csrDer)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error)); // bookkeeping
debug('createKeyAndCsr: csr file (DER) saved at %s', csrFile);
callback(null, csrDer);
};
// TODO: download the chain in a loop following 'up' header
Acme.prototype.downloadChain = function (linkHeader, callback) {
if (!linkHeader) return new AcmeError(AcmeError.EXTERNAL_ERROR, 'Empty link header when downloading certificate chain');
debug('downloadChain: linkHeader %s', linkHeader);
var linkInfo = parseLinks(linkHeader);
if (!linkInfo || !linkInfo.up) return new AcmeError(AcmeError.EXTERNAL_ERROR, 'Failed to parse link header when downloading certificate chain');
var intermediateCertUrl = linkInfo.up.startsWith('https://') ? linkInfo.up : (this.caOrigin + linkInfo.up);
debug('downloadChain: downloading from %s', intermediateCertUrl);
superagent.get(intermediateCertUrl).buffer().parse(function (res, done) {
var data = [ ];
res.on('data', function(chunk) { data.push(chunk); });
res.on('end', function () { res.text = Buffer.concat(data); done(); });
}).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when downloading certificate'));
if (result.statusCode !== 200) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
var chainDer = result.text;
var chainPem = execSync('openssl x509 -inform DER -outform PEM', { input: chainDer }); // this is really just base64 encoding with header
if (!chainPem) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
callback(null, chainPem);
});
};
Acme.prototype.downloadCertificate = function (domain, certUrl, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof certUrl, 'string');
assert.strictEqual(typeof callback, 'function');
var outdir = paths.APP_CERTS_DIR;
var that = this;
superagent.get(certUrl).buffer().parse(function (res, done) {
var data = [ ];
res.on('data', function(chunk) { data.push(chunk); });
res.on('end', function () { res.text = Buffer.concat(data); done(); });
}).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when downloading certificate'));
if (result.statusCode === 202) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, 'Retry not implemented yet'));
if (result.statusCode !== 200) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
var certificateDer = result.text;
safe.fs.writeFileSync(path.join(outdir, domain + '.der'), certificateDer);
debug('downloadCertificate: cert der file for %s saved', domain);
var certificatePem = execSync('openssl x509 -inform DER -outform PEM', { input: certificateDer }); // this is really just base64 encoding with header
if (!certificatePem) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
that.downloadChain(result.header['link'], function (error, chainPem) {
if (error) return callback(error);
var certificateFile = path.join(outdir, domain + '.cert');
var fullChainPem = Buffer.concat([certificatePem, chainPem]);
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
debug('downloadCertificate: cert file for %s saved at %s', domain, certificateFile);
callback();
});
});
};
Acme.prototype.acmeFlow = function (domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
if (!fs.existsSync(paths.ACME_ACCOUNT_KEY_FILE)) {
debug('getCertificate: generating acme account key on first run');
this.accountKeyPem = safe.child_process.execSync('openssl genrsa 4096');
if (!this.accountKeyPem) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
safe.fs.writeFileSync(paths.ACME_ACCOUNT_KEY_FILE, this.accountKeyPem);
} else {
debug('getCertificate: using existing acme account key');
this.accountKeyPem = fs.readFileSync(paths.ACME_ACCOUNT_KEY_FILE);
}
var that = this;
this.registerUser(function (error) {
if (error) return callback(error);
that.registerDomain(domain, function (error, result) {
if (error) return callback(error);
debug('acmeFlow: challenges: %j', result);
var httpChallenges = result.challenges.filter(function(x) { return x.type === 'http-01'; });
if (httpChallenges.length === 0) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'no http challenges'));
var challenge = httpChallenges[0];
async.waterfall([
that.prepareHttpChallenge.bind(that, challenge),
that.notifyChallengeReady.bind(that, challenge),
that.waitForChallenge.bind(that, challenge),
that.createKeyAndCsr.bind(that, domain),
that.signCertificate.bind(that, domain),
that.downloadCertificate.bind(that, domain)
], callback);
});
});
};
Acme.prototype.getCertificate = function (domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
debug('getCertificate: start acme flow for %s from %s', domain, this.caOrigin);
this.acmeFlow(domain, function (error) {
if (error) return callback(error);
var outdir = paths.APP_CERTS_DIR;
callback(null, path.join(outdir, domain + '.cert'), path.join(outdir, domain + '.key'));
});
};
function getCertificate(domain, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var acme = new Acme(options || { });
acme.getCertificate(domain, callback);
}
-636
View File
@@ -1,636 +0,0 @@
'use strict';
var assert = require('assert'),
async = require('async'),
crypto = require('crypto'),
debug = require('debug')('box:cert/acme2'),
domains = require('../domains.js'),
fs = require('fs'),
path = require('path'),
paths = require('../paths.js'),
safe = require('safetydance'),
superagent = require('superagent'),
util = require('util'),
_ = require('underscore');
const CA_PROD_DIRECTORY_URL = 'https://acme-v02.api.letsencrypt.org/directory',
CA_STAGING_DIRECTORY_URL = 'https://acme-staging-v02.api.letsencrypt.org/directory';
exports = module.exports = {
getCertificate: getCertificate,
// testing
_name: 'acme',
_getChallengeSubdomain: getChallengeSubdomain
};
function Acme2Error(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;
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(Acme2Error, Error);
Acme2Error.INTERNAL_ERROR = 'Internal Error';
Acme2Error.EXTERNAL_ERROR = 'External Error';
Acme2Error.ALREADY_EXISTS = 'Already Exists';
Acme2Error.NOT_COMPLETED = 'Not Completed';
Acme2Error.FORBIDDEN = 'Forbidden';
// http://jose.readthedocs.org/en/latest/
// https://www.ietf.org/proceedings/92/slides/slides-92-acme-1.pdf
// https://community.letsencrypt.org/t/list-of-client-implementations/2103
function Acme2(options) {
assert.strictEqual(typeof options, 'object');
this.accountKeyPem = null; // Buffer
this.email = options.email;
this.keyId = null;
this.caDirectory = options.prod ? CA_PROD_DIRECTORY_URL : CA_STAGING_DIRECTORY_URL;
this.directory = {};
this.performHttpAuthorization = !!options.performHttpAuthorization;
this.wildcard = !!options.wildcard;
}
Acme2.prototype.getNonce = function (callback) {
superagent.get(this.directory.newNonce).timeout(30 * 1000).end(function (error, response) {
if (error && !error.response) return callback(error);
if (response.statusCode !== 204) return callback(new Error('Invalid response code when fetching nonce : ' + response.statusCode));
return callback(null, response.headers['Replay-Nonce'.toLowerCase()]);
});
};
// urlsafe base64 encoding (jose)
function urlBase64Encode(string) {
return string.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
function b64(str) {
var buf = util.isBuffer(str) ? str : new Buffer(str);
return urlBase64Encode(buf.toString('base64'));
}
function getModulus(pem) {
assert(util.isBuffer(pem));
var stdout = safe.child_process.execSync('openssl rsa -modulus -noout', { input: pem, encoding: 'utf8' });
if (!stdout) return null;
var match = stdout.match(/Modulus=([0-9a-fA-F]+)$/m);
if (!match) return null;
return Buffer.from(match[1], 'hex');
}
Acme2.prototype.sendSignedRequest = function (url, payload, callback) {
assert.strictEqual(typeof url, 'string');
assert.strictEqual(typeof payload, 'string');
assert.strictEqual(typeof callback, 'function');
assert(util.isBuffer(this.accountKeyPem));
const that = this;
let header = {
url: url,
alg: 'RS256'
};
// keyId is null when registering account
if (this.keyId) {
header.kid = this.keyId;
} else {
header.jwk = {
e: b64(Buffer.from([0x01, 0x00, 0x01])), // exponent - 65537
kty: 'RSA',
n: b64(getModulus(this.accountKeyPem))
};
}
var payload64 = b64(payload);
this.getNonce(function (error, nonce) {
if (error) return callback(error);
debug('sendSignedRequest: using nonce %s for url %s', nonce, url);
var protected64 = b64(JSON.stringify(_.extend({ }, header, { nonce: nonce })));
var signer = crypto.createSign('RSA-SHA256');
signer.update(protected64 + '.' + payload64, 'utf8');
var signature64 = urlBase64Encode(signer.sign(that.accountKeyPem, 'base64'));
var data = {
protected: protected64,
payload: payload64,
signature: signature64
};
superagent.post(url).set('Content-Type', 'application/jose+json').set('User-Agent', 'acme-cloudron').send(JSON.stringify(data)).timeout(30 * 1000).end(function (error, res) {
if (error && !error.response) return callback(error); // network errors
callback(null, res);
});
});
};
Acme2.prototype.updateContact = function (registrationUri, callback) {
assert.strictEqual(typeof registrationUri, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`updateContact: registrationUri: ${registrationUri} email: ${this.email}`);
// https://github.com/ietf-wg-acme/acme/issues/30
const payload = {
contact: [ 'mailto:' + this.email ]
};
const that = this;
this.sendSignedRequest(registrationUri, JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
if (result.statusCode !== 200) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to update contact. Expecting 200, got %s %s', result.statusCode, result.text)));
debug(`updateContact: contact of user updated to ${that.email}`);
callback();
});
};
Acme2.prototype.registerUser = function (callback) {
assert.strictEqual(typeof callback, 'function');
var payload = {
termsOfServiceAgreed: true
};
debug('registerUser: registering user');
var that = this;
this.sendSignedRequest(this.directory.newAccount, JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when registering new account: ' + error.message));
// 200 if already exists. 201 for new accounts
if (result.statusCode !== 200 && result.statusCode !== 201) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to register new account. Expecting 200 or 201, got %s %s', result.statusCode, result.text)));
debug(`registerUser: user registered keyid: ${result.headers.location}`);
that.keyId = result.headers.location;
that.updateContact(result.headers.location, callback);
});
};
Acme2.prototype.newOrder = function (domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
var payload = {
identifiers: [{
type: 'dns',
value: domain
}]
};
debug('newOrder: %s', domain);
this.sendSignedRequest(this.directory.newOrder, JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when registering domain: ' + error.message));
if (result.statusCode === 403) return callback(new Acme2Error(Acme2Error.FORBIDDEN, result.body.detail));
if (result.statusCode !== 201) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
debug('newOrder: created order %s %j', domain, result.body);
const order = result.body, orderUrl = result.headers.location;
if (!Array.isArray(order.authorizations)) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'invalid authorizations in order'));
if (typeof order.finalize !== 'string') return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'invalid finalize in order'));
if (typeof orderUrl !== 'string') return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'invalid order location in order header'));
callback(null, order, orderUrl);
});
};
Acme2.prototype.waitForOrder = function (orderUrl, callback) {
assert.strictEqual(typeof orderUrl, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`waitForOrder: ${orderUrl}`);
async.retry({ times: 15, interval: 20000 }, function (retryCallback) {
debug('waitForOrder: getting status');
superagent.get(orderUrl).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) {
debug('waitForOrder: network error getting uri %s', orderUrl);
return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message)); // network error
}
if (result.statusCode !== 200) {
debug('waitForOrder: invalid response code getting uri %s', result.statusCode);
return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
}
debug('waitForOrder: status is "%s %j', result.body.status, result.body);
if (result.body.status === 'pending' || result.body.status === 'processing') return retryCallback(new Acme2Error(Acme2Error.NOT_COMPLETED));
else if (result.body.status === 'valid' && result.body.certificate) return retryCallback(null, result.body.certificate);
else return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Unexpected status or invalid response: ' + result.body));
});
}, callback);
};
Acme2.prototype.getKeyAuthorization = function (token) {
assert(util.isBuffer(this.accountKeyPem));
let jwk = {
e: b64(Buffer.from([0x01, 0x00, 0x01])), // Exponent - 65537
kty: 'RSA',
n: b64(getModulus(this.accountKeyPem))
};
let shasum = crypto.createHash('sha256');
shasum.update(JSON.stringify(jwk));
let thumbprint = urlBase64Encode(shasum.digest('base64'));
return token + '.' + thumbprint;
};
Acme2.prototype.notifyChallengeReady = function (challenge, callback) {
assert.strictEqual(typeof challenge, 'object'); // { type, status, url, token }
assert.strictEqual(typeof callback, 'function');
debug('notifyChallengeReady: %s was met', challenge.url);
const keyAuthorization = this.getKeyAuthorization(challenge.token);
var payload = {
resource: 'challenge',
keyAuthorization: keyAuthorization
};
this.sendSignedRequest(challenge.url, JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when notifying challenge: ' + error.message));
if (result.statusCode !== 200) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to notify challenge. Expecting 200, got %s %s', result.statusCode, result.text)));
callback();
});
};
Acme2.prototype.waitForChallenge = function (challenge, callback) {
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
debug('waitingForChallenge: %j', challenge);
async.retry({ times: 15, interval: 20000 }, function (retryCallback) {
debug('waitingForChallenge: getting status');
superagent.get(challenge.url).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) {
debug('waitForChallenge: network error getting uri %s', challenge.url);
return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message)); // network error
}
if (result.statusCode !== 200) {
debug('waitForChallenge: invalid response code getting uri %s', result.statusCode);
return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
}
debug('waitForChallenge: status is "%s %j', result.body.status, result.body);
if (result.body.status === 'pending') return retryCallback(new Acme2Error(Acme2Error.NOT_COMPLETED));
else if (result.body.status === 'valid') return retryCallback();
else return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Unexpected status: ' + result.body.status));
});
}, function retryFinished(error) {
// async.retry will pass 'undefined' as second arg making it unusable with async.waterfall()
callback(error);
});
};
// https://community.letsencrypt.org/t/public-beta-rate-limits/4772 for rate limits
Acme2.prototype.signCertificate = function (domain, finalizationUrl, csrDer, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof finalizationUrl, 'string');
assert(util.isBuffer(csrDer));
assert.strictEqual(typeof callback, 'function');
const payload = {
csr: b64(csrDer)
};
debug('signCertificate: sending sign request');
this.sendSignedRequest(finalizationUrl, JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when signing certificate: ' + error.message));
// 429 means we reached the cert limit for this domain
if (result.statusCode !== 200) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to sign certificate. Expecting 200, got %s %s', result.statusCode, result.text)));
return callback(null);
});
};
Acme2.prototype.createKeyAndCsr = function (hostname, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof callback, 'function');
var outdir = paths.APP_CERTS_DIR;
const certName = hostname.replace('*.', '_.');
var csrFile = path.join(outdir, `${certName}.csr`);
var privateKeyFile = path.join(outdir, `${certName}.key`);
if (safe.fs.existsSync(privateKeyFile)) {
// in some old releases, csr file was corrupt. so always regenerate it
debug('createKeyAndCsr: reuse the key for renewal at %s', privateKeyFile);
} else {
var key = safe.child_process.execSync('openssl genrsa 4096');
if (!key) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
debug('createKeyAndCsr: key file saved at %s', privateKeyFile);
}
var csrDer = safe.child_process.execSync(`openssl req -new -key ${privateKeyFile} -outform DER -subj /CN=${hostname}`);
if (!csrDer) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(csrFile, csrDer)) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error)); // bookkeeping
debug('createKeyAndCsr: csr file (DER) saved at %s', csrFile);
callback(null, csrDer);
};
Acme2.prototype.downloadCertificate = function (hostname, certUrl, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof certUrl, 'string');
assert.strictEqual(typeof callback, 'function');
var outdir = paths.APP_CERTS_DIR;
superagent.get(certUrl).buffer().parse(function (res, done) {
var data = [ ];
res.on('data', function(chunk) { data.push(chunk); });
res.on('end', function () { res.text = Buffer.concat(data); done(); });
}).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when downloading certificate'));
if (result.statusCode === 202) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, 'Retry not implemented yet'));
if (result.statusCode !== 200) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
const fullChainPem = result.text;
const certName = hostname.replace('*.', '_.');
var certificateFile = path.join(outdir, `${certName}.cert`);
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
debug('downloadCertificate: cert file for %s saved at %s', hostname, certificateFile);
callback();
});
};
Acme2.prototype.prepareHttpChallenge = function (hostname, domain, authorization, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof authorization, 'object');
assert.strictEqual(typeof callback, 'function');
debug('acmeFlow: challenges: %j', authorization);
let httpChallenges = authorization.challenges.filter(function(x) { return x.type === 'http-01'; });
if (httpChallenges.length === 0) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'no http challenges'));
let challenge = httpChallenges[0];
debug('prepareHttpChallenge: preparing for challenge %j', challenge);
let keyAuthorization = this.getKeyAuthorization(challenge.token);
debug('prepareHttpChallenge: writing %s to %s', keyAuthorization, path.join(paths.ACME_CHALLENGES_DIR, challenge.token));
fs.writeFile(path.join(paths.ACME_CHALLENGES_DIR, challenge.token), keyAuthorization, function (error) {
if (error) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, error));
callback(null, challenge);
});
};
Acme2.prototype.cleanupHttpChallenge = function (hostname, domain, challenge, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
debug('cleanupHttpChallenge: unlinking %s', path.join(paths.ACME_CHALLENGES_DIR, challenge.token));
fs.unlink(path.join(paths.ACME_CHALLENGES_DIR, challenge.token), callback);
};
function getChallengeSubdomain(hostname, domain) {
let challengeSubdomain;
if (hostname === domain) {
challengeSubdomain = '_acme-challenge';
} else if (hostname.includes('*')) { // wildcard
let subdomain = hostname.slice(0, -domain.length - 1);
challengeSubdomain = subdomain ? subdomain.replace('*', '_acme-challenge') : '_acme-challenge';
} else {
challengeSubdomain = '_acme-challenge.' + hostname.slice(0, -domain.length - 1);
}
debug(`getChallengeSubdomain: challenge subdomain for hostname ${hostname} at domain ${domain} is ${challengeSubdomain}`);
return challengeSubdomain;
}
Acme2.prototype.prepareDnsChallenge = function (hostname, domain, authorization, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof authorization, 'object');
assert.strictEqual(typeof callback, 'function');
debug('acmeFlow: challenges: %j', authorization);
let dnsChallenges = authorization.challenges.filter(function(x) { return x.type === 'dns-01'; });
if (dnsChallenges.length === 0) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'no dns challenges'));
let challenge = dnsChallenges[0];
const keyAuthorization = this.getKeyAuthorization(challenge.token);
let shasum = crypto.createHash('sha256');
shasum.update(keyAuthorization);
const txtValue = urlBase64Encode(shasum.digest('base64'));
let challengeSubdomain = getChallengeSubdomain(hostname, domain);
debug(`prepareDnsChallenge: update ${challengeSubdomain} with ${txtValue}`);
domains.upsertDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ], function (error) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message));
domains.waitForDnsRecord(challengeSubdomain, domain, 'TXT', txtValue, { interval: 5000, times: 200 }, function (error) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message));
callback(null, challenge);
});
});
};
Acme2.prototype.cleanupDnsChallenge = function (hostname, domain, challenge, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
const keyAuthorization = this.getKeyAuthorization(challenge.token);
let shasum = crypto.createHash('sha256');
shasum.update(keyAuthorization);
const txtValue = urlBase64Encode(shasum.digest('base64'));
let challengeSubdomain = getChallengeSubdomain(hostname, domain);
debug(`cleanupDnsChallenge: remove ${challengeSubdomain} with ${txtValue}`);
domains.removeDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ], function (error) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error));
callback(null);
});
};
Acme2.prototype.prepareChallenge = function (hostname, domain, authorizationUrl, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof authorizationUrl, 'string');
assert.strictEqual(typeof callback, 'function');
const that = this;
superagent.get(authorizationUrl).timeout(30 * 1000).end(function (error, response) {
if (error && !error.response) return callback(error);
if (response.statusCode !== 200) return callback(new Error('Invalid response code getting authorization : ' + response.statusCode));
const authorization = response.body;
if (that.performHttpAuthorization) {
that.prepareHttpChallenge(hostname, domain, authorization, callback);
} else {
that.prepareDnsChallenge(hostname, domain, authorization, callback);
}
});
};
Acme2.prototype.cleanupChallenge = function (hostname, domain, challenge, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
if (this.performHttpAuthorization) {
this.cleanupHttpChallenge(hostname, domain, challenge, callback);
} else {
this.cleanupDnsChallenge(hostname, domain, challenge, callback);
}
};
Acme2.prototype.acmeFlow = function (hostname, domain, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
if (!fs.existsSync(paths.ACME_ACCOUNT_KEY_FILE)) {
debug('getCertificate: generating acme account key on first run');
this.accountKeyPem = safe.child_process.execSync('openssl genrsa 4096');
if (!this.accountKeyPem) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
safe.fs.writeFileSync(paths.ACME_ACCOUNT_KEY_FILE, this.accountKeyPem);
} else {
debug('getCertificate: using existing acme account key');
this.accountKeyPem = fs.readFileSync(paths.ACME_ACCOUNT_KEY_FILE);
}
var that = this;
this.registerUser(function (error) {
if (error) return callback(error);
that.newOrder(hostname, function (error, order, orderUrl) {
if (error) return callback(error);
async.eachSeries(order.authorizations, function (authorizationUrl, iteratorCallback) {
debug(`acmeFlow: authorizing ${authorizationUrl}`);
that.prepareChallenge(hostname, domain, authorizationUrl, function (error, challenge) {
if (error) return iteratorCallback(error);
async.waterfall([
that.notifyChallengeReady.bind(that, challenge),
that.waitForChallenge.bind(that, challenge),
that.createKeyAndCsr.bind(that, hostname),
that.signCertificate.bind(that, hostname, order.finalize),
that.waitForOrder.bind(that, orderUrl),
that.downloadCertificate.bind(that, hostname)
], function (error) {
that.cleanupChallenge(hostname, domain, challenge, function (cleanupError) {
if (cleanupError) debug('acmeFlow: ignoring error when cleaning up challenge:', cleanupError);
iteratorCallback(error);
});
});
});
}, callback);
});
});
};
Acme2.prototype.getDirectory = function (callback) {
const that = this;
superagent.get(this.caDirectory).timeout(30 * 1000).end(function (error, response) {
if (error && !error.response) return callback(error);
if (response.statusCode !== 200) return callback(new Error('Invalid response code when fetching directory : ' + response.statusCode));
if (typeof response.body.newNonce !== 'string' ||
typeof response.body.newOrder !== 'string' ||
typeof response.body.newAccount !== 'string') return callback(new Error(`Invalid response body : ${response.body}`));
that.directory = response.body;
callback(null);
});
};
Acme2.prototype.getCertificate = function (hostname, domain, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`getCertificate: start acme flow for ${hostname} from ${this.caDirectory}`);
if (hostname !== domain && this.wildcard) { // bare domain is not part of wildcard SAN
hostname = domains.makeWildcard(hostname);
debug(`getCertificate: will get wildcard cert for ${hostname}`);
}
const that = this;
this.getDirectory(function (error) {
if (error) return callback(error);
that.acmeFlow(hostname, domain, function (error) {
if (error) return callback(error);
var outdir = paths.APP_CERTS_DIR;
const certName = hostname.replace('*.', '_.');
callback(null, path.join(outdir, `${certName}.cert`), path.join(outdir, `${certName}.key`));
});
});
};
function getCertificate(hostname, domain, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var acme = new Acme2(options || { });
acme.getCertificate(hostname, domain, callback);
}
+3 -4
View File
@@ -10,13 +10,12 @@ exports = module.exports = {
var assert = require('assert'),
debug = require('debug')('box:cert/caas.js');
function getCertificate(hostname, domain, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
function getCertificate(vhost, options, callback) {
assert.strictEqual(typeof vhost, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debug('getCertificate: using fallback certificate', hostname);
debug('getCertificate: using fallback certificate', vhost);
return callback(null, '', '');
}
+3 -4
View File
@@ -10,13 +10,12 @@ exports = module.exports = {
var assert = require('assert'),
debug = require('debug')('box:cert/fallback.js');
function getCertificate(hostname, domain, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
function getCertificate(vhost, options, callback) {
assert.strictEqual(typeof vhost, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debug('getCertificate: using fallback certificate', hostname);
debug('getCertificate: using fallback certificate', vhost);
return callback(null, '', '');
}
+1 -2
View File
@@ -12,8 +12,7 @@ exports = module.exports = {
var assert = require('assert');
function getCertificate(hostname, domain, options, callback) {
assert.strictEqual(typeof hostname, 'string');
function getCertificate(domain, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
+73 -190
View File
@@ -10,44 +10,30 @@ exports = module.exports = {
getLogs: getLogs,
reboot: reboot,
isRebootRequired: isRebootRequired,
onActivated: onActivated,
prepareDashboardDomain: prepareDashboardDomain,
setDashboardDomain: setDashboardDomain,
renewCerts: renewCerts,
runSystemChecks: runSystemChecks,
// exposed for testing
_checkDiskSpace: checkDiskSpace
checkDiskSpace: checkDiskSpace
};
var assert = require('assert'),
async = require('async'),
clients = require('./clients.js'),
config = require('./config.js'),
constants = require('./constants.js'),
cron = require('./cron.js'),
debug = require('debug')('box:cloudron'),
domains = require('./domains.js'),
DomainsError = require('./domains.js').DomainsError,
df = require('@sindresorhus/df'),
eventlog = require('./eventlog.js'),
fs = require('fs'),
mail = require('./mail.js'),
notifications = require('./notifications.js'),
mailer = require('./mailer.js'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
platform = require('./platform.js'),
progress = require('./progress.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
shell = require('./shell.js'),
spawn = require('child_process').spawn,
split = require('split'),
tasks = require('./tasks.js'),
users = require('./users.js'),
util = require('util');
@@ -79,50 +65,41 @@ CloudronError.INTERNAL_ERROR = 'Internal Error';
CloudronError.EXTERNAL_ERROR = 'External Error';
CloudronError.BAD_STATE = 'Bad state';
CloudronError.ALREADY_UPTODATE = 'No Update Available';
CloudronError.NOT_FOUND = 'Not found';
CloudronError.SELF_UPGRADE_NOT_SUPPORTED = 'Self upgrade not supported';
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
cron.startPreActivationJobs(callback);
runStartupTasks();
async.series([
settings.initialize,
reverseProxy.configureDefaultServer,
cron.initialize, // required for caas heartbeat before activation
onActivated
], callback);
}
function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function');
async.series([
cron.stopJobs,
platform.stop
cron.uninitialize,
platform.stop,
settings.uninitialize
], callback);
}
function onActivated(callback) {
assert.strictEqual(typeof callback, 'function');
callback = callback || NOOP_CALLBACK;
// Starting the platform after a user is available means:
// 1. mail bounces can now be sent to the cloudron owner
// 2. the restore code path can run without sudo (since mail/ is non-root)
async.series([
platform.start,
cron.startPostActivationJobs
], callback);
}
users.count(function (error, count) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
if (!count) return callback(); // not activated
// each of these tasks can fail. we will add some routes to fix/re-run them
function runStartupTasks() {
// configure nginx to be reachable by IP
reverseProxy.configureDefaultServer(NOOP_CALLBACK);
// always generate webadmin config since we have no versioning mechanism for the ejs
if (config.adminDomain()) reverseProxy.writeAdminConfig(config.adminDomain(), NOOP_CALLBACK);
// check activation state and start the platform
users.isActivated(function (error, activated) {
if (error) return debug(error);
if (!activated) return debug('initialize: not activated yet'); // not activated
onActivated(NOOP_CALLBACK);
platform.start(callback);
});
}
@@ -166,6 +143,7 @@ function getConfig(callback) {
adminFqdn: config.adminFqdn(),
mailFqdn: config.mailFqdn(),
version: config.version(),
progress: progress.getAll(),
isDemo: config.isDemo(),
edition: config.edition(),
memory: os.totalmem(),
@@ -176,45 +154,11 @@ function getConfig(callback) {
}
function reboot(callback) {
shell.sudo('reboot', [ REBOOT_CMD ], {}, callback);
}
function isRebootRequired(callback) {
assert.strictEqual(typeof callback, 'function');
// https://serverfault.com/questions/92932/how-does-ubuntu-keep-track-of-the-system-restart-required-flag-in-motd
callback(null, fs.existsSync('/var/run/reboot-required'));
}
// called from cron.js
function runSystemChecks() {
async.parallel([
checkBackupConfiguration,
checkDiskSpace,
checkMailStatus
], function () {
debug('runSystemChecks: done');
});
}
function checkBackupConfiguration(callback) {
assert.strictEqual(typeof callback, 'function');
debug('Checking backup configuration');
settings.getBackupConfig(function (error, backupConfig) {
if (error) return console.error(error);
if (backupConfig.provider === 'noop') {
notifications.backupConfigWarning('Cloudron backups are disabled. Please ensure this server is backed up using alternate means. See https://cloudron.io/documentation/backups/#storage-providers for more information.');
} else if (backupConfig.provider === 'filesystem' && !backupConfig.externalDisk) {
notifications.backupConfigWarning('Cloudron backups are currently on the same disk as the Cloudron server instance. This is dangerous and can lead to complete data loss if the disk fails. See https://cloudron.io/documentation/backups/#storage-providers for storing backups in an external location.');
}
});
shell.sudo('reboot', [ REBOOT_CMD ], callback);
}
function checkDiskSpace(callback) {
assert.strictEqual(typeof callback, 'function');
callback = callback || NOOP_CALLBACK;
debug('Checking disk space');
@@ -244,42 +188,13 @@ function checkDiskSpace(callback) {
debug('Disk space checked. ok: %s', !oos);
if (oos) notifications.diskSpaceWarning(JSON.stringify(entries, null, 4));
if (oos) mailer.outOfDiskSpace(JSON.stringify(entries, null, 4));
callback();
}).catch(function (error) {
if (error) console.error(error);
callback();
});
});
}
function checkMailStatus(callback) {
assert.strictEqual(typeof callback, 'function');
debug('checking mail status');
domains.getAll(function (error, allDomains) {
if (error) return callback(error);
async.filterSeries(allDomains, function (domainObject, iteratorCallback) {
mail.getStatus(config.adminDomain(), function (error, result) {
if (error) return iteratorCallback(null, true);
let mailError = Object.keys(result.dns).some((record) => !result.dns[record].status);
if (result.rbl && !result.rbl.status) mailError = true;
if (result.relay && !result.relay.status) mailError = true;
iteratorCallback(null, mailError);
});
}, function (error, erroredDomainObjects) {
if (error || erroredDomainObjects.length === 0) return callback(error);
const erroredDomains = erroredDomainObjects.map((d) => d.domain);
debug(`checkMailStatus: ${erroredDomains.join(',')} failed status checks`);
if (erroredDomains.length) notifications.mailStatusWarning(`Email status check of the following domain(s) failed - ${erroredDomains.join(',')}. See the Status tab in the [Email view](/#/email/) for more information.`);
callback();
debug('df error %s', error.message);
mailer.outOfDiskSpace(error.message);
return callback();
});
});
}
@@ -289,38 +204,61 @@ function getLogs(unit, options, callback) {
assert(options && typeof options === 'object');
assert.strictEqual(typeof callback, 'function');
assert.strictEqual(typeof options.lines, 'number');
assert.strictEqual(typeof options.format, 'string');
assert.strictEqual(typeof options.follow, 'boolean');
var lines = options.lines === -1 ? '+1' : options.lines,
var lines = options.lines || 100,
format = options.format || 'json',
follow = options.follow;
follow = !!options.follow;
assert.strictEqual(typeof lines, 'number');
assert.strictEqual(typeof format, 'string');
assert.strictEqual(typeof lines, 'number');
assert.strictEqual(typeof format, 'string');
debug('Getting logs for %s as %s', unit, format);
let args = [ '--lines=' + lines ];
if (follow) args.push('--follow');
var cp, transformStream;
if (unit === 'box') {
let args = [ '--no-pager', `--lines=${lines}` ];
if (format === 'short') args.push('--output=short', '-a'); else args.push('--output=json');
if (follow) args.push('--follow');
args.push('--unit=box');
args.push('--unit=cloudron-updater');
cp = spawn('/bin/journalctl', args);
// need to handle box.log without subdir
if (unit === 'box') args.push(path.join(paths.LOG_DIR, 'box.log'));
else args.push(path.join(paths.LOG_DIR, unit, 'app.log'));
transformStream = split(function mapper(line) {
if (format !== 'json') return line + '\n';
var cp = spawn('/usr/bin/tail', args);
var obj = safe.JSON.parse(line);
if (!obj) return undefined;
var transformStream = split(function mapper(line) {
if (format !== 'json') return line + '\n';
return JSON.stringify({
realtimeTimestamp: obj.__REALTIME_TIMESTAMP,
monotonicTimestamp: obj.__MONOTONIC_TIMESTAMP,
message: obj.MESSAGE,
source: obj.SYSLOG_IDENTIFIER || ''
}) + '\n';
});
} else { // mail, mongodb, mysql, postgresql, backup
let args = [ '--lines=' + lines ];
if (follow) args.push('--follow');
args.push(path.join(paths.LOG_DIR, unit, 'app.log'));
var data = line.split(' '); // logs are <ISOtimestamp> <msg>
var timestamp = (new Date(data[0])).getTime();
if (isNaN(timestamp)) timestamp = 0;
cp = spawn('/usr/bin/tail', args);
return JSON.stringify({
realtimeTimestamp: timestamp * 1000,
message: line.slice(data[0].length+1),
source: unit
}) + '\n';
});
transformStream = split(function mapper(line) {
if (format !== 'json') return line + '\n';
var data = line.split(' '); // logs are <ISOtimestamp> <msg>
var timestamp = (new Date(data[0])).getTime();
if (isNaN(timestamp)) timestamp = 0;
return JSON.stringify({
realtimeTimestamp: timestamp * 1000,
message: line.slice(data[0].length+1),
source: unit
}) + '\n';
});
}
transformStream.close = cp.kill.bind(cp, 'SIGKILL'); // closing stream kills the child process
@@ -328,58 +266,3 @@ function getLogs(unit, options, callback) {
return callback(null, transformStream);
}
function prepareDashboardDomain(domain, auditSource, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug(`prepareDashboardDomain: ${domain}`);
let task = tasks.startTask(tasks.TASK_PREPARE_DASHBOARD_DOMAIN, [ domain, auditSource ]);
task.on('error', (error) => callback(new CloudronError(CloudronError.INTERNAL_ERROR, error)));
task.on('start', (taskId) => callback(null, taskId));
}
function setDashboardDomain(domain, auditSource, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug(`setDashboardDomain: ${domain}`);
domains.get(domain, function (error, domainObject) {
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new CloudronError(CloudronError.BAD_FIELD, 'No such domain'));
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
reverseProxy.writeAdminConfig(domain, function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
const fqdn = domains.fqdn(constants.ADMIN_LOCATION, domainObject);
config.setAdminDomain(domain);
config.setAdminLocation(constants.ADMIN_LOCATION);
config.setAdminFqdn(fqdn);
clients.addDefaultClients(config.adminOrigin(), function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_DASHBOARD_DOMAIN_UPDATE, auditSource, { domain: domain, fqdn: fqdn });
mail.setMailFqdn(fqdn, domain, NOOP_CALLBACK);
callback(null);
});
});
});
}
function renewCerts(options, auditSource, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
let task = tasks.startTask(tasks.TASK_RENEW_CERTS, [ options, auditSource ]);
task.on('error', (error) => callback(new CloudronError(CloudronError.INTERNAL_ERROR, error)));
task.on('start', (taskId) => callback(null, taskId));
}
+33 -15
View File
@@ -22,6 +22,7 @@ exports = module.exports = {
setAdminFqdn: setAdminFqdn,
setAdminLocation: setAdminLocation,
version: version,
setVersion: setVersion,
database: database,
edition: edition,
@@ -36,11 +37,13 @@ exports = module.exports = {
hasIPv6: hasIPv6,
dkimSelector: dkimSelector,
isManaged: isManaged,
isDemo: isDemo,
// feature flags based on editions (these have a separate license from standard edition)
isSpacesEnabled: isSpacesEnabled,
allowHyphenatedSubdomains: allowHyphenatedSubdomains,
allowOperatorActions: allowOperatorActions,
isAdminDomainLocked: isAdminDomainLocked,
// for testing resets to defaults
_reset: _reset
@@ -56,20 +59,24 @@ var assert = require('assert'),
// assert on unknown environment can't proceed
assert(exports.CLOUDRON || exports.TEST, 'Unknown environment. This should not happen!');
var homeDir = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
var data = { };
function baseDir() {
const homeDir = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
if (exports.CLOUDRON) return homeDir;
if (exports.TEST) return path.join(homeDir, '.cloudron_test');
// cannot reach
}
const cloudronConfigFileName = exports.CLOUDRON ? '/etc/cloudron/cloudron.conf' : path.join(baseDir(), 'cloudron.conf');
var cloudronConfigFileName = path.join(baseDir(), 'configs/cloudron.conf');
// only tests can run without a config file on disk, they use the defaults with runtime overrides
if (exports.CLOUDRON) assert(fs.existsSync(cloudronConfigFileName), 'No cloudron.conf found, cannot proceed');
function saveSync() {
// only save values we want to have in the cloudron.conf, see start.sh
var conf = {
version: data.version,
apiServerOrigin: data.apiServerOrigin,
webServerOrigin: data.webServerOrigin,
adminDomain: data.adminDomain,
@@ -97,6 +104,7 @@ function initConfig() {
data.adminDomain = '';
data.adminLocation = 'my';
data.port = 3000;
data.version = null;
data.apiServerOrigin = null;
data.webServerOrigin = null;
data.provider = 'generic';
@@ -117,11 +125,11 @@ function initConfig() {
// overrides for local testings
if (exports.TEST) {
data.version = '1.1.1-test';
data.port = 5454;
data.apiServerOrigin = 'http://localhost:6060'; // hock doesn't support https
// see setupTest script how the mysql-server is run
data.database.hostname = require('child_process').execSync('docker inspect -f "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" mysql-server').toString().trim();
data.database.password = '';
data.database.name = 'boxtest';
}
// overwrite defaults with saved config
@@ -206,8 +214,11 @@ function sysadminOrigin() {
}
function version() {
if (exports.TEST) return '3.0.0-test';
return fs.readFileSync(path.join(__dirname, '../VERSION'), 'utf8').trim();
return get('version');
}
function setVersion(version) {
set('version', version);
}
function database() {
@@ -222,18 +233,25 @@ function isSpacesEnabled() {
return get('edition') === 'education';
}
function allowHyphenatedSubdomains() {
return get('edition') === 'hostingprovider';
}
function allowOperatorActions() {
return get('edition') !== 'hostingprovider';
}
function isAdminDomainLocked() {
return get('edition') === 'hostingprovider';
}
function provider() {
return get('provider');
}
function isManaged() {
return edition() === 'hostingprovider';
}
function hasIPv6() {
const IPV6_PROC_FILE = '/proc/net/if_inet6';
// on contabo, /proc/net/if_inet6 is an empty file. so just exists is not enough
return fs.existsSync(IPV6_PROC_FILE) && fs.readFileSync(IPV6_PROC_FILE, 'utf8').trim().length !== 0;
return fs.existsSync(IPV6_PROC_FILE);
}
// it has to change with the adminLocation so that multiple cloudrons
+2 -2
View File
@@ -17,9 +17,9 @@ exports = module.exports = {
'admins', 'users' // ldap code uses 'users' pseudo group
],
ADMIN_LOCATION: 'my',
ADMIN_NAME: 'Settings',
NGINX_DEFAULT_CONFIG_FILE_NAME: 'default.conf',
NGINX_ADMIN_CONFIG_FILE_NAME: 'admin.conf',
GHOST_USER_FILE: '/tmp/cloudron_ghost.json',
+29 -45
View File
@@ -1,14 +1,11 @@
'use strict';
exports = module.exports = {
startPostActivationJobs: startPostActivationJobs,
startPreActivationJobs: startPreActivationJobs,
stopJobs: stopJobs
initialize: initialize,
uninitialize: uninitialize
};
var appHealthMonitor = require('./apphealthmonitor.js'),
apps = require('./apps.js'),
var apps = require('./apps.js'),
appstore = require('./appstore.js'),
assert = require('assert'),
backups = require('./backups.js'),
@@ -22,6 +19,7 @@ var appHealthMonitor = require('./apphealthmonitor.js'),
dyndns = require('./dyndns.js'),
eventlog = require('./eventlog.js'),
janitor = require('./janitor.js'),
reverseProxy = require('./reverseproxy.js'),
scheduler = require('./scheduler.js'),
settings = require('./settings.js'),
updater = require('./updater.js'),
@@ -35,19 +33,18 @@ var gJobs = {
backup: null,
boxUpdateChecker: null,
caasHeartbeat: null,
systemChecks: null,
checkDiskSpace: null,
certificateRenew: null,
cleanupBackups: null,
cleanupEventlog: null,
cleanupTokens: null,
digestEmail: null,
dockerVolumeCleaner: null,
dynamicDns: null,
schedulerSync: null,
appHealthMonitor: null
dynamicDNS: null,
schedulerSync: null
};
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
var AUDIT_SOURCE = { userId: null, username: 'cron' };
// cron format
@@ -58,7 +55,9 @@ var AUDIT_SOURCE = { userId: null, username: 'cron' };
// Months: 0-11
// Day of Week: 0-6
function startPreActivationJobs(callback) {
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
if (config.provider() === 'caas') {
// hack: send the first heartbeat only after we are running for 60 seconds
// required as we end up sending a heartbeat and then cloudron-setup reboots the server
@@ -72,12 +71,6 @@ function startPreActivationJobs(callback) {
});
}
callback();
}
function startPostActivationJobs(callback) {
assert.strictEqual(typeof callback, 'function');
var randomHourMinute = Math.floor(60*Math.random());
gJobs.alive = new CronJob({
cronTime: '00 ' + randomHourMinute + ' * * * *', // every hour on a random minute
@@ -85,10 +78,10 @@ function startPostActivationJobs(callback) {
start: true
});
settings.on(settings.TIME_ZONE_KEY, recreateJobs);
settings.on(settings.APP_AUTOUPDATE_PATTERN_KEY, appAutoupdatePatternChanged);
settings.on(settings.BOX_AUTOUPDATE_PATTERN_KEY, boxAutoupdatePatternChanged);
settings.on(settings.DYNAMIC_DNS_KEY, dynamicDnsChanged);
settings.events.on(settings.TIME_ZONE_KEY, recreateJobs);
settings.events.on(settings.APP_AUTOUPDATE_PATTERN_KEY, appAutoupdatePatternChanged);
settings.events.on(settings.BOX_AUTOUPDATE_PATTERN_KEY, boxAutoupdatePatternChanged);
settings.events.on(settings.DYNAMIC_DNS_KEY, dynamicDnsChanged);
settings.getAll(function (error, allSettings) {
if (error) return callback(error);
@@ -115,12 +108,11 @@ function recreateJobs(tz) {
timeZone: tz
});
if (gJobs.systemChecks) gJobs.systemChecks.stop();
gJobs.systemChecks = new CronJob({
cronTime: '00 30 * * * *', // every hour
onTick: cloudron.runSystemChecks,
if (gJobs.checkDiskSpace) gJobs.checkDiskSpace.stop();
gJobs.checkDiskSpace = new CronJob({
cronTime: '00 30 */4 * * *', // every 4 hours
onTick: cloudron.checkDiskSpace,
start: true,
runOnInit: true, // run system check immediately
timeZone: tz
});
@@ -154,7 +146,7 @@ function recreateJobs(tz) {
if (gJobs.cleanupBackups) gJobs.cleanupBackups.stop();
gJobs.cleanupBackups = new CronJob({
cronTime: '00 45 */6 * * *', // every 6 hours. try not to overlap with ensureBackup job
onTick: backups.startCleanupTask.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
onTick: backups.cleanup.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
start: true,
timeZone: tz
});
@@ -186,7 +178,7 @@ function recreateJobs(tz) {
if (gJobs.certificateRenew) gJobs.certificateRenew.stop();
gJobs.certificateRenew = new CronJob({
cronTime: '00 00 */12 * * *', // every 12 hours
onTick: cloudron.renewCerts.bind(null, {}, AUDIT_SOURCE, NOOP_CALLBACK),
onTick: reverseProxy.renewAll.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
start: true,
timeZone: tz
});
@@ -198,14 +190,6 @@ function recreateJobs(tz) {
start: true,
timeZone: tz
});
if (gJobs.appHealthMonitor) gJobs.appHealthMonitor.stop();
gJobs.appHealthMonitor = new CronJob({
cronTime: '*/10 * * * * *', // every 10 seconds
onTick: appHealthMonitor.run.bind(null, 10, NOOP_CALLBACK),
start: true,
timeZone: tz
});
}
function boxAutoupdatePatternChanged(pattern) {
@@ -267,25 +251,25 @@ function dynamicDnsChanged(enabled) {
debug('Dynamic DNS setting changed to %s', enabled);
if (enabled) {
gJobs.dynamicDns = new CronJob({
gJobs.dynamicDNS = new CronJob({
cronTime: '00 */10 * * * *',
onTick: dyndns.sync,
start: true,
timeZone: gJobs.boxUpdateCheckerJob.cronTime.zone // hack
});
} else {
if (gJobs.dynamicDns) gJobs.dynamicDns.stop();
gJobs.dynamicDns = null;
if (gJobs.dynamicDNS) gJobs.dynamicDNS.stop();
gJobs.dynamicDNS = null;
}
}
function stopJobs(callback) {
function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function');
settings.removeListener(settings.TIME_ZONE_KEY, recreateJobs);
settings.removeListener(settings.APP_AUTOUPDATE_PATTERN_KEY, appAutoupdatePatternChanged);
settings.removeListener(settings.BOX_AUTOUPDATE_PATTERN_KEY, boxAutoupdatePatternChanged);
settings.removeListener(settings.DYNAMIC_DNS_KEY, dynamicDnsChanged);
settings.events.removeListener(settings.TIME_ZONE_KEY, recreateJobs);
settings.events.removeListener(settings.APP_AUTOUPDATE_PATTERN_KEY, appAutoupdatePatternChanged);
settings.events.removeListener(settings.BOX_AUTOUPDATE_PATTERN_KEY, boxAutoupdatePatternChanged);
settings.events.removeListener(settings.DYNAMIC_DNS_KEY, dynamicDnsChanged);
for (var job in gJobs) {
if (!gJobs[job]) continue;
+7 -3
View File
@@ -85,7 +85,7 @@ function reconnect(callback) {
function clear(callback) {
assert.strictEqual(typeof callback, 'function');
var cmd = util.format('mysql --host="%s" --user="%s" --password="%s" -Nse "SHOW TABLES" %s | grep -v "^migrations$" | while read table; do mysql --host="%s" --user="%s" --password="%s" -e "SET FOREIGN_KEY_CHECKS = 0; TRUNCATE TABLE $table" %s; done',
var cmd = util.format('mysql --host=%s --user="%s" --password="%s" -Nse "SHOW TABLES" %s | grep -v "^migrations$" | while read table; do mysql --host=%s --user="%s" --password="%s" -e "SET FOREIGN_KEY_CHECKS = 0; TRUNCATE TABLE $table" %s; done',
config.database().hostname, config.database().username, config.database().password, config.database().name,
config.database().hostname, config.database().username, config.database().password, config.database().name);
@@ -177,7 +177,9 @@ function importFromFile(file, callback) {
assert.strictEqual(typeof file, 'string');
assert.strictEqual(typeof callback, 'function');
var cmd = `/usr/bin/mysql -h "${config.database().hostname}" -u ${config.database().username} -p${config.database().password} ${config.database().name} < ${file}`;
var password = config.database().password ? '-p' + config.database().password : '--skip-password';
var cmd = `/usr/bin/mysql -u ${config.database().username} ${password} ${config.database().name} < ${file}`;
async.series([
query.bind(null, 'CREATE DATABASE IF NOT EXISTS box'),
@@ -189,7 +191,9 @@ function exportToFile(file, callback) {
assert.strictEqual(typeof file, 'string');
assert.strictEqual(typeof callback, 'function');
var cmd = `/usr/bin/mysqldump -h "${config.database().hostname}" -u root -p${config.database().password} --single-transaction --routines --triggers ${config.database().name} > "${file}"`;
var password = config.database().password ? '-p' + config.database().password : '--skip-password';
var cmd = `/usr/bin/mysqldump -u root ${password} --single-transaction --routines \
--triggers ${config.database().name} > "${file}"`;
child_process.exec(cmd, callback);
}
-55
View File
@@ -1,55 +0,0 @@
'use strict';
let assert = require('assert'),
path = require('path');
class DataLayout {
constructor(localRoot, dirMap) {
assert.strictEqual(typeof localRoot, 'string');
assert(Array.isArray(dirMap), 'Expecting layout to be an array');
this._localRoot = localRoot;
this._dirMap = dirMap;
this._remoteRegexps = dirMap.map((l) => new RegExp('^\\./' + l.remoteDir + '/?'));
this._localRegexps = dirMap.map((l) => new RegExp('^' + l.localDir + '/?'));
}
toLocalPath(remoteName) {
assert.strictEqual(typeof remoteName, 'string');
for (let i = 0; i < this._remoteRegexps.length; i++) {
if (!remoteName.match(this._remoteRegexps[i])) continue;
return remoteName.replace(this._remoteRegexps[i], this._dirMap[i].localDir + '/'); // make paths absolute
}
return remoteName.replace(new RegExp('^\\.'), this._localRoot);
}
toRemotePath(localName) {
assert.strictEqual(typeof localName, 'string');
for (let i = 0; i < this._localRegexps.length; i++) {
if (!localName.match(this._localRegexps[i])) continue;
return localName.replace(this._localRegexps[i], './' + this._dirMap[i].remoteDir + '/'); // make paths relative
}
return localName.replace(new RegExp('^' + this._localRoot + '/?'), './');
}
localRoot() {
return this._localRoot;
}
getBasename() { // used to generate cache file names
return path.basename(this._localRoot);
}
toString() {
return JSON.stringify({ localRoot: this._localRoot, layout: this._dirMap });
}
localPaths() {
return [ this._localRoot ].concat(this._dirMap.map((l) => l.localDir));
}
directoryMap() {
return this._dirMap;
}
static fromString(str) {
const obj = JSON.parse(str);
return new DataLayout(obj.localRoot, obj.layout);
}
}
exports = module.exports = DataLayout;
+2 -1
View File
@@ -1,6 +1,7 @@
'use strict';
var debug = require('debug')('box:digest'),
var appstore = require('./appstore.js'),
debug = require('debug')('box:digest'),
eventlog = require('./eventlog.js'),
updatechecker = require('./updatechecker.js'),
mailer = require('./mailer.js'),
+44 -64
View File
@@ -1,56 +1,38 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
wait: wait,
waitForDns: require('./waitfordns.js'),
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
config = require('../config.js'),
debug = require('debug')('box:dns/caas'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
superagent = require('superagent'),
util = require('util'),
waitForDns = require('./waitfordns.js');
util = require('util');
function getFqdn(location, domain) {
assert.strictEqual(typeof location, 'string');
function getFqdn(subdomain, domain) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domain, 'string');
return (location === '') ? domain : location + '-' + domain;
return (subdomain === '') ? domain : subdomain + '-' + domain;
}
function removePrivateFields(domainObject) {
domainObject.config.token = domains.SECRET_PLACEHOLDER;
// do not return the 'key'. in caas, this is private
delete domainObject.fallbackCertificate.key;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
function add(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config;
var fqdn = subdomain !== '' && type === 'TXT' ? subdomain + '.' + dnsConfig.fqdn : getFqdn(subdomain, dnsConfig.fqdn);
let fqdn = location !== '' && type === 'TXT' ? location + '.' + domainObject.domain : getFqdn(location, domainObject.domain);
debug('add: %s for zone %s of type %s with values %j', location, domainObject.domain, type, values);
debug('add: %s for zone %s of type %s with values %j', subdomain, dnsConfig.fqdn, type, values);
var data = {
type: type,
@@ -72,16 +54,16 @@ function upsert(domainObject, location, type, values, callback) {
});
}
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config;
const fqdn = location !== '' && type === 'TXT' ? location + '.' + domainObject.domain : getFqdn(location, domainObject.domain);
var fqdn = subdomain !== '' && type === 'TXT' ? subdomain + '.' + dnsConfig.fqdn : getFqdn(subdomain, dnsConfig.fqdn);
debug('get: zoneName: %s subdomain: %s type: %s fqdn: %s', domainObject.domain, location, type, fqdn);
debug('get: zoneName: %s subdomain: %s type: %s fqdn: %s', dnsConfig.fqdn, subdomain, type, fqdn);
superagent
.get(config.apiServerOrigin() + '/api/v1/domains/' + fqdn)
@@ -95,15 +77,26 @@ function get(domainObject, location, type, callback) {
});
}
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config;
debug('del: %s for zone %s of type %s with values %j', location, domainObject.domain, type, values);
add(dnsConfig, zoneName, subdomain, type, values, callback);
}
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug('del: %s for zone %s of type %s with values %j', subdomain, dnsConfig.fqdn, type, values);
var data = {
type: type,
@@ -111,7 +104,7 @@ function del(domainObject, location, type, values, callback) {
};
superagent
.del(config.apiServerOrigin() + '/api/v1/domains/' + getFqdn(location, domainObject.domain))
.del(config.apiServerOrigin() + '/api/v1/domains/' + getFqdn(subdomain, dnsConfig.fqdn))
.query({ token: dnsConfig.token })
.send(data)
.timeout(30 * 1000)
@@ -126,42 +119,29 @@ function del(domainObject, location, type, values, callback) {
});
}
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config;
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a non-empty string'));
const ip = '127.0.0.1';
var credentials = {
token: dnsConfig.token,
fqdn: domain,
hyphenatedSubdomains: true // this will ensure we always use them, regardless of passed-in configs
};
const location = 'cloudrontestdns';
const testSubdomain = 'cloudrontestdns';
upsert(domainObject, location, 'A', [ ip ], function (error) {
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added');
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
del(domainObject, location, 'A', [ ip ], function (error) {
del(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
+71 -119
View File
@@ -1,12 +1,10 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
wait: wait,
waitForDns: require('./waitfordns.js'),
verifyDnsConfig: verifyDnsConfig
};
@@ -14,25 +12,14 @@ var assert = require('assert'),
async = require('async'),
debug = require('debug')('box:dns/cloudflare'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
superagent = require('superagent'),
util = require('util'),
waitForDns = require('./waitfordns.js'),
_ = require('underscore');
// we are using latest v4 stable API https://api.cloudflare.com/#getting-started-endpoints
var CLOUDFLARE_ENDPOINT = 'https://api.cloudflare.com/client/v4';
function removePrivateFields(domainObject) {
domainObject.config.token = domains.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
function translateRequestError(result, callback) {
assert.strictEqual(typeof result, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -42,6 +29,11 @@ function translateRequestError(result, callback) {
if ((result.statusCode === 400 || result.statusCode === 401 || result.statusCode === 403) && result.body.errors.length > 0) {
let error = result.body.errors[0];
let message = `message: ${error.message} statusCode: ${result.statusCode} code:${error.code}`;
if (error.code === 6003) {
if (error.error_chain[0] && error.error_chain[0].code === 6103) message = 'Invalid API Key';
else message = 'Invalid credentials';
}
return callback(new DomainsError(DomainsError.ACCESS_DENIED, message));
}
@@ -66,14 +58,16 @@ function getZoneByName(dnsConfig, zoneName, callback) {
});
}
// gets records filtered by zone, type and fqdn
function getDnsRecords(dnsConfig, zoneId, fqdn, type, callback) {
function getDnsRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneId, 'string');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
var fqdn = subdomain === '' ? zoneName : subdomain + '.' + zoneName;
superagent.get(CLOUDFLARE_ENDPOINT + '/zones/' + zoneId + '/dns_records')
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
@@ -89,34 +83,36 @@ function getDnsRecords(dnsConfig, zoneId, fqdn, type, callback) {
});
}
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
var fqdn = subdomain === '' ? zoneName : subdomain + '.' + zoneName;
debug('upsert: %s for zone %s of type %s with values %j', fqdn, zoneName, type, values);
debug('upsert: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
getZoneByName(dnsConfig, zoneName, function(error, result) {
getZoneByName(dnsConfig, zoneName, function(error, result){
if (error) return callback(error);
let zoneId = result.id;
var zoneId = result.id;
getDnsRecords(dnsConfig, zoneId, fqdn, type, function (error, dnsRecords) {
getDnsRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, function (error, result) {
if (error) return callback(error);
let i = 0; // // used to track available records to update instead of create
var dnsRecords = result;
async.eachSeries(values, function (value, iteratorCallback) {
// used to track available records to update instead of create
var i = 0;
async.eachSeries(values, function (value, callback) {
var priority = null;
if (type === 'MX') {
priority = parseInt(value.split(' ')[0], 10);
priority = value.split(' ')[0];
value = value.split(' ')[1];
}
@@ -125,41 +121,35 @@ function upsert(domainObject, location, type, values, callback) {
name: fqdn,
content: value,
priority: priority,
proxied: false,
ttl: 120 // 1 means "automatic" (meaning 300ms) and 120 is the lowest supported
};
if (i >= dnsRecords.length) { // create a new record
debug(`upsert: Adding new record fqdn: ${fqdn}, zoneName: ${zoneName} proxied: false`);
superagent.post(CLOUDFLARE_ENDPOINT + '/zones/' + zoneId + '/dns_records')
.set('X-Auth-Key', dnsConfig.token)
.set('X-Auth-Email', dnsConfig.email)
if (i >= dnsRecords.length) {
superagent.post(CLOUDFLARE_ENDPOINT + '/zones/'+ zoneId + '/dns_records')
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
.send(data)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return iteratorCallback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, iteratorCallback);
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
iteratorCallback(null);
callback(null);
});
} else { // replace existing record
data.proxied = dnsRecords[i].proxied; // preserve proxied parameter
debug(`upsert: Updating existing record fqdn: ${fqdn}, zoneName: ${zoneName} proxied: ${data.proxied}`);
superagent.put(CLOUDFLARE_ENDPOINT + '/zones/' + zoneId + '/dns_records/' + dnsRecords[i].id)
.set('X-Auth-Key', dnsConfig.token)
.set('X-Auth-Email', dnsConfig.email)
} else {
superagent.put(CLOUDFLARE_ENDPOINT + '/zones/'+ zoneId + '/dns_records/' + dnsRecords[i].id)
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
.send(data)
.timeout(30 * 1000)
.end(function (error, result) {
++i; // increment, as we have consumed the record
// increment, as we have consumed the record
++i;
if (error && !error.response) return iteratorCallback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, iteratorCallback);
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
iteratorCallback(null);
callback(null);
});
}
}, callback);
@@ -167,20 +157,17 @@ function upsert(domainObject, location, type, values, callback) {
});
}
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
getZoneByName(dnsConfig, zoneName, function(error, zone) {
getZoneByName(dnsConfig, zoneName, function(error, result){
if (error) return callback(error);
getDnsRecords(dnsConfig, zone.id, fqdn, type, function (error, result) {
getDnsRecordsByZoneId(dnsConfig, result.id, zoneName, subdomain, type, function(error, result) {
if (error) return callback(error);
var tmp = result.map(function (record) { return record.content; });
@@ -191,21 +178,18 @@ function get(domainObject, location, type, callback) {
});
}
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
getZoneByName(dnsConfig, zoneName, function(error, zone) {
getZoneByName(dnsConfig, zoneName, function(error, result){
if (error) return callback(error);
getDnsRecords(dnsConfig, zone.id, fqdn, type, function(error, result) {
getDnsRecordsByZoneId(dnsConfig, result.id, zoneName, subdomain, type, function(error, result) {
if (error) return callback(error);
if (result.length === 0) return callback(null);
@@ -218,8 +202,8 @@ function del(domainObject, location, type, values, callback) {
async.eachSeries(tmp, function (record, callback) {
superagent.del(CLOUDFLARE_ENDPOINT + '/zones/'+ zoneId + '/dns_records/' + record.id)
.set('X-Auth-Key', dnsConfig.token)
.set('X-Auth-Email', dnsConfig.email)
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
@@ -238,53 +222,21 @@ function del(domainObject, location, type, values, callback) {
});
}
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
debug('wait: %s for zone %s of type %s', fqdn, zoneName, type);
getZoneByName(dnsConfig, zoneName, function(error, result) {
if (error) return callback(error);
let zoneId = result.id;
getDnsRecords(dnsConfig, zoneId, fqdn, type, function (error, dnsRecords) {
if (error) return callback(error);
if (dnsRecords.length === 0) return callback(new DomainsError(DomainsError.NOT_FOUND, 'Domain not found'));
if (!dnsRecords[0].proxied) return waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
debug('wait: skipping wait of proxied domain');
callback(null); // maybe we can check for dns to be cloudflare IPs? https://api.cloudflare.com/#cloudflare-ips-cloudflare-ip-details
});
});
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName;
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a non-empty string'));
if (!dnsConfig.email || typeof dnsConfig.email !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'email must be a non-empty string'));
const ip = '127.0.0.1';
if ('hyphenatedSubdomains' in dnsConfig && typeof dnsConfig.hyphenatedSubdomains !== 'boolean') return callback(new DomainsError(DomainsError.BAD_FIELD, 'hyphenatedSubdomains must be a boolean'));
var credentials = {
token: dnsConfig.token,
email: dnsConfig.email
email: dnsConfig.email,
hyphenatedSubdomains: !!dnsConfig.hyphenatedSubdomains
};
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
@@ -293,22 +245,22 @@ function verifyDnsConfig(domainObject, callback) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
getZoneByName(dnsConfig, zoneName, function(error, zone) {
getZoneByName(dnsConfig, zoneName, function(error, result) {
if (error) return callback(error);
if (!_.isEqual(zone.name_servers.sort(), nameservers.sort())) {
debug('verifyDnsConfig: %j and %j do not match', nameservers, zone.name_servers);
if (!_.isEqual(result.name_servers.sort(), nameservers.sort())) {
debug('verifyDnsConfig: %j and %j do not match', nameservers, result.name_servers);
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Cloudflare'));
}
const location = 'cloudrontestdns';
const testSubdomain = 'cloudrontestdns';
upsert(domainObject, location, 'A', [ ip ], function (error) {
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added');
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
del(domainObject, location, 'A', [ ip ], function (error) {
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
+37 -66
View File
@@ -1,12 +1,10 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
wait: wait,
waitForDns: require('./waitfordns.js'),
verifyDnsConfig: verifyDnsConfig
};
@@ -14,12 +12,10 @@ var assert = require('assert'),
async = require('async'),
debug = require('debug')('box:dns/digitalocean'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
safe = require('safetydance'),
superagent = require('superagent'),
util = require('util'),
waitForDns = require('./waitfordns.js');
util = require('util');
var DIGITALOCEAN_ENDPOINT = 'https://api.digitalocean.com';
@@ -27,19 +23,10 @@ function formatError(response) {
return util.format('DigitalOcean DNS error [%s] %j', response.statusCode, response.body);
}
function removePrivateFields(domainObject) {
domainObject.config.token = domains.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
function getInternal(dnsConfig, zoneName, name, type, callback) {
function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -58,7 +45,7 @@ function getInternal(dnsConfig, zoneName, name, type, callback) {
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
matchingRecords = matchingRecords.concat(result.body.domain_records.filter(function (record) {
return (record.type === type && record.name === name);
return (record.type === type && record.name === subdomain);
}));
nextPage = (result.body.links && result.body.links.pages) ? result.body.links.pages.next : null;
@@ -74,20 +61,19 @@ function getInternal(dnsConfig, zoneName, name, type, callback) {
});
}
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
subdomain = subdomain || '@';
debug('upsert: %s for zone %s of type %s with values %j', name, zoneName, type, values);
debug('upsert: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
getInternal(dnsConfig, zoneName, name, type, function (error, result) {
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
if (error) return callback(error);
// used to track available records to update instead of create
@@ -103,7 +89,7 @@ function upsert(domainObject, location, type, values, callback) {
var data = {
type: type,
name: name,
name: subdomain,
data: value,
priority: priority,
ttl: 1
@@ -147,17 +133,16 @@ function upsert(domainObject, location, type, values, callback) {
});
}
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
subdomain = subdomain || '@';
getInternal(dnsConfig, zoneName, name, type, function (error, result) {
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
if (error) return callback(error);
// We only return the value string
@@ -169,18 +154,17 @@ function get(domainObject, location, type, callback) {
});
}
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
subdomain = subdomain || '@';
getInternal(dnsConfig, zoneName, name, type, function (error, result) {
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
if (error) return callback(error);
if (result.length === 0) return callback(null);
@@ -209,32 +193,19 @@ function del(domainObject, location, type, values, callback) {
});
}
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName;
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a non-empty string'));
const ip = '127.0.0.1';
if ('hyphenatedSubdomains' in dnsConfig && typeof dnsConfig.hyphenatedSubdomains !== 'boolean') return callback(new DomainsError(DomainsError.BAD_FIELD, 'hyphenatedSubdomains must be a boolean'));
var credentials = {
token: dnsConfig.token
token: dnsConfig.token,
hyphenatedSubdomains: !!dnsConfig.hyphenatedSubdomains
};
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
@@ -248,14 +219,14 @@ function verifyDnsConfig(domainObject, callback) {
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Digital Ocean'));
}
const location = 'cloudrontestdns';
const testSubdomain = 'cloudrontestdns';
upsert(domainObject, location, 'A', [ ip ], function (error) {
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added');
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
del(domainObject, location, 'A', [ ip ], function (error) {
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
+35 -64
View File
@@ -1,23 +1,19 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
wait: wait,
waitForDns: require('./waitfordns.js'),
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
debug = require('debug')('box:dns/gandi'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
superagent = require('superagent'),
util = require('util'),
waitForDns = require('./waitfordns.js');
util = require('util');
var GANDI_API = 'https://dns.api.gandi.net/api/v5';
@@ -25,34 +21,24 @@ function formatError(response) {
return util.format(`Gandi DNS error [${response.statusCode}] ${response.body.message}`);
}
function removePrivateFields(domainObject) {
domainObject.config.token = domains.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
subdomain = subdomain || '@';
debug(`upsert: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
debug(`upsert: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
var data = {
'rrset_ttl': 300, // this is the minimum allowed
'rrset_values': values // for mx records, value is already of the '<priority> <server>' format
};
superagent.put(`${GANDI_API}/domains/${zoneName}/records/${name}/${type}`)
superagent.put(`${GANDI_API}/domains/${zoneName}/records/${subdomain}/${type}`)
.set('X-Api-Key', dnsConfig.token)
.timeout(30 * 1000)
.send(data)
@@ -66,19 +52,18 @@ function upsert(domainObject, location, type, values, callback) {
});
}
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
subdomain = subdomain || '@';
debug(`get: ${name} in zone ${zoneName} of type ${type}`);
debug(`get: ${subdomain} in zone ${zoneName} of type ${type}`);
superagent.get(`${GANDI_API}/domains/${zoneName}/records/${name}/${type}`)
superagent.get(`${GANDI_API}/domains/${zoneName}/records/${subdomain}/${type}`)
.set('X-Api-Key', dnsConfig.token)
.timeout(30 * 1000)
.end(function (error, result) {
@@ -93,20 +78,19 @@ function get(domainObject, location, type, callback) {
});
}
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
subdomain = subdomain || '@';
debug(`del: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
debug(`del: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
superagent.del(`${GANDI_API}/domains/${zoneName}/records/${name}/${type}`)
superagent.del(`${GANDI_API}/domains/${zoneName}/records/${subdomain}/${type}`)
.set('X-Api-Key', dnsConfig.token)
.timeout(30 * 1000)
.end(function (error, result) {
@@ -121,34 +105,21 @@ function del(domainObject, location, type, values, callback) {
});
}
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName;
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a non-empty string'));
if ('hyphenatedSubdomains' in dnsConfig && typeof dnsConfig.hyphenatedSubdomains !== 'boolean') return callback(new DomainsError(DomainsError.BAD_FIELD, 'hyphenatedSubdomains must be a boolean'));
var credentials = {
token: dnsConfig.token
token: dnsConfig.token,
hyphenatedSubdomains: !!dnsConfig.hyphenatedSubdomains
};
const ip = '127.0.0.1';
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
@@ -160,14 +131,14 @@ function verifyDnsConfig(domainObject, callback) {
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Gandi'));
}
const location = 'cloudrontestdns';
const testSubdomain = 'cloudrontestdns';
upsert(domainObject, location, 'A', [ ip ], function (error) {
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added');
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
del(domainObject, location, 'A', [ ip ], function (error) {
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
+35 -67
View File
@@ -1,34 +1,21 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
wait: wait,
waitForDns: require('./waitfordns.js'),
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
debug = require('debug')('box:dns/gcdns'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
GCDNS = require('@google-cloud/dns'),
util = require('util'),
waitForDns = require('./waitfordns.js'),
_ = require('underscore');
function removePrivateFields(domainObject) {
domainObject.config.credentials.private_key = domains.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.credentials.private_key === domains.SECRET_PLACEHOLDER && currentConfig.credentials) newConfig.credentials.private_key = currentConfig.credentials.private_key;
}
function getDnsCredentials(dnsConfig) {
assert.strictEqual(typeof dnsConfig, 'object');
@@ -37,7 +24,8 @@ function getDnsCredentials(dnsConfig) {
credentials: {
client_email: dnsConfig.credentials.client_email,
private_key: dnsConfig.credentials.private_key
}
},
hyphenatedSubdomains: !!dnsConfig.hyphenatedSubdomains
};
}
@@ -68,23 +56,22 @@ function getZoneByName(dnsConfig, zoneName, callback) {
});
}
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
debug('add: %s for zone %s of type %s with values %j', fqdn, zoneName, type, values);
debug('add: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
getZoneByName(getDnsCredentials(dnsConfig), zoneName, function (error, zone) {
if (error) return callback(error);
zone.getRecords({ type: type, name: fqdn + '.' }, function (error, oldRecords) {
var domain = (subdomain ? subdomain + '.' : '') + zoneName + '.';
zone.getRecords({ type: type, name: domain }, function (error, oldRecords) {
if (error && error.code === 403) return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
if (error) {
debug('upsert->zone.getRecords', error);
@@ -92,12 +79,12 @@ function upsert(domainObject, location, type, values, callback) {
}
var newRecord = zone.record(type, {
name: fqdn + '.',
name: domain,
data: values,
ttl: 1
});
zone.createChange({ delete: oldRecords, add: newRecord }, function(error /*, change */) {
zone.createChange({ delete: oldRecords, add: newRecord }, function(error, change) {
if (error && error.code === 403) return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
if (error && error.code === 412) return callback(new DomainsError(DomainsError.STILL_BUSY, error.message));
if (error) {
@@ -111,21 +98,18 @@ function upsert(domainObject, location, type, values, callback) {
});
}
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
getZoneByName(getDnsCredentials(dnsConfig), zoneName, function (error, zone) {
if (error) return callback(error);
var params = {
name: fqdn + '.',
name: (subdomain ? subdomain + '.' : '') + zoneName + '.',
type: type
};
@@ -139,21 +123,20 @@ function get(domainObject, location, type, callback) {
});
}
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
getZoneByName(getDnsCredentials(dnsConfig), zoneName, function (error, zone) {
if (error) return callback(error);
zone.getRecords({ type: type, name: fqdn + '.' }, function(error, oldRecords) {
var domain = (subdomain ? subdomain + '.' : '') + zoneName + '.';
zone.getRecords({ type: type, name: domain }, function(error, oldRecords) {
if (error && error.code === 403) return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
if (error) {
debug('del->zone.getRecords', error);
@@ -174,35 +157,20 @@ function del(domainObject, location, type, values, callback) {
});
}
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName;
if (typeof dnsConfig.projectId !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'projectId must be a string'));
if (!dnsConfig.credentials || typeof dnsConfig.credentials !== 'object') return callback(new DomainsError(DomainsError.BAD_FIELD, 'credentials must be an object'));
if (typeof dnsConfig.credentials.client_email !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'credentials.client_email must be a string'));
if (typeof dnsConfig.credentials.private_key !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'credentials.private_key must be a string'));
if ('hyphenatedSubdomains' in dnsConfig && typeof dnsConfig.hyphenatedSubdomains !== 'boolean') return callback(new DomainsError(DomainsError.BAD_FIELD, 'hyphenatedSubdomains must be a boolean'));
var credentials = getDnsCredentials(dnsConfig);
const ip = '127.0.0.1';
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
@@ -218,14 +186,14 @@ function verifyDnsConfig(domainObject, callback) {
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Google Cloud DNS'));
}
const location = 'cloudrontestdns';
const testSubdomain = 'cloudrontestdns';
upsert(domainObject, location, 'A', [ ip ], function (error) {
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added');
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
del(domainObject, location, 'A', [ ip ], function (error) {
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
+38 -68
View File
@@ -1,23 +1,19 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
wait: wait,
waitForDns: require('./waitfordns.js'),
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
debug = require('debug')('box:dns/godaddy'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
superagent = require('superagent'),
util = require('util'),
waitForDns = require('./waitfordns.js');
util = require('util');
// const GODADDY_API_OTE = 'https://api.ote-godaddy.com/v1/domains';
const GODADDY_API = 'https://api.godaddy.com/v1/domains';
@@ -25,33 +21,22 @@ const GODADDY_API = 'https://api.godaddy.com/v1/domains';
// this is a workaround for godaddy not having a delete API
// https://stackoverflow.com/questions/39347464/delete-record-libcloud-godaddy-api
const GODADDY_INVALID_IP = '0.0.0.0';
const GODADDY_INVALID_TXT = '""';
function formatError(response) {
return util.format(`GoDaddy DNS error [${response.statusCode}] ${response.body.message}`);
}
function removePrivateFields(domainObject) {
domainObject.config.apiSecret = domains.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.apiSecret === domains.SECRET_PLACEHOLDER) newConfig.apiSecret = currentConfig.apiSecret;
}
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
subdomain = subdomain || '@';
debug(`upsert: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
debug(`upsert: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
var records = [ ];
values.forEach(function (value) {
@@ -67,7 +52,7 @@ function upsert(domainObject, location, type, values, callback) {
records.push(record);
});
superagent.put(`${GODADDY_API}/${zoneName}/records/${type}/${name}`)
superagent.put(`${GODADDY_API}/${zoneName}/records/${type}/${subdomain}`)
.set('Authorization', `sso-key ${dnsConfig.apiKey}:${dnsConfig.apiSecret}`)
.timeout(30 * 1000)
.send(records)
@@ -82,19 +67,18 @@ function upsert(domainObject, location, type, values, callback) {
});
}
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
subdomain = subdomain || '@';
debug(`get: ${name} in zone ${zoneName} of type ${type}`);
debug(`get: ${subdomain} in zone ${zoneName} of type ${type}`);
superagent.get(`${GODADDY_API}/${zoneName}/records/${type}/${name}`)
superagent.get(`${GODADDY_API}/${zoneName}/records/${type}/${subdomain}`)
.set('Authorization', `sso-key ${dnsConfig.apiKey}:${dnsConfig.apiSecret}`)
.timeout(30 * 1000)
.end(function (error, result) {
@@ -113,33 +97,32 @@ function get(domainObject, location, type, callback) {
});
}
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
subdomain = subdomain || '@';
debug(`get: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
debug(`get: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
if (type !== 'A' && type !== 'TXT') return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, new Error('Record deletion is not supported by GoDaddy API')));
if (type !== 'A') return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, new Error('Not supported by GoDaddy API'))); // can never happen
// check if the record exists at all so that we don't insert the "Dead" record for no reason
get(domainObject, location, type, function (error, values) {
get(dnsConfig, zoneName, subdomain, type, function (error, values) {
if (error) return callback(error);
if (values.length === 0) return callback();
// godaddy does not have a delete API. so fill it up with an invalid IP that we can ignore in future get()
var records = [{
ttl: 600,
data: type === 'A' ? GODADDY_INVALID_IP : GODADDY_INVALID_TXT
data: GODADDY_INVALID_IP
}];
superagent.put(`${GODADDY_API}/${zoneName}/records/${type}/${name}`)
superagent.put(`${GODADDY_API}/${zoneName}/records/${type}/${subdomain}`)
.set('Authorization', `sso-key ${dnsConfig.apiKey}:${dnsConfig.apiSecret}`)
.send(records)
.timeout(30 * 1000)
@@ -156,34 +139,21 @@ function del(domainObject, location, type, values, callback) {
});
}
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName;
if (!dnsConfig.apiKey || typeof dnsConfig.apiKey !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'apiKey must be a non-empty string'));
if (!dnsConfig.apiSecret || typeof dnsConfig.apiSecret !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'apiSecret must be a non-empty string'));
const ip = '127.0.0.1';
if ('hyphenatedSubdomains' in dnsConfig && typeof dnsConfig.hyphenatedSubdomains !== 'boolean') return callback(new DomainsError(DomainsError.BAD_FIELD, 'hyphenatedSubdomains must be a boolean'));
var credentials = {
apiKey: dnsConfig.apiKey,
apiSecret: dnsConfig.apiSecret
apiSecret: dnsConfig.apiSecret,
hyphenatedSubdomains: !!dnsConfig.hyphenatedSubdomains
};
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
@@ -197,14 +167,14 @@ function verifyDnsConfig(domainObject, callback) {
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to GoDaddy'));
}
const location = 'cloudrontestdns';
const testSubdomain = 'cloudrontestdns';
upsert(domainObject, location, 'A', [ ip ], function (error) {
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added');
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
del(domainObject, location, 'A', [ ip ], function (error) {
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
+19 -34
View File
@@ -7,30 +7,21 @@
// -------------------------------------------
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
wait: wait,
waitForDns: require('./waitfordns.js'),
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
DomainsError = require('../domains.js').DomainsError,
util = require('util');
function removePrivateFields(domainObject) {
// in-place removal of tokens and api keys with domains.SECRET_PLACEHOLDER
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
// in-place injection of tokens and api keys which came in with domains.SECRET_PLACEHOLDER
}
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
@@ -40,9 +31,10 @@ function upsert(domainObject, location, type, values, callback) {
callback(new Error('not implemented'));
}
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -51,9 +43,10 @@ function get(domainObject, location, type, callback) {
callback(new Error('not implemented'));
}
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
@@ -63,19 +56,11 @@ function del(domainObject, location, type, values, callback) {
callback(new Error('not implemented'));
}
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
callback();
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
// Result: dnsConfig object
+27 -40
View File
@@ -1,55 +1,46 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
wait: wait,
waitForDns: require('./waitfordns.js'),
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
debug = require('debug')('box:dns/manual'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
util = require('util'),
waitForDns = require('./waitfordns.js');
util = require('util');
function removePrivateFields(domainObject) {
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
}
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug('upsert: %s for zone %s of type %s with values %j', location, domainObject.zoneName, type, values);
debug('upsert: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
return callback(null);
}
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
callback(null, [ ]); // returning ip confuses apptask into thinking the entry already exists
}
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
@@ -57,30 +48,26 @@ function del(domainObject, location, type, values, callback) {
return callback();
}
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
if ('wildcard' in dnsConfig && typeof dnsConfig.wildcard !== 'boolean') return callback(new DomainsError(DomainsError.BAD_FIELD, 'wildcard must be a boolean'));
if ('hyphenatedSubdomains' in dnsConfig && typeof dnsConfig.hyphenatedSubdomains !== 'boolean') return callback(new DomainsError(DomainsError.BAD_FIELD, 'hyphenatedSubdomains must be a boolean'));
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const zoneName = domainObject.zoneName;
var config = {
wildcard: !!dnsConfig.wildcard,
hyphenatedSubdomains: !!dnsConfig.hyphenatedSubdomains
};
// Very basic check if the nameservers can be fetched
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
callback(null, {});
callback(null, config);
});
}
-297
View File
@@ -1,297 +0,0 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
verifyDnsConfig: verifyDnsConfig,
wait: wait
};
var assert = require('assert'),
debug = require('debug')('box:dns/namecheap'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
Namecheap = require('namecheap'),
sysinfo = require('../sysinfo.js'),
util = require('util'),
waitForDns = require('./waitfordns.js');
function formatError(response) {
return util.format('NameCheap DNS error [%s] %j', response.code, response.message);
}
function removePrivateFields(domainObject) {
domainObject.config.token = domains.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
// Only send required fields - https://www.namecheap.com/support/api/methods/domains-dns/set-hosts.aspx
function mapHosts(hosts) {
return hosts.map(function (host) {
let tmp = {};
tmp.TTL = '300';
tmp.RecordType = host.RecordType || host.Type;
tmp.HostName = host.HostName || host.Name;
tmp.Address = host.Address;
if (tmp.RecordType === 'MX') {
tmp.EmailType = 'MX';
if (host.MXPref) tmp.MXPref = host.MXPref;
}
return tmp;
});
}
function getApi(dnsConfig, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
// Note that for all NameCheap calls to go through properly, the public IP returned by the getPublicIp method below must be whitelisted on NameCheap's API dashboard
let namecheap = new Namecheap(dnsConfig.username, dnsConfig.token, ip);
namecheap.setUsername(dnsConfig.username);
callback(null, namecheap);
});
}
function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
getApi(dnsConfig, function (error, namecheap) {
if (error) return callback(error);
namecheap.domains.dns.getHosts(zoneName, function (error, result) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(error)));
debug('entire getInternal response: %j', result);
return callback(null, result['DomainDNSGetHostsResult']['host']);
});
});
}
function setInternal(dnsConfig, zoneName, hosts, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert(Array.isArray(hosts));
assert.strictEqual(typeof callback, 'function');
let mappedHosts = mapHosts(hosts);
getApi(dnsConfig, function (error, namecheap) {
if (error) return callback(error);
namecheap.domains.dns.setHosts(zoneName, mappedHosts, function (error, result) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(error)));
return callback(null, result);
});
});
}
function upsert(domainObject, subdomain, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config;
const zoneName = domainObject.zoneName;
subdomain = domains.getName(domainObject, subdomain, type) || '@';
debug('upsert: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
if (error) return callback(error);
// Array to keep track of records that need to be inserted
let toInsert = [];
for (var i = 0; i < values.length; i++) {
let curValue = values[i];
let wasUpdate = false;
for (var j = 0; j < result.length; j++) {
let curHost = result[j];
if (curHost.Type === type && curHost.Name === subdomain) {
// Updating an already existing host
wasUpdate = true;
if (type === 'MX') {
curHost.MXPref = curValue.split(' ')[0];
curHost.Address = curValue.split(' ')[1];
} else {
curHost.Address = curValue;
}
}
}
// We don't have this host at all yet, let's push to toInsert array
if (!wasUpdate) {
let newRecord = {
RecordType: type,
HostName: subdomain,
Address: curValue
};
// Special case for MX records
if (type === 'MX') {
newRecord.MXPref = curValue.split(' ')[0];
newRecord.Address = curValue.split(' ')[1];
}
toInsert.push(newRecord);
}
}
let toUpsert = result.concat(toInsert);
setInternal(dnsConfig, zoneName, toUpsert, callback);
});
}
function get(domainObject, subdomain, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config;
const zoneName = domainObject.zoneName;
subdomain = domains.getName(domainObject, subdomain, type) || '@';
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
if (error) return callback(error);
// We need to filter hosts to ones with this subdomain and type
let actualHosts = result.filter((host) => host.Type === type && host.Name === subdomain);
// We only return the value string
var tmp = actualHosts.map(function (record) { return record.Address; });
debug('get: %j', tmp);
return callback(null, tmp);
});
}
function del(domainObject, subdomain, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config;
const zoneName = domainObject.zoneName;
subdomain = domains.getName(domainObject, subdomain, type) || '@';
debug('del: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
if (error) return callback(error);
if (result.length === 0) return callback();
let removed = false;
for (var i = 0; i < values.length; i++) {
let curValue = values[i];
for (var j = 0; j < result.length; j++) {
let curHost = result[i];
if (curHost.Type === type && curHost.Name === subdomain && curHost.Address === curValue) {
removed = true;
result.splice(i, 1); // Remove element from result array
}
}
}
// Only set hosts if we actually removed a host
if (removed) return setInternal(dnsConfig, zoneName, result, callback);
callback();
});
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config;
const zoneName = domainObject.zoneName;
const ip = '127.0.0.1';
if (!dnsConfig.username || typeof dnsConfig.username !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'username must be a non-empty string'));
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a non-empty string'));
let credentials = {
username: dnsConfig.username,
token: dnsConfig.token
};
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
if (nameservers.some(function (n) { return n.toLowerCase().indexOf('.registrar-servers.com') === -1; })) {
debug('verifyDnsConfig: %j does not contains NC NS', nameservers);
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to NameCheap'));
}
const testSubdomain = 'cloudrontestdns';
upsert(domainObject, testSubdomain, 'A', [ip], function (error, changeId) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
del(domainObject, testSubdomain, 'A', [ip], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
callback(null, credentials);
});
});
});
}
function wait(domainObject, subdomain, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(subdomain, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}
+52 -82
View File
@@ -1,24 +1,19 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
wait: wait,
waitForDns: require('./waitfordns.js'),
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
debug = require('debug')('box:dns/namecom'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
safe = require('safetydance'),
superagent = require('superagent'),
util = require('util'),
waitForDns = require('./waitfordns.js');
DomainsError = require('../domains.js').DomainsError,
superagent = require('superagent');
const NAMECOM_API = 'https://api.name.com/v4';
@@ -26,27 +21,18 @@ function formatError(response) {
return `Name.com DNS error [${response.statusCode}] ${response.text}`;
}
function removePrivateFields(domainObject) {
domainObject.config.token = domains.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
function addRecord(dnsConfig, zoneName, name, type, values, callback) {
function addRecord(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug(`add: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
debug(`add: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
var data = {
host: name,
host: subdomain,
type: type,
ttl: 300 // 300 is the lowest
};
@@ -71,19 +57,19 @@ function addRecord(dnsConfig, zoneName, name, type, values, callback) {
});
}
function updateRecord(dnsConfig, zoneName, recordId, name, type, values, callback) {
function updateRecord(dnsConfig, zoneName, recordId, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof recordId, 'number');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug(`update:${recordId} on ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
debug(`update:${recordId} on ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
var data = {
host: name,
host: subdomain,
type: type,
ttl: 300 // 300 is the lowest
};
@@ -108,14 +94,16 @@ function updateRecord(dnsConfig, zoneName, recordId, name, type, values, callbac
});
}
function getInternal(dnsConfig, zoneName, name, type, callback) {
function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`getInternal: ${name} in zone ${zoneName} of type ${type}`);
subdomain = subdomain || '@';
debug(`getInternal: ${subdomain} in zone ${zoneName} of type ${type}`);
superagent.get(`${NAMECOM_API}/domains/${zoneName}/records`)
.auth(dnsConfig.username, dnsConfig.token)
@@ -135,7 +123,7 @@ function getInternal(dnsConfig, zoneName, name, type, callback) {
});
var results = result.body.records.filter(function (r) {
return (r.host === name && r.type === type);
return (r.host === subdomain && r.type === type);
});
debug('getInternal: %j', results);
@@ -144,39 +132,35 @@ function getInternal(dnsConfig, zoneName, name, type, callback) {
});
}
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
subdomain = subdomain || '@';
debug(`upsert: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
debug(`upsert: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
getInternal(dnsConfig, zoneName, name, type, function (error, result) {
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
if (error) return callback(error);
if (result.length === 0) return addRecord(dnsConfig, zoneName, name, type, values, callback);
if (result.length === 0) return addRecord(dnsConfig, zoneName, subdomain, type, values, callback);
return updateRecord(dnsConfig, zoneName, result[0].id, name, type, values, callback);
return updateRecord(dnsConfig, zoneName, result[0].id, subdomain, type, values, callback);
});
}
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
getInternal(dnsConfig, zoneName, name, type, function (error, result) {
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
if (error) return callback(error);
var tmp = result.map(function (record) { return record.answer; });
@@ -187,20 +171,19 @@ function get(domainObject, location, type, callback) {
});
}
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
subdomain = subdomain || '@';
debug(`del: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
debug(`del: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
getInternal(dnsConfig, zoneName, name, type, function (error, result) {
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
if (error) return callback(error);
if (result.length === 0) return callback();
@@ -218,36 +201,23 @@ function del(domainObject, location, type, values, callback) {
});
}
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName;
if (typeof dnsConfig.username !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'username must be a string'));
if (typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a string'));
if ('hyphenatedSubdomains' in dnsConfig && typeof dnsConfig.hyphenatedSubdomains !== 'boolean') return callback(new DomainsError(DomainsError.BAD_FIELD, 'hyphenatedSubdomains must be a boolean'));
var credentials = {
username: dnsConfig.username,
token: dnsConfig.token
token: dnsConfig.token,
hyphenatedSubdomains: !!dnsConfig.hyphenatedSubdomains
};
const ip = '127.0.0.1';
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
@@ -259,14 +229,14 @@ function verifyDnsConfig(domainObject, callback) {
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Name.com'));
}
const location = 'cloudrontestdns';
const testSubdomain = 'cloudrontestdns';
upsert(domainObject, location, 'A', [ ip ], function (error) {
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added');
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
del(domainObject, location, 'A', [ ip ], function (error) {
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
+22 -26
View File
@@ -1,12 +1,10 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
wait: wait,
waitForDns: waitForDns,
verifyDnsConfig: verifyDnsConfig
};
@@ -14,37 +12,33 @@ var assert = require('assert'),
debug = require('debug')('box:dns/noop'),
util = require('util');
function removePrivateFields(domainObject) {
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
}
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug('upsert: %s for zone %s of type %s with values %j', location, domainObject.zoneName, type, values);
debug('upsert: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
return callback(null);
}
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
callback(null, [ ]); // returning ip confuses apptask into thinking the entry already exists
}
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
@@ -52,10 +46,9 @@ function del(domainObject, location, type, values, callback) {
return callback();
}
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
function waitForDns(domain, zoneName, value, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
@@ -63,8 +56,11 @@ function wait(domainObject, location, type, value, options, callback) {
callback();
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
return callback(null, { });
+42 -61
View File
@@ -1,34 +1,24 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
wait: wait,
verifyDnsConfig: verifyDnsConfig
waitForDns: require('./waitfordns.js'),
verifyDnsConfig: verifyDnsConfig,
// not part of "dns" interface
getHostedZone: getHostedZone
};
var assert = require('assert'),
AWS = require('aws-sdk'),
debug = require('debug')('box:dns/route53'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
util = require('util'),
waitForDns = require('./waitfordns.js'),
_ = require('underscore');
function removePrivateFields(domainObject) {
domainObject.config.secretAccessKey = domains.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.secretAccessKey === domains.SECRET_PLACEHOLDER) newConfig.secretAccessKey = currentConfig.secretAccessKey;
}
function getDnsCredentials(dnsConfig) {
assert.strictEqual(typeof dnsConfig, 'object');
@@ -92,22 +82,20 @@ function getHostedZone(dnsConfig, zoneName, callback) {
});
}
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
function add(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
debug('add: %s for zone %s of type %s with values %j', fqdn, zoneName, type, values);
debug('add: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
getZoneByName(dnsConfig, zoneName, function (error, zone) {
if (error) return callback(error);
var fqdn = subdomain === '' ? zoneName : subdomain + '.' + zoneName;
var records = values.map(function (v) { return { Value: v }; }); // for mx records, value is already of the '<priority> <server>' format
var params = {
@@ -138,15 +126,23 @@ function upsert(domainObject, location, type, values, callback) {
});
}
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
add(dnsConfig, zoneName, subdomain, type, values, callback);
}
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
getZoneByName(dnsConfig, zoneName, function (error, zone) {
if (error) return callback(error);
@@ -154,7 +150,7 @@ function get(domainObject, location, type, callback) {
var params = {
HostedZoneId: zone.Id,
MaxItems: '1',
StartRecordName: fqdn + '.',
StartRecordName: (subdomain ? subdomain + '.' : '') + zoneName + '.',
StartRecordType: type
};
@@ -173,20 +169,18 @@ function get(domainObject, location, type, callback) {
});
}
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
getZoneByName(dnsConfig, zoneName, function (error, zone) {
if (error) return callback(error);
var fqdn = subdomain === '' ? zoneName : subdomain + '.' + zoneName;
var records = values.map(function (v) { return { Value: v }; });
var resourceRecordSet = {
@@ -232,28 +226,16 @@ function del(domainObject, location, type, values, callback) {
});
}
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName;
if (!dnsConfig.accessKeyId || typeof dnsConfig.accessKeyId !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'accessKeyId must be a non-empty string'));
if (!dnsConfig.secretAccessKey || typeof dnsConfig.secretAccessKey !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'secretAccessKey must be a non-empty string'));
if ('hyphenatedSubdomains' in dnsConfig && typeof dnsConfig.hyphenatedSubdomains !== 'boolean') return callback(new DomainsError(DomainsError.BAD_FIELD, 'hyphenatedSubdomains must be a boolean'));
var credentials = {
accessKeyId: dnsConfig.accessKeyId,
@@ -261,10 +243,9 @@ function verifyDnsConfig(domainObject, callback) {
region: dnsConfig.region || 'us-east-1',
endpoint: dnsConfig.endpoint || null,
listHostedZonesByName: true, // new/updated creds require this perm
hyphenatedSubdomains: !!dnsConfig.hyphenatedSubdomains
};
const ip = '127.0.0.1';
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
@@ -279,14 +260,14 @@ function verifyDnsConfig(domainObject, callback) {
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Route53'));
}
const location = 'cloudrontestdns';
const testSubdomain = 'cloudrontestdns';
upsert(domainObject, location, 'A', [ ip ], function (error) {
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added');
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
del(domainObject, location, 'A', [ ip ], function (error) {
del(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
+14 -26
View File
@@ -30,9 +30,8 @@ function resolveIp(hostname, options, callback) {
});
}
function isChangeSynced(hostname, type, value, nameserver, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof type, 'string');
function isChangeSynced(domain, value, nameserver, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof value, 'string');
assert.strictEqual(typeof nameserver, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -45,30 +44,20 @@ function isChangeSynced(hostname, type, value, nameserver, callback) {
}
async.every(nsIps, function (nsIp, iteratorCallback) {
const resolveOptions = { server: nsIp, timeout: 5000 };
const resolver = type === 'A' ? resolveIp.bind(null, hostname) : dns.resolve.bind(null, hostname, 'TXT');
resolver(resolveOptions, function (error, answer) {
resolveIp(domain, { server: nsIp, timeout: 5000 }, function (error, answer) {
if (error && error.code === 'TIMEOUT') {
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) timed out when resolving ${hostname} (${type})`);
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) timed out when resolving ${domain}`);
return iteratorCallback(null, true); // should be ok if dns server is down
}
if (error) {
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) errored when resolve ${hostname} (${type}): ${error}`);
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) errored when resolve ${domain}: ${error}`);
return iteratorCallback(null, false);
}
let match;
if (type === 'A') {
match = answer.length === 1 && answer[0] === value;
} else if (type === 'TXT') { // answer is a 2d array of strings
match = answer.some(function (a) { return value === a.join(''); });
}
debug(`isChangeSynced: ${domain} was resolved to ${answer} at NS ${nameserver} (${nsIp}). Expecting ${value}`);
debug(`isChangeSynced: ${hostname} (${type}) was resolved to ${answer} at NS ${nameserver} (${nsIp}). Expecting ${value}. Match ${match}`);
iteratorCallback(null, match);
iteratorCallback(null, answer.length === 1 && answer[0] === value);
});
}, callback);
@@ -76,26 +65,25 @@ function isChangeSynced(hostname, type, value, nameserver, callback) {
}
// check if IP change has propagated to every nameserver
function waitForDns(hostname, zoneName, type, value, options, callback) {
assert.strictEqual(typeof hostname, 'string');
function waitForDns(domain, zoneName, value, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert(type === 'A' || type === 'TXT');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
debug('waitForDns: hostname %s to be %s in zone %s.', hostname, value, zoneName);
debug('waitForDns: domain %s to be %s in zone %s.', domain, value, zoneName);
var attempt = 0;
async.retry(options, function (retryCallback) {
++attempt;
debug(`waitForDns (try ${attempt}): ${hostname} to be ${value} in zone ${zoneName}`);
debug(`waitForDns (try ${attempt}): ${domain} to be ${value} in zone ${zoneName}`);
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error || !nameservers) return retryCallback(error || new DomainsError(DomainsError.EXTERNAL_ERROR, 'Unable to get nameservers'));
async.every(nameservers, isChangeSynced.bind(null, hostname, type, value), function (error, synced) {
debug('waitForDns: %s %s ns: %j', hostname, synced ? 'done' : 'not done', nameservers);
async.every(nameservers, isChangeSynced.bind(null, domain, value), function (error, synced) {
debug('waitForDns: %s %s ns: %j', domain, synced ? 'done' : 'not done', nameservers);
retryCallback(synced ? null : new DomainsError(DomainsError.EXTERNAL_ERROR, 'ETRYAGAIN'));
});
@@ -103,7 +91,7 @@ function waitForDns(hostname, zoneName, type, value, options, callback) {
}, function retryDone(error) {
if (error) return callback(error);
debug(`waitForDns: ${hostname} has propagated`);
debug(`waitForDns: ${domain} has propagated`);
callback(null);
});
-100
View File
@@ -1,100 +0,0 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
wait: wait,
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
debug = require('debug')('box:dns/manual'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
sysinfo = require('../sysinfo.js'),
util = require('util'),
waitForDns = require('./waitfordns.js');
function removePrivateFields(domainObject) {
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
}
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug('upsert: %s for zone %s of type %s with values %j', location, domainObject.zoneName, type, values);
return callback(null);
}
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
callback(null, [ ]); // returning ip confuses apptask into thinking the entry already exists
}
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
return callback();
}
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const zoneName = domainObject.zoneName;
// Very basic check if the nameservers can be fetched
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
const location = 'cloudrontestdns';
const fqdn = domains.fqdn(location, domainObject);
dns.resolve(fqdn, 'A', { server: '127.0.0.1', timeout: 5000 }, function (error, result) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, `Unable to resolve ${fqdn}`));
if (error || !result) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : `Unable to resolve ${fqdn}`));
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, `Failed to detect IP of this server: ${error.message}`));
if (result.length !== 1 || ip !== result[0]) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, `Domain resolves to ${JSON.stringify(result)} instead of ${ip}`));
callback(null, {});
});
});
});
}
+17 -176
View File
@@ -1,13 +1,7 @@
'use strict';
exports = module.exports = {
DockerError: DockerError,
connection: connectionInstance(),
setRegistryConfig: setRegistryConfig,
ping: ping,
downloadImage: downloadImage,
createContainer: createContainer,
startContainer: startContainer,
@@ -22,26 +16,21 @@ exports = module.exports = {
getContainerIdByIp: getContainerIdByIp,
inspect: inspect,
inspectByName: inspect,
memoryUsage: memoryUsage,
execContainer: execContainer,
createVolume: createVolume,
removeVolume: removeVolume,
clearVolume: clearVolume
execContainer: execContainer
};
// timeout is optional
function connectionInstance(timeout) {
function connectionInstance() {
var Docker = require('dockerode');
var docker;
if (process.env.BOX_ENV === 'test') {
// test code runs a docker proxy on this port
docker = new Docker({ host: 'http://localhost', port: 5687, timeout: timeout });
docker = new Docker({ host: 'http://localhost', port: 5687 });
// proxy code uses this to route to the real docker
docker.options = { socketPath: '/var/run/docker.sock' };
} else {
docker = new Docker({ socketPath: '/var/run/docker.sock', timeout: timeout });
docker = new Docker({ socketPath: '/var/run/docker.sock' });
}
return docker;
@@ -55,80 +44,24 @@ var addons = require('./addons.js'),
constants = require('./constants.js'),
debug = require('debug')('box:docker.js'),
once = require('once'),
path = require('path'),
safe = require('safetydance'),
shell = require('./shell.js'),
spawn = child_process.spawn,
util = require('util'),
_ = require('underscore');
const CLEARVOLUME_CMD = path.join(__dirname, 'scripts/clearvolume.sh'),
MKDIRVOLUME_CMD = path.join(__dirname, 'scripts/mkdirvolume.sh');
function DockerError(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;
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(DockerError, Error);
DockerError.INTERNAL_ERROR = 'Internal Error';
DockerError.NOT_FOUND = 'Not found';
DockerError.BAD_FIELD = 'Bad field';
function debugApp(app, args) {
assert(typeof app === 'object');
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
function setRegistryConfig(auth, callback) {
assert.strictEqual(typeof auth, 'object');
assert.strictEqual(typeof callback, 'function');
const isLogin = !!auth.password;
// currently, auth info is not stashed in the db but maybe it should for restore to work?
const cmd = isLogin ? `docker login ${auth.serveraddress} --username ${auth.username} --password ${auth.password}` : `docker logout ${auth.serveraddress}`;
child_process.exec(cmd, { }, function (error, stdout, stderr) {
if (error) return callback(new DockerError(DockerError.BAD_FIELD, stderr));
callback();
});
}
function ping(callback) {
assert.strictEqual(typeof callback, 'function');
// do not let the request linger
var docker = connectionInstance(1000);
docker.ping(function (error, result) {
if (error) return callback(new DockerError(DockerError.INTERNAL_ERROR, error));
if (result !== 'OK') return callback(new DockerError(DockerError.INTERNAL_ERROR, 'Unable to ping the docker daemon'));
callback(null);
});
}
function pullImage(manifest, callback) {
var docker = exports.connection;
// Use docker CLI here to support downloading of private repos. for dockerode, we have to use
// https://github.com/apocas/dockerode#pull-from-private-repos
shell.spawn('pullImage', '/usr/bin/docker', [ 'pull', manifest.dockerImage ], {}, function (error) {
shell.exec('pullImage', '/usr/bin/docker', [ 'pull', manifest.dockerImage ], { }, function (error) {
if (error) {
debug(`pullImage: Error pulling image ${manifest.dockerImage} of ${manifest.id}: ${error.message}`);
return callback(new Error('Failed to pull image'));
@@ -180,10 +113,8 @@ 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 = [
'CLOUDRON=1',
'CLOUDRON_PROXY_IP=172.18.0.1',
'WEBADMIN_ORIGIN=' + config.adminOrigin(),
'API_ORIGIN=' + config.adminOrigin(),
'APP_ORIGIN=https://' + domain,
@@ -209,9 +140,6 @@ function createSubcontainer(app, name, cmd, options, callback) {
dockerPortBindings[`${containerPort}/${portType}`] = [ { HostIp: '0.0.0.0', HostPort: hostPort + '' } ];
}
let appEnv = [];
Object.keys(app.env).forEach(function (name) { appEnv.push(`${name}=${app.env[name]}`); });
// first check db record, then manifest
var memoryLimit = app.memoryLimit || manifest.memoryLimit || 0;
@@ -225,6 +153,9 @@ function createSubcontainer(app, name, cmd, options, callback) {
// if required, we can make this a manifest and runtime argument later
if (!isAppContainer) memoryLimit *= 2;
// apparmor is disabled on few servers
var enableSecurityOpt = config.CLOUDRON && safe(function () { return child_process.spawnSync('aa-enabled').status === 0; }, false);
addons.getEnvironment(app, function (error, addonEnv) {
if (error) return callback(new Error('Error getting addon environment : ' + error));
@@ -236,10 +167,9 @@ function createSubcontainer(app, name, cmd, options, callback) {
var containerOptions = {
name: name, // used for filtering logs
Tty: isAppContainer,
Hostname: app.id, // set to something 'constant' so app containers can use this to communicate (across app updates)
Image: app.manifest.dockerImage,
Cmd: (isAppContainer && app.debugMode && app.debugMode.cmd) ? app.debugMode.cmd : cmd,
Env: stdEnv.concat(addonEnv).concat(portEnv).concat(appEnv),
Env: stdEnv.concat(addonEnv).concat(portEnv),
ExposedPorts: isAppContainer ? exposedPorts : { },
Volumes: { // see also ReadonlyRootfs
'/tmp': {},
@@ -248,11 +178,10 @@ function createSubcontainer(app, name, cmd, options, callback) {
Labels: {
'fqdn': app.fqdn,
'appId': app.id,
'isSubcontainer': String(!isAppContainer),
'isCloudronManaged': String(true)
'isSubcontainer': String(!isAppContainer)
},
HostConfig: {
Mounts: addons.getMountsSync(app, app.manifest.addons),
Binds: addons.getBindsSync(app, app.manifest.addons),
LogConfig: {
Type: 'syslog',
Config: {
@@ -275,7 +204,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
NetworkMode: 'cloudron',
Dns: ['172.18.0.1'], // use internal dns
DnsSearch: ['.'], // use internal dns
SecurityOpt: [ 'apparmor=docker-cloudron-app' ]
SecurityOpt: enableSecurityOpt ? [ 'apparmor=docker-cloudron-app' ] : null // profile available only on cloudron
}
};
@@ -371,19 +300,15 @@ function deleteContainer(containerId, callback) {
});
}
function deleteContainers(appId, options, callback) {
function deleteContainers(appId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var docker = exports.connection;
debug('deleting containers of %s', appId);
let labels = [ 'appId=' + appId ];
if (options.managedOnly) labels.push('isCloudronManaged=true');
docker.listContainers({ all: 1, filters: JSON.stringify({ label: labels }) }, function (error, containers) {
docker.listContainers({ all: 1, filters: JSON.stringify({ label: [ 'appId=' + appId ] }) }, function (error, containers) {
if (error) return callback(error);
async.eachSeries(containers, function (container, iteratorDone) {
@@ -427,8 +352,7 @@ function deleteImage(manifest, callback) {
// just removes the tag). we used to remove the image by id. this is not required anymore because aliases are
// not created anymore after https://github.com/docker/docker/pull/10571
docker.getImage(dockerImage).remove(removeOptions, function (error) {
if (error && error.statusCode === 400) return callback(null); // invalid image format. this can happen if user installed with a bad --docker-image
if (error && error.statusCode === 404) return callback(null); // not found
if (error && error.statusCode === 404) return callback(null);
if (error && error.statusCode === 409) return callback(null); // another container using the image
if (error) debug('Error removing image %s : %j', dockerImage, error);
@@ -467,23 +391,7 @@ function inspect(containerId, callback) {
var container = exports.connection.getContainer(containerId);
container.inspect(function (error, result) {
if (error && error.statusCode === 404) return callback(new DockerError(DockerError.NOT_FOUND));
if (error) return callback(new DockerError(DockerError.INTERNAL_ERROR, error));
callback(null, result);
});
}
function memoryUsage(containerId, callback) {
assert.strictEqual(typeof containerId, 'string');
assert.strictEqual(typeof callback, 'function');
var container = exports.connection.getContainer(containerId);
container.stats({ stream: false }, function (error, result) {
if (error && error.statusCode === 404) return callback(new DockerError(DockerError.NOT_FOUND));
if (error) return callback(new DockerError(DockerError.INTERNAL_ERROR, error));
if (error) return callback(error);
callback(null, result);
});
}
@@ -518,70 +426,3 @@ function execContainer(containerId, cmd, options, callback) {
if (options.stdin) options.stdin.pipe(cp.stdin).on('error', callback);
}
function createVolume(app, name, volumeDataDir, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof volumeDataDir, 'string');
assert.strictEqual(typeof callback, 'function');
let docker = exports.connection;
const volumeOptions = {
Name: name,
Driver: 'local',
DriverOpts: { // https://github.com/moby/moby/issues/19990#issuecomment-248955005
type: 'none',
device: volumeDataDir,
o: 'bind'
},
Labels: {
'fqdn': app.fqdn,
'appId': app.id
},
};
// requires sudo because the path can be outside appsdata
shell.sudo('createVolume', [ MKDIRVOLUME_CMD, volumeDataDir ], {}, function (error) {
if (error) return callback(new Error(`Error creating app data dir: ${error.message}`));
docker.createVolume(volumeOptions, function (error) {
if (error) return callback(error);
callback();
});
});
}
function clearVolume(app, name, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
let docker = exports.connection;
let volume = docker.getVolume(name);
volume.inspect(function (error, v) {
if (error && error.statusCode === 404) return callback();
if (error) return callback(error);
const volumeDataDir = v.Options.device;
shell.sudo('clearVolume', [ CLEARVOLUME_CMD, options.removeDirectory ? 'rmdir' : 'clear', volumeDataDir ], {}, callback);
});
}
// this only removes the volume and not the data
function removeVolume(app, name, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
let docker = exports.connection;
let volume = docker.getVolume(name);
volume.remove(function (error) {
if (error && error.statusCode !== 404) return callback(new Error(`removeVolume: Error removing volume of ${app.id} ${error.message}`));
callback();
});
}
+2 -2
View File
@@ -70,7 +70,7 @@ function attachDockerRequest(req, res, next) {
function containersCreate(req, res, next) {
safe.set(req.body, 'HostConfig.NetworkMode', 'cloudron'); // overwrite the network the container lives in
safe.set(req.body, 'NetworkingConfig', {}); // drop any custom network configs
safe.set(req.body, 'Labels', _.extend({ }, safe.query(req.body, 'Labels'), { appId: req.app.id, isCloudronManaged: String(false) })); // overwrite the app id to track containers of an app
safe.set(req.body, 'Labels', _.extend({ }, safe.query(req.body, 'Labels'), { appId: req.app.id })); // overwrite the app id to track containers of an app
safe.set(req.body, 'HostConfig.LogConfig', { Type: 'syslog', Config: { 'tag': req.app.id, 'syslog-address': 'udp://127.0.0.1:2514', 'syslog-format': 'rfc5424' }});
const appDataDir = path.join(paths.APPS_DATA_DIR, req.app.id, 'data'),
@@ -122,7 +122,7 @@ function start(callback) {
if (config.TEST) {
proxyServer.use(function (req, res, next) {
debug('proxying: ' + req.method, req.url);
console.log('Proxying: ' + req.method, req.url);
next();
});
}
+3 -4
View File
@@ -8,7 +8,8 @@ exports = module.exports = {
getAll: getAll,
update: update,
del: del,
clear: clear
_clear: clear
};
var assert = require('assert'),
@@ -16,7 +17,7 @@ var assert = require('assert'),
DatabaseError = require('./databaseerror'),
safe = require('safetydance');
var DOMAINS_FIELDS = [ 'domain', 'zoneName', 'provider', 'configJson', 'tlsConfigJson', 'locked' ].join(',');
var DOMAINS_FIELDS = [ 'domain', 'zoneName', 'provider', 'configJson', 'tlsConfigJson' ].join(',');
function postProcess(data) {
data.config = safe.JSON.parse(data.configJson);
@@ -24,8 +25,6 @@ function postProcess(data) {
delete data.configJson;
delete data.tlsConfigJson;
data.locked = !!data.locked; // make it bool
return data;
}
+135 -195
View File
@@ -6,10 +6,10 @@ module.exports = exports = {
getAll: getAll,
update: update,
del: del,
clear: clear,
isLocked: isLocked,
fqdn: fqdn,
getName: getName,
setAdmin: setAdmin,
getDnsRecords: getDnsRecords,
upsertDnsRecords: upsertDnsRecords,
@@ -22,33 +22,29 @@ module.exports = exports = {
validateHostname: validateHostname,
makeWildcard: makeWildcard,
parentDomain: parentDomain,
prepareDashboardDomain: prepareDashboardDomain,
DomainsError: DomainsError,
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8)
DomainsError: DomainsError
};
var assert = require('assert'),
async = require('async'),
caas = require('./caas.js'),
config = require('./config.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:domains'),
domaindb = require('./domaindb.js'),
eventlog = require('./eventlog.js'),
path = require('path'),
reverseProxy = require('./reverseproxy.js'),
ReverseProxyError = reverseProxy.ReverseProxyError,
safe = require('safetydance'),
shell = require('./shell.js'),
sysinfo = require('./sysinfo.js'),
tld = require('tldjs'),
util = require('util'),
_ = require('underscore');
var RESTART_CMD = path.join(__dirname, 'scripts/restart.sh');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function DomainsError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
@@ -77,7 +73,7 @@ DomainsError.STILL_BUSY = 'Still busy';
DomainsError.IN_USE = 'In Use';
DomainsError.INTERNAL_ERROR = 'Internal error';
DomainsError.ACCESS_DENIED = 'Access denied';
DomainsError.INVALID_PROVIDER = 'provider must be route53, gcdns, digitalocean, gandi, cloudflare, namecom, noop, wildcard, manual or caas';
DomainsError.INVALID_PROVIDER = 'provider must be route53, gcdns, digitalocean, gandi, cloudflare, namecom, noop, manual or caas';
// choose which subdomain backend we use for test purpose we use route53
function api(provider) {
@@ -92,42 +88,24 @@ function api(provider) {
case 'gandi': return require('./dns/gandi.js');
case 'godaddy': return require('./dns/godaddy.js');
case 'namecom': return require('./dns/namecom.js');
case 'namecheap': return require('./dns/namecheap.js');
case 'noop': return require('./dns/noop.js');
case 'manual': return require('./dns/manual.js');
case 'wildcard': return require('./dns/wildcard.js');
default: return null;
}
}
function parentDomain(domain) {
assert.strictEqual(typeof domain, 'string');
return domain.replace(/^\S+?\./, ''); // +? means non-greedy
}
function verifyDnsConfig(dnsConfig, domain, zoneName, provider, callback) {
assert(dnsConfig && typeof dnsConfig === 'object'); // the dns config to test with
function verifyDnsConfig(config, domain, zoneName, provider, ip, callback) {
assert(config && typeof config === 'object'); // the dns config to test with
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof provider, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
var backend = api(provider);
if (!backend) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid provider'));
if (!backend) return callback(new DomainsError(DomainsError.INVALID_PROVIDER));
const domainObject = { config: dnsConfig, domain: domain, zoneName: zoneName };
api(provider).verifyDnsConfig(domainObject, function (error, result) {
if (error && error.reason === DomainsError.ACCESS_DENIED) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Incorrect configuration. Access denied'));
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Zone not found'));
if (error && error.reason === DomainsError.EXTERNAL_ERROR) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Configuration error: ' + error.message));
if (error && error.reason === DomainsError.BAD_FIELD) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
if (error && error.reason === DomainsError.INVALID_PROVIDER) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
result.hyphenatedSubdomains = !!dnsConfig.hyphenatedSubdomains;
callback(null, result);
});
api(provider).verifyDnsConfig(config, domain, zoneName, ip, callback);
}
function fqdn(location, domainObject) {
@@ -173,39 +151,15 @@ function validateHostname(location, domainObject) {
return null;
}
function validateTlsConfig(tlsConfig, dnsProvider) {
assert.strictEqual(typeof tlsConfig, 'object');
assert.strictEqual(typeof dnsProvider, 'string');
switch (tlsConfig.provider) {
case 'letsencrypt-prod':
case 'letsencrypt-staging':
case 'fallback':
case 'caas':
break;
default:
return new DomainsError(DomainsError.BAD_FIELD, 'tlsConfig.provider must be caas, fallback, letsencrypt-prod/staging');
}
if (tlsConfig.wildcard) {
if (!tlsConfig.provider.startsWith('letsencrypt')) return new DomainsError(DomainsError.BAD_FIELD, 'wildcard can only be set with letsencrypt');
if (dnsProvider === 'manual' || dnsProvider === 'noop' || dnsProvider === 'wildcard') return new DomainsError(DomainsError.BAD_FIELD, 'wildcard cert requires a programmable DNS backend');
}
return null;
}
function add(domain, data, auditSource, callback) {
function add(domain, zoneName, provider, dnsConfig, fallbackCertificate, tlsConfig, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof data.zoneName, 'string');
assert.strictEqual(typeof data.provider, 'string');
assert.strictEqual(typeof data.config, 'object');
assert.strictEqual(typeof data.fallbackCertificate, 'object');
assert.strictEqual(typeof data.tlsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof provider, 'string');
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof fallbackCertificate, 'object');
assert.strictEqual(typeof tlsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
let { zoneName, provider, config, fallbackCertificate, tlsConfig } = data;
if (!tld.isValid(domain)) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid domain'));
if (domain.endsWith('.')) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid domain'));
@@ -217,34 +171,45 @@ function add(domain, data, auditSource, callback) {
}
if (fallbackCertificate) {
let error = reverseProxy.validateCertificate('test', { domain, config }, fallbackCertificate);
let error = reverseProxy.validateCertificate(`test.${domain}`, fallbackCertificate.cert, fallbackCertificate.key);
if (error) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
} else {
fallbackCertificate = reverseProxy.generateFallbackCertificateSync({ domain, config });
if (fallbackCertificate.error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, fallbackCertificate.error));
}
let error = validateTlsConfig(tlsConfig, provider);
if (error) return callback(error);
if (tlsConfig.provider !== 'fallback' && tlsConfig.provider !== 'caas' && tlsConfig.provider.indexOf('letsencrypt-') !== 0) {
return callback(new DomainsError(DomainsError.BAD_FIELD, 'tlsConfig.provider must be caas, fallback or le-*'));
}
verifyDnsConfig(config, domain, zoneName, provider, function (error, sanitizedConfig) {
if (error) return callback(error);
if (dnsConfig.hyphenatedSubdomains && !config.allowHyphenatedSubdomains()) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Not allowed in this edition'));
domaindb.add(domain, { zoneName: zoneName, provider: provider, config: sanitizedConfig, tlsConfig: tlsConfig }, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new DomainsError(DomainsError.ALREADY_EXISTS));
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
verifyDnsConfig(dnsConfig, domain, zoneName, provider, ip, function (error, result) {
if (error && error.reason === DomainsError.ACCESS_DENIED) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Error adding A record. Access denied'));
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Zone not found'));
if (error && error.reason === DomainsError.EXTERNAL_ERROR) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Error adding A record: ' + error.message));
if (error && error.reason === DomainsError.BAD_FIELD) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
if (error && error.reason === DomainsError.INVALID_PROVIDER) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
domaindb.add(domain, { zoneName: zoneName, provider: provider, config: result, tlsConfig: tlsConfig }, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new DomainsError(DomainsError.ALREADY_EXISTS));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_DOMAIN_ADD, auditSource, { domain, zoneName, provider });
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
callback();
callback();
});
});
});
});
}
function isLocked(domain) {
return domain === config.adminDomain() && config.isAdminDomainLocked();
}
function get(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -254,6 +219,8 @@ function get(domain, callback) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainsError(DomainsError.NOT_FOUND));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
result.locked = isLocked(domain);
reverseProxy.getFallbackCertificate(domain, function (error, bundle) {
if (error && error.reason !== ReverseProxyError.NOT_FOUND) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
@@ -275,73 +242,72 @@ function getAll(callback) {
domaindb.getAll(function (error, result) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
result.forEach(function (r) { r.locked = isLocked(r.domain); });
return callback(null, result);
});
}
function update(domain, data, auditSource, callback) {
function update(domain, zoneName, provider, dnsConfig, fallbackCertificate, tlsConfig, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof data.zoneName, 'string');
assert.strictEqual(typeof data.provider, 'string');
assert.strictEqual(typeof data.config, 'object');
assert.strictEqual(typeof data.fallbackCertificate, 'object');
assert.strictEqual(typeof data.tlsConfig, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof provider, 'string');
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof fallbackCertificate, 'object');
assert.strictEqual(typeof tlsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
let { zoneName, provider, config, fallbackCertificate, tlsConfig } = data;
domaindb.get(domain, function (error, domainObject) {
domaindb.get(domain, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainsError(DomainsError.NOT_FOUND));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
if (zoneName) {
if (!tld.isValid(zoneName)) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid zoneName'));
} else {
zoneName = domainObject.zoneName;
zoneName = result.zoneName;
}
if (fallbackCertificate) {
let error = reverseProxy.validateCertificate('test', domainObject, fallbackCertificate);
let error = reverseProxy.validateCertificate(`test.${domain}`, fallbackCertificate.cert, fallbackCertificate.key);
if (error) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
}
error = validateTlsConfig(tlsConfig, provider);
if (error) return callback(error);
if (tlsConfig.provider !== 'fallback' && tlsConfig.provider !== 'caas' && tlsConfig.provider.indexOf('letsencrypt-') !== 0) {
return callback(new DomainsError(DomainsError.BAD_FIELD, 'tlsConfig.provider must be caas, fallback or letsencrypt-*'));
}
if (provider === domainObject.provider) api(provider).injectPrivateFields(config, domainObject.config);
if (dnsConfig.hyphenatedSubdomains && !config.allowHyphenatedSubdomains()) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Not allowed in this edition'));
verifyDnsConfig(config, domain, zoneName, provider, function (error, sanitizedConfig) {
if (error) return callback(error);
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
let newData = {
config: sanitizedConfig,
zoneName: zoneName,
provider: provider,
tlsConfig: tlsConfig
};
domaindb.update(domain, newData, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainsError(DomainsError.NOT_FOUND));
verifyDnsConfig(dnsConfig, domain, zoneName, provider, ip, function (error, result) {
if (error && error.reason === DomainsError.ACCESS_DENIED) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Error adding A record. Access denied'));
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Zone not found'));
if (error && error.reason === DomainsError.EXTERNAL_ERROR) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Error adding A record:' + error.message));
if (error && error.reason === DomainsError.BAD_FIELD) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
if (error && error.reason === DomainsError.INVALID_PROVIDER) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
if (!fallbackCertificate) return callback();
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
domaindb.update(domain, { zoneName: zoneName, provider: provider, config: result, tlsConfig: tlsConfig }, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainsError(DomainsError.NOT_FOUND));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_DOMAIN_UPDATE, auditSource, { domain, zoneName, provider });
if (!fallbackCertificate) return callback();
callback();
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
callback();
});
});
});
});
});
}
function del(domain, auditSource, callback) {
function del(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
if (domain === config.adminDomain()) return callback(new DomainsError(DomainsError.IN_USE));
@@ -351,53 +317,38 @@ function del(domain, auditSource, callback) {
if (error && error.reason === DatabaseError.IN_USE) return callback(new DomainsError(DomainsError.IN_USE));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_DOMAIN_REMOVE, auditSource, { domain });
return callback(null);
});
}
function clear(callback) {
assert.strictEqual(typeof callback, 'function');
domaindb.clear(function (error) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
return callback(null);
});
}
// returns the 'name' that needs to be inserted into zone
function getName(domain, location, type) {
const part = domain.domain.slice(0, -domain.zoneName.length - 1);
function getName(domain, subdomain, type) {
// support special caas domains
if (domain.provider === 'caas') return subdomain;
if (location === '') return part;
if (domain.domain === domain.zoneName) return subdomain;
if (!domain.config.hyphenatedSubdomains) return part ? `${location}.${part}` : location;
var part = domain.domain.slice(0, -domain.zoneName.length - 1);
// hyphenatedSubdomains
if (type !== 'TXT') return `${location}-${part}`;
if (location.startsWith('_acme-challenge.')) {
return `${location}-${part}`;
} else if (location === '_acme-challenge') {
const up = part.replace(/^[^.]*\.?/, ''); // this gets the domain one level up
return up ? `${location}.${up}` : location;
if (subdomain === '') {
return part;
} else if (type === 'TXT') {
return `${subdomain}.${part}`;
} else {
return `${location}.${part}`;
return subdomain + (domain.config.hyphenatedSubdomains ? '-' : '.') + part;
}
}
function getDnsRecords(location, domain, type, callback) {
assert.strictEqual(typeof location, 'string');
function getDnsRecords(subdomain, domain, type, callback) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
get(domain, function (error, domainObject) {
get(domain, function (error, result) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
api(domainObject.provider).get(domainObject, location, type, function (error, values) {
api(result.provider).get(result.config, result.zoneName, getName(result, subdomain, type), type, function (error, values) {
if (error) return callback(error);
callback(null, values);
@@ -405,20 +356,19 @@ function getDnsRecords(location, domain, type, callback) {
});
}
// note: for TXT records the values must be quoted
function upsertDnsRecords(location, domain, type, values, callback) {
assert.strictEqual(typeof location, 'string');
function upsertDnsRecords(subdomain, domain, type, values, callback) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug('upsertDNSRecord: %s on %s type %s values', location, domain, type, values);
debug('upsertDNSRecord: %s on %s type %s values', subdomain, domain, type, values);
get(domain, function (error, domainObject) {
get(domain, function (error, result) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
api(domainObject.provider).upsert(domainObject, location, type, values, function (error) {
api(result.provider).upsert(result.config, result.zoneName, getName(result, subdomain, type), type, values, function (error) {
if (error) return callback(error);
callback(null);
@@ -426,19 +376,19 @@ function upsertDnsRecords(location, domain, type, values, callback) {
});
}
function removeDnsRecords(location, domain, type, values, callback) {
assert.strictEqual(typeof location, 'string');
function removeDnsRecords(subdomain, domain, type, values, callback) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug('removeDNSRecord: %s on %s type %s values', location, domain, type, values);
debug('removeDNSRecord: %s on %s type %s values', subdomain, domain, type, values);
get(domain, function (error, domainObject) {
get(domain, function (error, result) {
if (error) return callback(error);
api(domainObject.provider).del(domainObject, location, type, values, function (error) {
api(result.provider).del(result.config, result.zoneName, getName(result, subdomain, type), type, values, function (error) {
if (error && error.reason !== DomainsError.NOT_FOUND) return callback(error);
callback(null);
@@ -446,25 +396,51 @@ function removeDnsRecords(location, domain, type, values, callback) {
});
}
function waitForDnsRecord(location, domain, type, value, options, callback) {
assert.strictEqual(typeof location, 'string');
// only wait for A record
function waitForDnsRecord(fqdn, domain, value, options, callback) {
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof domain, 'string');
assert(type === 'A' || type === 'TXT');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
get(domain, function (error, domainObject) {
get(domain, function (error, result) {
if (error) return callback(error);
api(domainObject.provider).wait(domainObject, location, type, value, options, callback);
api(result.provider).waitForDns(fqdn, result ? result.zoneName : domain, value, options, callback);
});
}
function setAdmin(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
debug('setAdmin domain:%s', domain);
get(domain, function (error, result) {
if (error) return callback(error);
var setPtrRecord = config.provider() === 'caas' ? caas.setPtrRecord : function (d, next) { next(); };
setPtrRecord(domain, function (error) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Error setting PTR record:' + error.message));
config.setAdminDomain(result.domain);
config.setAdminLocation('my');
config.setAdminFqdn('my' + (result.config.hyphenatedSubdomains ? '-' : '.') + result.domain);
callback();
shell.sudo('restart', [ RESTART_CMD ], NOOP_CALLBACK);
});
});
}
// removes all fields that are strictly private and should never be returned by API calls
function removePrivateFields(domain) {
var result = _.pick(domain, 'domain', 'zoneName', 'provider', 'config', 'tlsConfig', 'fallbackCertificate', 'locked');
return api(result.provider).removePrivateFields(result);
if (result.fallbackCertificate) delete result.fallbackCertificate.key; // do not return the 'key'. in caas, this is private
return result;
}
// removes all fields that are not accessible by a normal user
@@ -476,39 +452,3 @@ function removeRestrictedFields(domain) {
return result;
}
function makeWildcard(hostname) {
assert.strictEqual(typeof hostname, 'string');
let parts = hostname.split('.');
parts[0] = '*';
return parts.join('.');
}
function prepareDashboardDomain(domain, auditSource, progressCallback, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
get(domain, function (error, domainObject) {
if (error) return callback(error);
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error.message));
async.series([
(done) => { progressCallback({ percent: 10, message: 'Updating DNS' }); done(); },
upsertDnsRecords.bind(null, constants.ADMIN_LOCATION, domain, 'A', [ ip ]),
(done) => { progressCallback({ percent: 40, message: 'Waiting for DNS' }); done(); },
waitForDnsRecord.bind(null, constants.ADMIN_LOCATION, domain, 'A', ip, { interval: 30000, times: 50000 }),
(done) => { progressCallback({ percent: 70, message: 'Getting certificate' }); done(); },
reverseProxy.ensureCertificate.bind(null, fqdn(constants.ADMIN_LOCATION, domainObject), domain, auditSource)
], function (error) {
if (error) return callback(error);
callback(null);
});
});
});
}
+3 -16
View File
@@ -10,9 +10,6 @@ var appdb = require('./appdb.js'),
config = require('./config.js'),
debug = require('debug')('box:dyndns'),
domains = require('./domains.js'),
eventlog = require('./eventlog.js'),
paths = require('./paths.js'),
safe = require('safetydance'),
sysinfo = require('./sysinfo.js');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
@@ -24,18 +21,12 @@ function sync(callback) {
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
let info = safe.JSON.parse(safe.fs.readFileSync(paths.DYNDNS_INFO_FILE, 'utf8')) || { ip: null };
if (info.ip === ip) {
debug(`refreshDNS: no change in IP ${ip}`);
return callback();
}
debug(`refreshDNS: updating ip from ${info.ip} to ${ip}`);
debug('refreshDNS: current ip %s', ip);
domains.upsertDnsRecords(config.adminLocation(), config.adminDomain(), 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('refreshDNS: updated admin location');
debug('refreshDNS: done for admin location');
apps.getAll(function (error, result) {
if (error) return callback(error);
@@ -48,11 +39,7 @@ function sync(callback) {
}, function (error) {
if (error) return callback(error);
debug('refreshDNS: updated apps');
eventlog.add(eventlog.ACTION_DYNDNS_UPDATE, { userId: null, username: 'cron' }, { fromIp: info.ip, toIp: ip });
info.ip = ip;
safe.fs.writeFileSync(paths.DYNDNS_INFO_FILE, JSON.stringify(info), 'utf8');
debug('refreshDNS: done for apps');
callback();
});
+4 -55
View File
@@ -18,53 +18,23 @@ exports = module.exports = {
ACTION_APP_UNINSTALL: 'app.uninstall',
ACTION_APP_UPDATE: 'app.update',
ACTION_APP_LOGIN: 'app.login',
ACTION_APP_OOM: 'app.oom',
ACTION_APP_UP: 'app.up',
ACTION_APP_DOWN: 'app.down',
ACTION_APP_TASK_CRASH: 'app.task.crash',
ACTION_BACKUP_FINISH: 'backup.finish',
ACTION_BACKUP_START: 'backup.start',
ACTION_BACKUP_CLEANUP_START: 'backup.cleanup.start',
ACTION_BACKUP_CLEANUP_FINISH: 'backup.cleanup.finish',
ACTION_CERTIFICATE_NEW: 'certificate.new',
ACTION_BACKUP_CLEANUP: 'backup.cleanup',
ACTION_CERTIFICATE_RENEWAL: 'certificate.renew',
ACTION_DASHBOARD_DOMAIN_UPDATE: 'dashboard.domain.update',
ACTION_DOMAIN_ADD: 'domain.add',
ACTION_DOMAIN_UPDATE: 'domain.update',
ACTION_DOMAIN_REMOVE: 'domain.remove',
ACTION_MAIL_ENABLED: 'mail.enabled',
ACTION_MAIL_DISABLED: 'mail.disabled',
ACTION_MAIL_MAILBOX_ADD: 'mail.box.add',
ACTION_MAIL_MAILBOX_REMOVE: 'mail.box.remove',
ACTION_MAIL_LIST_ADD: 'mail.list.add',
ACTION_MAIL_LIST_REMOVE: 'mail.list.remove',
ACTION_PROVISION: 'cloudron.provision',
ACTION_RESTORE: 'cloudron.restore', // unused
ACTION_START: 'cloudron.start',
ACTION_UPDATE: 'cloudron.update',
ACTION_USER_ADD: 'user.add',
ACTION_USER_LOGIN: 'user.login',
ACTION_USER_REMOVE: 'user.remove',
ACTION_USER_UPDATE: 'user.update',
ACTION_USER_TRANSFER: 'user.transfer',
ACTION_DYNDNS_UPDATE: 'dyndns.update',
ACTION_PROCESS_CRASH: 'system.crash'
};
var assert = require('assert'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:eventlog'),
eventlogdb = require('./eventlogdb.js'),
notifications = require('./notifications.js'),
util = require('util'),
uuid = require('uuid');
@@ -100,31 +70,10 @@ function add(action, source, data, callback) {
callback = callback || NOOP_CALLBACK;
// we do only daily upserts for login actions, so they don't spam the db
var api = action === exports.ACTION_USER_LOGIN ? eventlogdb.upsert : eventlogdb.add;
api(uuid.v4(), action, source, data, function (error, id) {
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
var id = uuid.v4();
// decide if we want to add notifications as well
if (action === exports.ACTION_USER_ADD) {
notifications.userAdded(source.userId, id, data.user);
} else if (action === exports.ACTION_USER_REMOVE) {
notifications.userRemoved(source.userId, id, data.user);
} else if (action === exports.ACTION_USER_UPDATE && data.adminStatusChanged) {
notifications.adminChanged(source.userId, id, data.user);
} else if (action === exports.ACTION_APP_OOM) {
notifications.oomEvent(id, data.app ? data.app.id : data.containerId, { app: data.app, details: data });
} else if (action === exports.ACTION_APP_DOWN) {
notifications.appDied(id, data.app);
} else if (action === exports.ACTION_APP_UP) {
notifications.appUp(id, data.app);
} else if (action === exports.ACTION_APP_TASK_CRASH) {
notifications.apptaskCrash(id, data.appId, data.crashLogFile);
} else if (action === exports.ACTION_PROCESS_CRASH) {
notifications.processCrash(id, data.processName, data.crashLogFile);
} else {
// no notification
}
eventlogdb.add(id, action, source, data, function (error) {
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
callback(null, { id: id });
});
+9 -45
View File
@@ -5,7 +5,6 @@ exports = module.exports = {
getAllPaged: getAllPaged,
getByCreationTime: getByCreationTime,
add: add,
upsert: upsert,
count: count,
delByCreationTime: delByCreationTime,
@@ -13,14 +12,13 @@ exports = module.exports = {
};
var assert = require('assert'),
async = require('async'),
database = require('./database.js'),
DatabaseError = require('./databaseerror'),
mysql = require('mysql'),
safe = require('safetydance'),
util = require('util');
var EVENTLOG_FIELDS = [ 'id', 'action', 'source', 'data', 'creationTime' ].join(',');
var EVENTLOGS_FIELDS = [ 'id', 'action', 'source', 'data', 'creationTime' ].join(',');
function postProcess(eventLog) {
// usually we have sourceJson and dataJson, however since this used to be the JSON data type, we don't
@@ -34,7 +32,7 @@ function get(eventId, callback) {
assert.strictEqual(typeof eventId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + EVENTLOG_FIELDS + ' FROM eventlog WHERE id = ?', [ eventId ], function (error, result) {
database.query('SELECT ' + EVENTLOGS_FIELDS + ' FROM eventlog WHERE id = ?', [ eventId ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
@@ -50,7 +48,7 @@ function getAllPaged(actions, search, page, perPage, callback) {
assert.strictEqual(typeof callback, 'function');
var data = [];
var query = 'SELECT ' + EVENTLOG_FIELDS + ' FROM eventlog';
var query = 'SELECT ' + EVENTLOGS_FIELDS + ' FROM eventlog';
if (actions.length || search) query += ' WHERE';
if (search) query += ' (source LIKE ' + mysql.escape('%' + search + '%') + ' OR data LIKE ' + mysql.escape('%' + search + '%') + ')';
@@ -80,7 +78,7 @@ function getByCreationTime(creationTime, callback) {
assert(util.isDate(creationTime));
assert.strictEqual(typeof callback, 'function');
var query = 'SELECT ' + EVENTLOG_FIELDS + ' FROM eventlog WHERE creationTime >= ? ORDER BY creationTime DESC';
var query = 'SELECT ' + EVENTLOGS_FIELDS + ' FROM eventlog WHERE creationTime >= ? ORDER BY creationTime DESC';
database.query(query, [ creationTime ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
@@ -101,33 +99,7 @@ function add(id, action, source, data, callback) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error));
if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null, id);
});
}
// id is only used if we didn't do an update but insert instead
function upsert(id, action, source, data, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof action, 'string');
assert.strictEqual(typeof source, 'object');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
// can't do a real sql upsert, for frequent eventlog entries we only have to do 2 queries once a day
var queries = [{
query: 'UPDATE eventlog SET creationTime=NOW(), data=? WHERE action = ? AND source LIKE ? AND DATE(creationTime)=CURDATE()',
args: [ JSON.stringify(data), action, JSON.stringify(source) ]
}, {
query: 'SELECT ' + EVENTLOG_FIELDS + ' FROM eventlog WHERE action = ? AND source LIKE ? AND DATE(creationTime)=CURDATE()',
args: [ action, JSON.stringify(source) ]
}];
database.transaction(queries, function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result[0].affectedRows >= 1) return callback(null, result[1][0].id);
// no existing eventlog found, create one
add(id, action, source, data, callback);
callback(null);
});
}
@@ -153,19 +125,11 @@ function delByCreationTime(creationTime, callback) {
assert(util.isDate(creationTime));
assert.strictEqual(typeof callback, 'function');
// since notifications reference eventlog items, we have to clean them up as well
database.query('SELECT * FROM eventlog WHERE creationTime < ?', [ creationTime ], function (error, result) {
var query = 'DELETE FROM eventlog WHERE creationTime < ?';
database.query(query, [ creationTime ], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
async.eachSeries(result, function (item, callback) {
database.query('DELETE FROM notifications WHERE eventId=?', [ item.id ], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
database.query('DELETE FROM eventlog WHERE id=?', [ item.id ], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback();
});
});
}, callback);
callback(error);
});
}
-40
View File
@@ -1,40 +0,0 @@
'use strict';
exports = module.exports = {
startGraphite: startGraphite
};
var assert = require('assert'),
infra = require('./infra_version.js'),
paths = require('./paths.js'),
shell = require('./shell.js');
function startGraphite(existingInfra, callback) {
assert.strictEqual(typeof existingInfra, 'object');
assert.strictEqual(typeof callback, 'function');
const tag = infra.images.graphite.tag;
const dataDir = paths.PLATFORM_DATA_DIR;
if (existingInfra.version === infra.version && infra.images.graphite.tag === existingInfra.images.graphite.tag) return callback();
const cmd = `docker run --restart=always -d --name="graphite" \
--net cloudron \
--net-alias graphite \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=graphite \
-m 75m \
--memory-swap 150m \
--dns 172.18.0.1 \
--dns-search=. \
-p 127.0.0.1:2003:2003 \
-p 127.0.0.1:2004:2004 \
-p 127.0.0.1:8417:8000 \
-v "${dataDir}/graphite:/var/lib/graphite" \
--label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`;
shell.exec('startGraphite', cmd, callback);
}
+14 -14
View File
@@ -34,7 +34,7 @@ function get(groupId, callback) {
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + GROUPS_FIELDS + ' FROM userGroups WHERE id = ? ORDER BY name', [ groupId ], function (error, result) {
database.query('SELECT ' + GROUPS_FIELDS + ' FROM groups WHERE id = ? ORDER BY name', [ groupId ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
@@ -47,9 +47,9 @@ function getWithMembers(groupId, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + GROUPS_FIELDS + ',GROUP_CONCAT(groupMembers.userId) AS userIds ' +
' FROM userGroups LEFT OUTER JOIN groupMembers ON userGroups.id = groupMembers.groupId ' +
' WHERE userGroups.id = ? ' +
' GROUP BY userGroups.id', [ groupId ], function (error, results) {
' FROM groups LEFT OUTER JOIN groupMembers ON groups.id = groupMembers.groupId ' +
' WHERE groups.id = ? ' +
' GROUP BY groups.id', [ groupId ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
@@ -63,7 +63,7 @@ function getWithMembers(groupId, callback) {
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + GROUPS_FIELDS + ' FROM userGroups', function (error, results) {
database.query('SELECT ' + GROUPS_FIELDS + ' FROM groups', function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null, results);
@@ -72,8 +72,8 @@ function getAll(callback) {
function getAllWithMembers(callback) {
database.query('SELECT ' + GROUPS_FIELDS + ',GROUP_CONCAT(groupMembers.userId) AS userIds ' +
' FROM userGroups LEFT OUTER JOIN groupMembers ON userGroups.id = groupMembers.groupId ' +
' GROUP BY userGroups.id', function (error, results) {
' FROM groups LEFT OUTER JOIN groupMembers ON groups.id = groupMembers.groupId ' +
' GROUP BY groups.id', function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
@@ -88,7 +88,7 @@ function add(id, name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO userGroups (id, name) VALUES (?, ?)', [ id, name ], function (error, result) {
database.query('INSERT INTO groups (id, name) VALUES (?, ?)', [ id, name ], function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error));
if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
@@ -112,8 +112,8 @@ function update(id, data, callback) {
}
args.push(id);
database.query('UPDATE userGroups SET ' + fields.join(', ') + ' WHERE id = ?', args, function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('userGroups_name') !== -1) return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, 'name already exists'));
database.query('UPDATE groups SET ' + fields.join(', ') + ' WHERE id = ?', args, function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('groups_name') !== -1) return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, 'name already exists'));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
@@ -128,7 +128,7 @@ function del(id, callback) {
// also cleanup the groupMembers table
var queries = [];
queries.push({ query: 'DELETE FROM groupMembers WHERE groupId = ?', args: [ id ] });
queries.push({ query: 'DELETE FROM userGroups WHERE id = ?', args: [ id ] });
queries.push({ query: 'DELETE FROM groups WHERE id = ?', args: [ id ] });
database.transaction(queries, function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
@@ -141,7 +141,7 @@ function del(id, callback) {
function count(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT COUNT(*) AS total FROM userGroups', function (error, result) {
database.query('SELECT COUNT(*) AS total FROM groups', function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
return callback(null, result[0].total);
@@ -152,7 +152,7 @@ function clear(callback) {
database.query('DELETE FROM groupMembers', function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
database.query('DELETE FROM userGroups', function (error) {
database.query('DELETE FROM groups', function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(error);
@@ -266,7 +266,7 @@ function getGroups(userId, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + GROUPS_FIELDS + ' ' +
' FROM userGroups INNER JOIN groupMembers ON userGroups.id = groupMembers.groupId AND groupMembers.userId = ?', [ userId ], function (error, results) {
' FROM groups INNER JOIN groupMembers ON groups.id = groupMembers.groupId AND groupMembers.userId = ?', [ userId ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null, results);
+12 -13
View File
@@ -5,21 +5,20 @@
// Do not require anything here!
exports = module.exports = {
// a version change recreates all containers with latest docker config
'version': '48.12.1',
// a major version makes all apps restore from backup. #451 must be fixed before we do this.
// a minor version makes all apps re-configure themselves
'version': '48.11.0',
'baseImages': [
{ repo: 'cloudron/base', tag: 'cloudron/base:1.0.0@sha256:147a648a068a2e746644746bbfb42eb7a50d682437cead3c67c933c546357617' }
],
'baseImages': [ 'cloudron/base:0.10.0' ],
// a major version bump in the db containers will trigger the restore logic that uses the db dumps
// docker inspect --format='{{index .RepoDigests 0}}' $IMAGE to get the sha256
// Note that if any of the databases include an upgrade, bump the infra version above
// This is because we upgrade using dumps instead of mysql_upgrade, pg_upgrade etc
'images': {
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:2.0.1@sha256:5a13360da4a2085c7d474bea6b1090c5eb24732d4f73459942af7612d4993d7f' },
'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.1.0@sha256:131db42dcb90111f679ab1f0f37c552f93f797d9b803b2346c7c202daf86ac36' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.0.2@sha256:454f035d60b768153d4f31210380271b5ba1c09367c9d95c7fa37f9e39d2f59c' }
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:1.1.0' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:1.1.0' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:1.1.0' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:1.0.0' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:1.4.0' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:1.0.0' }
}
};
+37 -61
View File
@@ -47,7 +47,7 @@ function getUsersWithAccessToApp(req, callback) {
assert.strictEqual(typeof req.app, 'object');
assert.strictEqual(typeof callback, 'function');
users.getAll(function (error, result) {
users.list(function (error, result) {
if (error) return callback(new ldap.OperationsError(error.toString()));
async.filter(result, apps.hasAccessTo.bind(null, req.app), function (error, allowedUsers) {
@@ -271,6 +271,7 @@ function mailboxSearch(req, res, next) {
cn: `${mailbox.name}@${mailbox.domain}`,
uid: `${mailbox.name}@${mailbox.domain}`,
mail: `${mailbox.name}@${mailbox.domain}`,
ownerType: mailbox.ownerType,
displayname: 'Max Mustermann',
givenName: 'Max',
username: 'mmustermann',
@@ -296,6 +297,9 @@ function mailboxSearch(req, res, next) {
var results = [];
// only send user mailboxes
result = result.filter(function (m) { return m.ownerType === mailboxdb.OWNER_TYPE_USER; });
// send mailbox objects
result.forEach(function (mailbox) {
var dn = ldap.parseDN(`cn=${mailbox.name}@${domain},domain=${domain},ou=mailboxes,dc=cloudron`);
@@ -307,7 +311,8 @@ function mailboxSearch(req, res, next) {
objectcategory: 'mailbox',
cn: `${mailbox.name}@${domain}`,
uid: `${mailbox.name}@${domain}`,
mail: `${mailbox.name}@${domain}`
mail: `${mailbox.name}@${domain}`,
ownerType: mailbox.ownerType
}
};
@@ -444,14 +449,14 @@ function authorizeUserForApp(req, res, next) {
// we return no such object, to avoid leakage of a users existence
if (!result) return next(new ldap.NoSuchObjectError(req.dn.toString()));
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: req.app.id }, { userId: req.user.id, user: users.removePrivateFields(req.user) });
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: req.app.id, app: req.app }, { userId: req.user.id, user: users.removePrivateFields(req.user) });
res.end();
});
}
function authenticateUserMailbox(req, res, next) {
debug('user mailbox auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
function authenticateMailbox(req, res, next) {
debug('mailbox auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
@@ -459,61 +464,30 @@ function authenticateUserMailbox(req, res, next) {
var parts = email.split('@');
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
mail.getDomain(parts[1], function (error, domain) {
if (error && error.reason === MailError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
mailboxdb.getMailbox(parts[0], parts[1], function (error, mailbox) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
if (!domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString()));
mailboxdb.getMailbox(parts[0], parts[1], function (error, mailbox) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
mail.getDomain(parts[1], function (error, domain) {
if (error && error.reason === MailError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
users.verify(mailbox.ownerId, req.credentials || '', function (error, result) {
if (error && error.reason === UsersError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error && error.reason === UsersError.WRONG_PASSWORD) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
if (mailbox.ownerType === mailboxdb.OWNER_TYPE_APP) {
var addonId = req.dn.rdns[1].attrs.ou.value.toLowerCase(); // 'sendmail' or 'recvmail'
var name;
if (addonId === 'sendmail') name = 'MAIL_SMTP_PASSWORD';
else if (addonId === 'recvmail') name = 'MAIL_IMAP_PASSWORD';
else return next(new ldap.OperationsError('Invalid DN'));
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) });
res.end();
});
});
});
}
appdb.getAddonConfigByName(mailbox.ownerId, addonId, name, function (error, value) {
if (error) return next(new ldap.OperationsError(error.message));
if (req.credentials !== value) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
function authenticateMailAddon(req, res, next) {
debug('mail addon auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
var email = req.dn.rdns[0].attrs.cn.value.toLowerCase();
var parts = email.split('@');
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
const addonId = req.dn.rdns[1].attrs.ou.value.toLowerCase(); // 'sendmail' or 'recvmail'
mail.getDomain(parts[1], function (error, domain) {
if (error && error.reason === MailError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
if (addonId === 'recvmail' && !domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString()));
let name;
if (addonId === 'sendmail') name = 'MAIL_SMTP_PASSWORD';
else if (addonId === 'recvmail') name = 'MAIL_IMAP_PASSWORD';
else return next(new ldap.OperationsError('Invalid DN'));
// note: with sendmail addon, apps can send mail without a mailbox (unlike users)
appdb.getAppIdByAddonConfigValue(addonId, name, req.credentials || '', function (error, appId) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return next(new ldap.OperationsError(error.message));
if (appId) { // matched app password
eventlog.add(eventlog.ACTION_APP_LOGIN, { authType: 'ldap', mailboxId: email }, { appId: appId, addonId: addonId });
return res.end();
}
mailboxdb.getMailbox(parts[0], parts[1], function (error, mailbox) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
eventlog.add(eventlog.ACTION_APP_LOGIN, { authType: 'ldap', mailboxId: name }, { appId: mailbox.ownerId, addonId: addonId });
return res.end();
});
} else if (mailbox.ownerType === mailboxdb.OWNER_TYPE_USER) {
if (!domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString()));
users.verify(mailbox.ownerId, req.credentials || '', function (error, result) {
if (error && error.reason === UsersError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
@@ -523,7 +497,9 @@ function authenticateMailAddon(req, res, next) {
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) });
res.end();
});
});
} else {
return next(new ldap.OperationsError('Unknown ownerType for mailbox'));
}
});
});
}
@@ -547,13 +523,13 @@ function start(callback) {
gServer.bind('ou=users,dc=cloudron', authenticateApp, authenticateUser, authorizeUserForApp);
// http://www.ietf.org/proceedings/43/I-D/draft-srivastava-ldap-mail-00.txt
gServer.search('ou=mailboxes,dc=cloudron', mailboxSearch); // haraka, dovecot
gServer.bind('ou=mailboxes,dc=cloudron', authenticateUserMailbox); // apps like sogo can use domain=${domain} to authenticate a mailbox
gServer.search('ou=mailaliases,dc=cloudron', mailAliasSearch); // haraka
gServer.search('ou=mailinglists,dc=cloudron', mailingListSearch); // haraka
gServer.search('ou=mailboxes,dc=cloudron', mailboxSearch);
gServer.search('ou=mailaliases,dc=cloudron', mailAliasSearch);
gServer.search('ou=mailinglists,dc=cloudron', mailingListSearch);
gServer.bind('ou=recvmail,dc=cloudron', authenticateMailAddon); // dovecot
gServer.bind('ou=sendmail,dc=cloudron', authenticateMailAddon); // haraka
gServer.bind('ou=mailboxes,dc=cloudron', authenticateMailbox);
gServer.bind('ou=recvmail,dc=cloudron', authenticateMailbox);
gServer.bind('ou=sendmail,dc=cloudron', authenticateMailbox);
gServer.compare('cn=users,ou=groups,dc=cloudron', authenticateApp, groupUsersCompare);
gServer.compare('cn=admins,ou=groups,dc=cloudron', authenticateApp, groupAdminsCompare);
+1
View File
@@ -18,6 +18,7 @@ Locker.prototype.OP_BOX_UPDATE = 'box_update';
Locker.prototype.OP_PLATFORM_START = 'platform_start';
Locker.prototype.OP_FULL_BACKUP = 'full_backup';
Locker.prototype.OP_APPTASK = 'apptask';
Locker.prototype.OP_MIGRATE = 'migrate';
Locker.prototype.lock = function (operation) {
assert.strictEqual(typeof operation, 'string');
+30 -21
View File
@@ -5,10 +5,9 @@ exports = module.exports = {
};
var assert = require('assert'),
eventlog = require('./eventlog.js'),
mailer = require('./mailer.js'),
safe = require('safetydance'),
path = require('path'),
paths = require('./paths.js'),
util = require('util');
var COLLECT_LOGS_CMD = path.join(__dirname, 'scripts/collectlogs.sh');
@@ -16,52 +15,62 @@ var COLLECT_LOGS_CMD = path.join(__dirname, 'scripts/collectlogs.sh');
var CRASH_LOG_TIMESTAMP_OFFSET = 1000 * 60 * 60; // 60 min
var CRASH_LOG_TIMESTAMP_FILE = '/tmp/crashlog.timestamp';
var CRASH_LOG_STASH_FILE = '/tmp/crashlog';
const AUDIT_SOURCE = { userId: null, username: 'healthmonitor' };
var CRASH_LOG_FILE_LIMIT = 2 * 1024 * 1024; // 2mb
function collectLogs(unitName, callback) {
assert.strictEqual(typeof unitName, 'string');
assert.strictEqual(typeof callback, 'function');
var logs = safe.child_process.execSync('sudo ' + COLLECT_LOGS_CMD + ' ' + unitName, { encoding: 'utf8' });
if (!logs) return callback(safe.error);
logs = logs + '\n\n=====================================\n\n';
// special case for box since the real logs are at path.join(paths.LOG_DIR, 'box.log')
if (unitName === 'box.service') {
logs += safe.child_process.execSync('tail --lines=500 ' + path.join(paths.LOG_DIR, 'box.log'), { encoding: 'utf8' });
}
callback(null, logs);
}
function sendFailureLogs(unitName) {
assert.strictEqual(typeof unitName, 'string');
function stashLogs(logs) {
var stat = safe.fs.statSync(CRASH_LOG_STASH_FILE);
if (stat && (stat.size > CRASH_LOG_FILE_LIMIT)) {
console.error('Dropping logs since crash file has become too big');
return;
}
collectLogs(unitName, function (error, logs) {
// append here
safe.fs.writeFileSync(CRASH_LOG_STASH_FILE, logs, { flag: 'a' });
}
function sendFailureLogs(processName, options) {
assert.strictEqual(typeof processName, 'string');
assert.strictEqual(typeof options, 'object');
collectLogs(options.unit || processName, function (error, newLogs) {
if (error) {
console.error('Failed to collect logs.', error);
logs = util.format('Failed to collect logs.', error);
newLogs = util.format('Failed to collect logs.', error);
}
console.log('Sending failure logs for', unitName);
if (!safe.fs.writeFileSync(CRASH_LOG_STASH_FILE, logs)) console.log(`Failed to stash logs to ${CRASH_LOG_STASH_FILE}`);
console.log('Sending failure logs for', processName);
var timestamp = safe.fs.readFileSync(CRASH_LOG_TIMESTAMP_FILE, 'utf8');
// check if we already sent a mail in the last CRASH_LOG_TIME_OFFSET window
if (timestamp && (parseInt(timestamp) + CRASH_LOG_TIMESTAMP_OFFSET) > Date.now()) {
console.log('Crash log already sent within window. Stashing logs.');
return;
return stashLogs(newLogs);
}
eventlog.add(eventlog.ACTION_PROCESS_CRASH, AUDIT_SOURCE, { processName: unitName, crashLogFile: CRASH_LOG_STASH_FILE }, function (error) {
if (error) console.log(`Error sending crashlog. Logs stashed at ${CRASH_LOG_STASH_FILE}`);
var stashedLogs = safe.fs.readFileSync(CRASH_LOG_STASH_FILE, 'utf8');
var compiledLogs = stashedLogs ? (stashedLogs + newLogs) : newLogs;
var mailSubject = processName + (stashedLogs ? ' and others' : '');
mailer.unexpectedExit(mailSubject, compiledLogs, function (error) {
if (error) {
console.log('Error sending crashlog. Stashing logs.');
return stashLogs(newLogs);
}
// write the new timestamp file and delete stash file
safe.fs.writeFileSync(CRASH_LOG_TIMESTAMP_FILE, String(Date.now()));
safe.fs.unlinkSync(CRASH_LOG_STASH_FILE);
});
});
}
+83 -174
View File
@@ -8,10 +8,8 @@ exports = module.exports = {
getDomain: getDomain,
addDomain: addDomain,
removeDomain: removeDomain,
clearDomains: clearDomains,
setDnsRecords: setDnsRecords,
setMailFqdn: setMailFqdn,
validateName: validateName,
@@ -24,11 +22,11 @@ exports = module.exports = {
sendTestMail: sendTestMail,
listMailboxes: listMailboxes,
getMailboxes: getMailboxes,
removeMailboxes: removeMailboxes,
getMailbox: getMailbox,
addMailbox: addMailbox,
updateMailboxOwner: updateMailboxOwner,
updateMailbox: updateMailbox,
removeMailbox: removeMailbox,
listAliases: listAliases,
@@ -53,14 +51,13 @@ var assert = require('assert'),
debug = require('debug')('box:mail'),
dns = require('./native-dns.js'),
domains = require('./domains.js'),
eventlog = require('./eventlog.js'),
hat = require('./hat.js'),
infra = require('./infra_version.js'),
mailboxdb = require('./mailboxdb.js'),
maildb = require('./maildb.js'),
mailer = require('./mailer.js'),
net = require('net'),
nodemailer = require('nodemailer'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
reverseProxy = require('./reverseproxy.js'),
@@ -111,6 +108,9 @@ function validateName(name) {
// also need to consider valid LDAP characters here (e.g '+' is reserved)
if (/[^a-zA-Z0-9.-]/.test(name)) return new MailError(MailError.BAD_FIELD, 'mailbox name can only contain alphanumerals and dot');
// app emails are sent using the .app suffix
if (name.indexOf('.app') !== -1) return new MailError(MailError.BAD_FIELD, 'mailbox name pattern is reserved for apps');
return null;
}
@@ -142,13 +142,13 @@ function checkOutboundPort25(callback) {
});
client.on('timeout', function () {
relay.status = false;
relay.value = `Connect to ${smtpServer} timed out. Check if port 25 (outbound) is blocked`;
relay.value = 'Connect to ' + smtpServer + ' timed out';
client.destroy();
callback(new Error('Timeout'), relay);
});
client.on('error', function (error) {
relay.status = false;
relay.value = `Connect to ${smtpServer} failed: ${error.message}. Check if port 25 (outbound) is blocked`;
relay.value = 'Connect to ' + smtpServer + ' failed: ' + error.message;
client.destroy();
callback(error, relay);
});
@@ -224,17 +224,13 @@ function checkDkim(domain, callback) {
});
}
function checkSpf(domain, mailFqdn, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof mailFqdn, 'string');
assert.strictEqual(typeof callback, 'function');
function checkSpf(domain, callback) {
var spf = {
domain: domain,
name: '@',
type: 'TXT',
value: null,
expected: 'v=spf1 a:' + mailFqdn + ' ~all',
expected: 'v=spf1 a:' + config.mailFqdn() + ' ~all',
status: false
};
@@ -260,17 +256,13 @@ function checkSpf(domain, mailFqdn, callback) {
});
}
function checkMx(domain, mailFqdn, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof mailFqdn, 'string');
assert.strictEqual(typeof callback, 'function');
function checkMx(domain, callback) {
var mx = {
domain: domain,
name: '@',
type: 'MX',
value: null,
expected: '10 ' + mailFqdn + '.',
expected: '10 ' + config.mailFqdn() + '.',
status: false
};
@@ -278,7 +270,7 @@ function checkMx(domain, mailFqdn, callback) {
if (error) return callback(error, mx);
if (mxRecords.length !== 0) {
mx.status = mxRecords.length == 1 && mxRecords[0].exchange === mailFqdn;
mx.status = mxRecords.length == 1 && mxRecords[0].exchange === config.mailFqdn();
mx.value = mxRecords.map(function (r) { return r.priority + ' ' + r.exchange + '.'; }).join(' ');
}
@@ -319,15 +311,12 @@ function checkDmarc(domain, callback) {
});
}
function checkPtr(mailFqdn, callback) {
assert.strictEqual(typeof mailFqdn, 'string');
assert.strictEqual(typeof callback, 'function');
function checkPtr(callback) {
var ptr = {
domain: null,
type: 'PTR',
value: null,
expected: mailFqdn, // any trailing '.' is added by client software (https://lists.gt.net/spf/devel/7918)
expected: config.mailFqdn(), // any trailing '.' is added by client software (https://lists.gt.net/spf/devel/7918)
status: false
};
@@ -468,25 +457,20 @@ function getStatus(domain, callback) {
};
}
const mailFqdn = config.mailFqdn();
getDomain(domain, function (error, result) {
if (error) return callback(error);
let checks = [];
if (result.enabled) {
checks.push(
recordResult('dns.mx', checkMx.bind(null, domain, mailFqdn)),
recordResult('dns.dmarc', checkDmarc.bind(null, domain))
);
}
var checks = [
recordResult('dns.mx', checkMx.bind(null, domain)),
recordResult('dns.dmarc', checkDmarc.bind(null, domain))
];
if (result.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.spf', checkSpf.bind(null, domain)),
recordResult('dns.dkim', checkDkim.bind(null, domain)),
recordResult('dns.ptr', checkPtr.bind(null, mailFqdn)),
recordResult('dns.ptr', checkPtr),
recordResult('relay', checkOutboundPort25),
recordResult('rbl', checkRblStatus.bind(null, domain))
);
@@ -500,8 +484,7 @@ function getStatus(domain, callback) {
});
}
function createMailConfig(mailFqdn, callback) {
assert.strictEqual(typeof mailFqdn, 'string');
function createMailConfig(callback) {
assert.strictEqual(typeof callback, 'function');
debug('createMailConfig: generating mail config');
@@ -510,7 +493,9 @@ function createMailConfig(mailFqdn, callback) {
if (error) return callback(error);
users.getOwner(function (error, owner) {
const alertsFrom = `no-reply@${config.adminDomain()}`;
const mailFqdn = config.mailFqdn();
const defaultDomain = config.adminDomain();
const alertsFrom = `no-reply@${defaultDomain}`;
const alertsTo = config.provider() === 'caas' ? [ 'support@cloudron.io' ] : [ ];
alertsTo.concat(error ? [] : owner.email).join(','); // owner may not exist yet
@@ -519,7 +504,7 @@ function createMailConfig(mailFqdn, callback) {
const mailInDomains = mailDomains.filter(function (d) { return d.enabled; }).map(function (d) { return d.domain; }).join(',');
if (!safe.fs.writeFileSync(path.join(paths.ADDON_CONFIG_DIR, 'mail/mail.ini'),
`mail_in_domains=${mailInDomains}\nmail_out_domains=${mailOutDomains}\nmail_server_name=${mailFqdn}\nalerts_from=${alertsFrom}\nalerts_to=${alertsTo}\n\n`, 'utf8')) {
`mail_in_domains=${mailInDomains}\nmail_out_domains=${mailOutDomains}\nmail_default_domain=${defaultDomain}\nmail_server_name=${mailFqdn}\nalerts_from=${alertsFrom}\nalerts_to=${alertsTo}\n\n`, 'utf8')) {
return callback(new Error('Could not create mail var file:' + safe.error.message));
}
@@ -559,21 +544,19 @@ function createMailConfig(mailFqdn, callback) {
});
}
function configureMail(mailFqdn, mailDomain, callback) {
assert.strictEqual(typeof mailFqdn, 'string');
assert.strictEqual(typeof mailDomain, 'string');
assert.strictEqual(typeof callback, 'function');
function restartMail(callback) {
// mail (note: 2525 is hardcoded in mail container and app use this port)
// MAIL_SERVER_NAME is the hostname of the mailserver i.e server uses these certs
// MAIL_DOMAIN is the domain for which this server is relaying mails
// mail container uses /app/data for backed up data and /run for restart-able data
const tag = infra.images.mail.tag;
const memoryLimit = 4 * 256;
const cloudronToken = hat(8 * 128), relayToken = hat(8 * 128);
if (process.env.BOX_ENV === 'test' && !process.env.TEST_CREATE_INFRA) return callback();
reverseProxy.getCertificate(mailFqdn, mailDomain, function (error, bundle) {
const tag = infra.images.mail.tag;
const memoryLimit = Math.max((1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 128, 256);
// admin and mail share the same certificate
reverseProxy.getCertificate({ fqdn: config.adminFqdn(), domain: config.adminDomain() }, function (error, bundle) {
if (error) return callback(error);
// the setup script copies dhparams.pem to /addons/mail
@@ -583,62 +566,37 @@ function configureMail(mailFqdn, mailDomain, callback) {
if (!safe.child_process.execSync(`cp ${bundle.certFilePath} ${mailCertFilePath}`)) return callback(new Error('Could not create cert file:' + safe.error.message));
if (!safe.child_process.execSync(`cp ${bundle.keyFilePath} ${mailKeyFilePath}`)) return callback(new Error('Could not create key file:' + safe.error.message));
shell.exec('startMail', 'docker rm -f mail || true', function (error) {
shell.execSync('startMail', 'docker rm -f mail || true');
createMailConfig(function (error, allowInbound) {
if (error) return callback(error);
createMailConfig(mailFqdn, function (error, allowInbound) {
if (error) return callback(error);
var ports = allowInbound ? '-p 587:2525 -p 993:9993 -p 4190:4190 -p 25:2525' : '';
var ports = allowInbound ? '-p 587:2525 -p 993:9993 -p 4190:4190 -p 25:2525' : '';
const cmd = `docker run --restart=always -d --name="mail" \
--net cloudron \
--net-alias mail \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=mail \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
-v "${paths.MAIL_DATA_DIR}:/app/data" \
-v "${paths.PLATFORM_DATA_DIR}/addons/mail:/etc/mail" \
${ports} \
-p 127.0.0.1:2020:2020 \
--read-only -v /run -v /tmp ${tag}`;
const cmd = `docker run --restart=always -d --name="mail" \
--net cloudron \
--net-alias mail \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=mail \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
-e CLOUDRON_MAIL_TOKEN="${cloudronToken}" \
-e CLOUDRON_RELAY_TOKEN="${relayToken}" \
-v "${paths.MAIL_DATA_DIR}:/app/data" \
-v "${paths.PLATFORM_DATA_DIR}/addons/mail:/etc/mail" \
${ports} \
-p 127.0.0.1:2020:2020 \
--label isCloudronManaged=true \
--read-only -v /run -v /tmp ${tag}`;
shell.execSync('startMail', cmd);
shell.exec('startMail', cmd, callback);
});
callback();
});
});
}
function restartMail(callback) {
assert.strictEqual(typeof callback, 'function');
if (process.env.BOX_ENV === 'test' && !process.env.TEST_CREATE_INFRA) return callback();
configureMail(config.mailFqdn(), config.adminDomain(), callback);
}
function restartMailIfActivated(callback) {
assert.strictEqual(typeof callback, 'function');
users.isActivated(function (error, activated) {
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
if (!activated) {
debug('restartMailIfActivated: skipping restart of mail container since Cloudron is not activated yet');
return callback(); // not provisioned yet, do not restart container after dns setup
}
restartMail(callback);
});
}
function getDomain(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -654,7 +612,7 @@ function getDomain(domain, callback) {
function getDomains(callback) {
assert.strictEqual(typeof callback, 'function');
maildb.list(function (error, results) {
maildb.getAll(function (error, results) {
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
return callback(null, results);
@@ -662,9 +620,8 @@ function getDomains(callback) {
}
// https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
function txtRecordsWithSpf(domain, mailFqdn, callback) {
function txtRecordsWithSpf(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof mailFqdn, 'string');
assert.strictEqual(typeof callback, 'function');
domains.getDnsRecords('', domain, 'TXT', function (error, txtRecords) {
@@ -679,17 +636,17 @@ function txtRecordsWithSpf(domain, mailFqdn, callback) {
if (matches === null) continue;
// this won't work if the entry is arbitrarily "split" across quoted strings
validSpf = txtRecords[i].indexOf('a:' + mailFqdn) !== -1;
validSpf = txtRecords[i].indexOf('a:' + config.mailFqdn()) !== -1;
break; // there can only be one SPF record
}
if (validSpf) return callback(null, null);
if (!matches) { // no spf record was found, create one
txtRecords.push('"v=spf1 a:' + mailFqdn + ' ~all"');
txtRecords.push('"v=spf1 a:' + config.mailFqdn() + ' ~all"');
debug('txtRecordsWithSpf: adding txt record');
} else { // just add ourself
txtRecords[i] = matches[1] + ' a:' + mailFqdn + txtRecords[i].slice(matches[1].length);
txtRecords[i] = matches[1] + ' a:' + config.mailFqdn() + txtRecords[i].slice(matches[1].length);
debug('txtRecordsWithSpf: inserting txt record');
}
@@ -746,9 +703,8 @@ function readDkimPublicKeySync(domain) {
return publicKey;
}
function upsertDnsRecords(domain, mailFqdn, callback) {
function setDnsRecords(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof mailFqdn, 'string');
assert.strictEqual(typeof callback, 'function');
maildb.get(domain, function (error, result) {
@@ -770,27 +726,27 @@ function upsertDnsRecords(domain, mailFqdn, callback) {
records.push(dkimRecord);
if (result.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 + '.' ] });
records.push({ subdomain: '', domain: domain, type: 'MX', values: [ '10 ' + config.mailFqdn() + '.' ] });
}
debug('upsertDnsRecords: %j', records);
debug('addDnsRecords: %j', records);
txtRecordsWithSpf(domain, mailFqdn, function (error, txtRecords) {
txtRecordsWithSpf(domain, function (error, txtRecords) {
if (error) return callback(error);
if (txtRecords) records.push({ subdomain: '', domain: domain, type: 'TXT', values: txtRecords });
debug('upsertDnsRecords: will update %j', records);
debug('addDnsRecords: will update %j', records);
async.mapSeries(records, function (record, iteratorCallback) {
domains.upsertDnsRecords(record.subdomain, record.domain, record.type, record.values, iteratorCallback);
}, function (error, changeIds) {
if (error) {
debug(`upsertDnsRecords: failed to update: ${error}`);
debug(`addDnsRecords: failed to update: ${error}`);
return callback(new MailError(MailError.EXTERNAL_ERROR, error.message));
}
debug('upsertDnsRecords: records %j added with changeIds %j', records, changeIds);
debug('addDnsRecords: records %j added with changeIds %j', records, changeIds);
callback(null);
});
@@ -798,31 +754,6 @@ function upsertDnsRecords(domain, mailFqdn, callback) {
});
}
function setDnsRecords(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
upsertDnsRecords(domain, config.mailFqdn(), callback);
}
function setMailFqdn(mailFqdn, mailDomain, callback) {
assert.strictEqual(typeof mailFqdn, 'string');
assert.strictEqual(typeof mailDomain, 'string');
assert.strictEqual(typeof callback, 'function');
domains.getAll(function (error, allDomains) {
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
async.eachOfSeries(allDomains, function (domainObject, idx, iteratorDone) {
upsertDnsRecords(domainObject.domain, mailFqdn, iteratorDone);
}, function (error) {
if (error) return callback(new MailError(MailError.EXTERNAL_ERROR, error.message));
configureMail(mailFqdn, mailDomain, callback);
});
});
}
function addDomain(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -833,8 +764,8 @@ function addDomain(domain, callback) {
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
async.series([
upsertDnsRecords.bind(null, domain, config.mailFqdn()), // do this first to ensure DKIM keys
restartMailIfActivated
setDnsRecords.bind(null, domain), // do this first to ensure DKIM keys
restartMail
], NOOP_CALLBACK); // do these asynchronously
callback();
@@ -858,16 +789,6 @@ function removeDomain(domain, callback) {
});
}
function clearDomains(callback) {
assert.strictEqual(typeof callback, 'function');
maildb.clear(function (error) {
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
callback();
});
}
function setMailFromValidation(domain, enabled, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof enabled, 'boolean');
@@ -917,10 +838,9 @@ function setMailRelay(domain, relay, callback) {
});
}
function setMailEnabled(domain, enabled, auditSource, callback) {
function setMailEnabled(domain, enabled, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof enabled, 'boolean');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
maildb.update(domain, { enabled: enabled }, function (error) {
@@ -929,8 +849,6 @@ function setMailEnabled(domain, enabled, auditSource, callback) {
restartMail(NOOP_CALLBACK);
eventlog.add(enabled ? eventlog.ACTION_MAIL_ENABLED : eventlog.ACTION_MAIL_DISABLED, auditSource, { domain });
callback(null);
});
}
@@ -949,7 +867,7 @@ function sendTestMail(domain, to, callback) {
});
}
function listMailboxes(domain, callback) {
function getMailboxes(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -984,11 +902,10 @@ function getMailbox(name, domain, callback) {
});
}
function addMailbox(name, domain, userId, auditSource, callback) {
function addMailbox(name, domain, userId, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
name = name.toLowerCase();
@@ -996,17 +913,15 @@ function addMailbox(name, domain, userId, auditSource, callback) {
var error = validateName(name);
if (error) return callback(error);
mailboxdb.addMailbox(name, domain, userId, function (error) {
mailboxdb.addMailbox(name, domain, userId, mailboxdb.OWNER_TYPE_USER, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new MailError(MailError.ALREADY_EXISTS, `mailbox ${name} already exists`));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_ADD, auditSource, { name, domain, userId });
callback(null);
});
}
function updateMailboxOwner(name, domain, userId, callback) {
function updateMailbox(name, domain, userId, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof userId, 'string');
@@ -1014,7 +929,10 @@ function updateMailboxOwner(name, domain, userId, callback) {
name = name.toLowerCase();
mailboxdb.updateMailboxOwner(name, domain, userId, function (error) {
var error = validateName(name);
if (error) return callback(error);
mailboxdb.updateMailbox(name, domain, userId, mailboxdb.OWNER_TYPE_USER, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such mailbox'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
@@ -1022,18 +940,15 @@ function updateMailboxOwner(name, domain, userId, callback) {
});
}
function removeMailbox(name, domain, auditSource, callback) {
function removeMailbox(name, domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
mailboxdb.del(name, domain, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such mailbox'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_REMOVE, auditSource, { name, domain });
callback(null);
});
}
@@ -1118,11 +1033,10 @@ function getList(domain, listName, callback) {
});
}
function addList(name, domain, members, auditSource, callback) {
function addList(name, domain, members, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof name, 'string');
assert(Array.isArray(members));
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
name = name.toLowerCase();
@@ -1141,8 +1055,6 @@ function addList(name, domain, members, auditSource, callback) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new MailError(MailError.ALREADY_EXISTS, 'list already exits'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_MAIL_LIST_ADD, auditSource, { name, domain });
callback();
});
}
@@ -1173,18 +1085,15 @@ function updateList(name, domain, members, callback) {
});
}
function removeList(name, domain, auditSource, callback) {
assert.strictEqual(typeof name, 'string');
function removeList(domain, listName, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof listName, 'string');
assert.strictEqual(typeof callback, 'function');
mailboxdb.del(name, domain, function (error) {
mailboxdb.del(listName, domain, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such list'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_MAIL_LIST_ADD, auditSource, { name, domain });
callback();
});
}
-14
View File
@@ -1,14 +0,0 @@
<%if (format === 'text') { %>
Dear Cloudron Admin,
The application '<%= title %>' installed at <%= appFqdn %> is back online
and responding to health checks.
Powered by https://cloudron.io
Sent at: <%= new Date().toUTCString() %>
<% } else { %>
<% } %>

Some files were not shown because too many files have changed in this diff Show More