Compare commits
377 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f839ae44e | |||
| db6404a7c6 | |||
| 93e0acc8e9 | |||
| 9fa7a48b86 | |||
| c0b929035f | |||
| 7612e38695 | |||
| 47329eaebc | |||
| f53a951daf | |||
| 2181137181 | |||
| 6e925f6b99 | |||
| 3b5495bf72 | |||
| 3617432113 | |||
| f95beff6d4 | |||
| 6d365fde14 | |||
| b16ff33688 | |||
| 9d8d0bed38 | |||
| f967116087 | |||
| 721352c5aa | |||
| 496ba986bf | |||
| 101a3b24ce | |||
| 201dc570cd | |||
| ff359c477f | |||
| 74cb8d9655 | |||
| 91d0710e04 | |||
| 0cc3f08ae7 | |||
| ac391bfc17 | |||
| e5a04e8d38 | |||
| 8cc07e51bf | |||
| 4b7090cf7c | |||
| 8c8cc035ab | |||
| 4b93d30ec0 | |||
| d8ff2488a3 | |||
| b771df88da | |||
| 54e237cec8 | |||
| b5c848474b | |||
| dae52089e3 | |||
| 4c4f3d04e9 | |||
| e8674487f2 | |||
| e2fadebf64 | |||
| d3331fea7f | |||
| bdcd9e035c | |||
| 7f3453ce5c | |||
| ed7a7bc879 | |||
| 5a6b8222df | |||
| 3262486a96 | |||
| c73b30556f | |||
| 2ec89d6a20 | |||
| a0b69df20d | |||
| 57aa3de9bb | |||
| 38a4c1aede | |||
| fcc77635c2 | |||
| 25be1563e1 | |||
| 4a9b0e8db6 | |||
| ab35821b59 | |||
| 14439ccf77 | |||
| 5ddfa989d0 | |||
| a915348b22 | |||
| a7fe35513a | |||
| 701024cf80 | |||
| 4ecb0d82e7 | |||
| 5279be64d0 | |||
| b9c3e85f89 | |||
| 8aaa671412 | |||
| 873ebddbd0 | |||
| 13c628b58b | |||
| 3500236d32 | |||
| 2f881c0c91 | |||
| 9d45e4e0ae | |||
| 13fac3072d | |||
| 6d8fdb131f | |||
| ee65089eb7 | |||
| 40c7d18382 | |||
| 3236a9a5b7 | |||
| d0522d7d4f | |||
| aef6b32019 | |||
| 11b4c886d7 | |||
| 3470252768 | |||
| 1a3d5d0bdc | |||
| 05f07b1f47 | |||
| 898f1dd151 | |||
| 17ac6bb1a4 | |||
| f05bed594b | |||
| e63b67b99e | |||
| efbc045c8a | |||
| 172d4b7c5e | |||
| 8b9177b484 | |||
| 2acb065d38 | |||
| 0b33b0b6a2 | |||
| 0390891280 | |||
| 9203534f67 | |||
| e15d11a693 | |||
| c021d3d9ce | |||
| ea3cc9b153 | |||
| 3612b64dae | |||
| 79f9180f6b | |||
| 766ef5f420 | |||
| bdbb9acfd0 | |||
| 6bdac3aaec | |||
| 14acdbe7d1 | |||
| 895280fc79 | |||
| 83ae303b31 | |||
| cc81a10dd2 | |||
| 6e3600011b | |||
| 2b07b5ba3a | |||
| 7b64b2a708 | |||
| 810f5e7409 | |||
| 1affb2517a | |||
| 85ea9b3255 | |||
| 07e052b865 | |||
| bc0ea740f1 | |||
| 841b4aa814 | |||
| 9989478b91 | |||
| d3227eceff | |||
| 5f71f6987c | |||
| 86dbb1bdcf | |||
| 77ac8d1e62 | |||
| e62d417324 | |||
| b8f85837fb | |||
| 2237d7ef8a | |||
| 65210ea91d | |||
| 16c1622b1f | |||
| 635557ca45 | |||
| b9daa62ece | |||
| 808be96de3 | |||
| 1e93289f23 | |||
| ccf0f84598 | |||
| 3ec4c7501d | |||
| f55034906c | |||
| cbd3c60c5d | |||
| 2037fec878 | |||
| 772fd1b563 | |||
| d9309cb215 | |||
| 433c34e4ce | |||
| 68a4769f1e | |||
| 248569d0a8 | |||
| 5146e39023 | |||
| ecd1d69863 | |||
| 06219b0c58 | |||
| 0a74bd1718 | |||
| 8a5b24afff | |||
| 6bdd7f7a57 | |||
| 1bb2552384 | |||
| b5b20452cc | |||
| 4a34703cd3 | |||
| a8d9b57c47 | |||
| 52bbf3be21 | |||
| 3bde0666e2 | |||
| b5374a1f90 | |||
| 18b8d23148 | |||
| f51b1e1b6b | |||
| ffc4f9d930 | |||
| 5680fc839b | |||
| 57d435ccf4 | |||
| 4b90b8e6d8 | |||
| fc8dcec2bb | |||
| a5245fda65 | |||
| 4eec2a6414 | |||
| a536e9fc4b | |||
| a961407379 | |||
| 1fd6c363ba | |||
| 0a7f1faad1 | |||
| e79d963802 | |||
| 1b4bbacd5f | |||
| 447c6fbb5f | |||
| 78acaccd89 | |||
| bdf9671280 | |||
| 357e44284d | |||
| 9dced3f596 | |||
| 63e3560dd7 | |||
| 434525943c | |||
| f0dbf2fc4d | |||
| 3137dbec33 | |||
| e71a8fce47 | |||
| ff550e897a | |||
| 10034fcbba | |||
| 36f8ce453f | |||
| c2e40acb2c | |||
| 82b1bb668d | |||
| 935a8258a6 | |||
| fa483e5806 | |||
| e0c9658cb9 | |||
| 0266a46b32 | |||
| e7294f2950 | |||
| c9f325e75d | |||
| 0fa353c2e2 | |||
| c7da090882 | |||
| ee609c8ef0 | |||
| 6891ce2bc8 | |||
| 94f5adba04 | |||
| b8f843993a | |||
| f9add21899 | |||
| 1277da8bfe | |||
| 55650fb734 | |||
| d2f4b68c9f | |||
| a76731a991 | |||
| 536b8166ce | |||
| d43106b0af | |||
| 3688371ce8 | |||
| 6d66eb7759 | |||
| 8502bf4bfa | |||
| d8225ad653 | |||
| cfb68a0511 | |||
| 76677e0aea | |||
| 515ee891d3 | |||
| 3aea1f3c9d | |||
| 8d944f9a4a | |||
| 331c8ae247 | |||
| c71a429f61 | |||
| 3bad9e523c | |||
| dfa61f1b2d | |||
| 6331fa5ced | |||
| 707b03b8c8 | |||
| f2f93ed141 | |||
| 37e16c7a4c | |||
| 41b0c3242e | |||
| 48ed051edf | |||
| 502642fd25 | |||
| 4abe6a7a00 | |||
| 3f8fa64b98 | |||
| 527ff1b1fb | |||
| 804467dce2 | |||
| 4d7f308821 | |||
| a5b8418845 | |||
| 93d428b8c5 | |||
| 7c424ad60c | |||
| 5b29a8680d | |||
| 8f57c44837 | |||
| b23939127b | |||
| 3196322063 | |||
| 54c96d98d1 | |||
| f5f92fbb03 | |||
| be0876603c | |||
| 7c1ef143f9 | |||
| 6d128595e7 | |||
| 2f55abfc60 | |||
| f93044ac3b | |||
| 7ed422a3c1 | |||
| 823b3b8aa8 | |||
| 9a701560f4 | |||
| 9800154d01 | |||
| 4b3f18ccdb | |||
| 840d78b2f4 | |||
| b409fd775d | |||
| dbcfb20fab | |||
| 12a5965740 | |||
| 006ab75433 | |||
| c72ea91743 | |||
| f39ce20580 | |||
| b5c59e6b7d | |||
| b0ecdcc8b6 | |||
| 8e1560f412 | |||
| df927eae74 | |||
| 30aea047e3 | |||
| cbcadaa449 | |||
| 9f4226093b | |||
| fca0e897b2 | |||
| 2f729b56fa | |||
| d9f3f64c76 | |||
| e8fa909c2f | |||
| 44f6636653 | |||
| 148a0d0fc6 | |||
| 632ba69663 | |||
| b2465dd2ee | |||
| e56b87766b | |||
| f7ca2e416a | |||
| 002f68b0a1 | |||
| aa31be5c5a | |||
| 6c0b7017bd | |||
| 581774e001 | |||
| 3847a6616e | |||
| 48fbe28355 | |||
| e3ee5bc1d5 | |||
| a2da9bea58 | |||
| e4512e12c5 | |||
| 114f48fb17 | |||
| 289e018160 | |||
| cb6699eeed | |||
| 802011bb7e | |||
| 6cd8e769be | |||
| 9f6f67d331 | |||
| 161a8fe2bf | |||
| b9c9839bb7 | |||
| 76edbee48c | |||
| 4142d7a050 | |||
| a0306c69e1 | |||
| 31823f6282 | |||
| 9b4fffde29 | |||
| cce03e250d | |||
| 9b32cad946 | |||
| 2877a1057e | |||
| e2debe3c39 | |||
| f54ab11f18 | |||
| b560e281d0 | |||
| 3bb4ef5727 | |||
| 900c008d20 | |||
| c1183a09a8 | |||
| e04b7b55b0 | |||
| 329cc80933 | |||
| a13f0706b4 | |||
| 55811de4b8 | |||
| ab456f179e | |||
| f9d5bcd352 | |||
| 6a337884b5 | |||
| f953d115da | |||
| 88e8fc840f | |||
| d1818e31b0 | |||
| 3f4bf647e8 | |||
| 725a7e6dec | |||
| e08b210001 | |||
| ec08ccb996 | |||
| b47a146c2b | |||
| 14dff27d45 | |||
| 305a3c94d0 | |||
| 218739a6b5 | |||
| 390e69c01c | |||
| 4ef274acf0 | |||
| 8267279779 | |||
| 6d971b9235 | |||
| 98dc160886 | |||
| a869c88b43 | |||
| 0b86070fe9 | |||
| 5c9b6736f0 | |||
| fd4057df94 | |||
| 1b1945e1f5 | |||
| ebb053b900 | |||
| 3381d9b595 | |||
| d7a11ef394 | |||
| 9d40cffabe | |||
| de44c63557 | |||
| ac25477cd7 | |||
| 59b86aa090 | |||
| 6abd48d480 | |||
| 72fc6b8c5a | |||
| fcce4a6853 | |||
| a3b1a2c781 | |||
| a838a1706f | |||
| a24c9fbafb | |||
| ab255e78c5 | |||
| 2628678d82 | |||
| eaf9b7f049 | |||
| b8df4d0b79 | |||
| eb315f34dc | |||
| af535757a8 | |||
| 600e030c6d | |||
| e86b813551 | |||
| af6653dfeb | |||
| f93e0c868c | |||
| e53aaddc9c | |||
| d3ebb99131 | |||
| 82037b70e4 | |||
| 323dfb1853 | |||
| 59b6d2ef66 | |||
| 7b99167d30 | |||
| c829f190df | |||
| f306f334c0 | |||
| 3fb8243264 | |||
| b8861b9d78 | |||
| 61d98ca4ca | |||
| a3a22604ba | |||
| 1766c3f6d6 | |||
| aae259479f | |||
| b640a053e3 | |||
| a3cc17705d | |||
| 78ac1d2a12 | |||
| 6ec36c9605 | |||
| bd2b03876b | |||
| 0c5cb9c98f | |||
| eddcc6fe27 | |||
| 953b55439c | |||
| e526890e3e | |||
| 71e1e2468b | |||
| fcb7c3e82a | |||
| 0d65635bc4 | |||
| 4aa76a859f | |||
| 003789265c | |||
| 1bb3b5e51b | |||
| 032ac3ed97 |
+2
-1
@@ -23,6 +23,7 @@
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
]
|
||||
],
|
||||
"no-console": "off"
|
||||
}
|
||||
}
|
||||
@@ -1425,3 +1425,100 @@
|
||||
* 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
|
||||
|
||||
|
||||
@@ -27,13 +27,15 @@ debconf-set-selections <<< 'mysql-server mysql-server/root_password_again passwo
|
||||
|
||||
# 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 \
|
||||
@@ -126,3 +128,9 @@ systemctl disable postfix || true
|
||||
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
|
||||
|
||||
|
||||
@@ -2,12 +2,16 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
require('supererror')({ splatchError: true });
|
||||
// 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)));
|
||||
};
|
||||
});
|
||||
|
||||
// remove timestamp from debug() based output
|
||||
require('debug').formatArgs = function formatArgs(args) {
|
||||
args[0] = this.namespace + ' ' + args[0];
|
||||
};
|
||||
require('supererror')({ splatchError: true });
|
||||
|
||||
let async = require('async'),
|
||||
config = require('./src/config.js'),
|
||||
|
||||
@@ -6,17 +6,18 @@ 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 <processName>');
|
||||
if (process.argv.length !== 3) return console.error('Usage: crashnotifier.js <unitName>');
|
||||
|
||||
var processName = process.argv[2];
|
||||
console.log('Started crash notifier for', processName);
|
||||
var unitName = process.argv[2];
|
||||
console.log('Started crash notifier for', unitName);
|
||||
|
||||
// mailer needs the db
|
||||
// eventlog api needs the db
|
||||
database.initialize(function (error) {
|
||||
if (error) return console.error('Cannot connect to database. Unable to send crash log.', error);
|
||||
|
||||
sendFailureLogs(processName, { unit: processName });
|
||||
sendFailureLogs(unitName);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
var cmd = "CREATE TABLE groups(" +
|
||||
var cmd = "CREATE TABLE userGroups(" +
|
||||
"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 groups', function (error) {
|
||||
db.runSql('DROP TABLE userGroups', 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 groups(id)," +
|
||||
"FOREIGN KEY(groupId) REFERENCES userGroups(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 groups (id, name) VALUES (?, ?)', [ ADMIN_GROUP_ID, 'admin' ]),
|
||||
db.runSql.bind(db, 'INSERT INTO userGroups (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 groups', function (error, results) {
|
||||
db.all('SELECT id, name FROM userGroups', 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 groups 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 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'),
|
||||
|
||||
@@ -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 groups WHERE id = ?', [ ADMIN_GROUP_ID ], function (error) {
|
||||
db.runSql('DELETE FROM userGroups 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 groups LEFT OUTER JOIN groupMembers ON groups.id = groupMembers.groupId ' +
|
||||
' GROUP BY groups.id', [ ], function (error, results) {
|
||||
' FROM userGroups LEFT OUTER JOIN groupMembers ON userGroups.id = groupMembers.groupId ' +
|
||||
' GROUP BY userGroups.id', [ ], function (error, results) {
|
||||
if (error) return done(error);
|
||||
|
||||
results.forEach(function (result) {
|
||||
|
||||
@@ -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 groups WHERE id=?', [ 'admin' ])
|
||||
db.runSql.bind(db, 'DELETE FROM userGroups WHERE id=?', [ 'admin' ])
|
||||
], callback);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
'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);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
'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();
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
'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();
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
'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();
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
'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();
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
'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);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
'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);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
'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);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
'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);
|
||||
});
|
||||
};
|
||||
+40
-11
@@ -29,7 +29,7 @@ CREATE TABLE IF NOT EXISTS users(
|
||||
|
||||
PRIMARY KEY(id));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS groups(
|
||||
CREATE TABLE IF NOT EXISTS userGroups(
|
||||
id VARCHAR(128) NOT NULL UNIQUE,
|
||||
name VARCHAR(254) NOT NULL UNIQUE,
|
||||
PRIMARY KEY(id));
|
||||
@@ -37,7 +37,7 @@ CREATE TABLE IF NOT EXISTS groups(
|
||||
CREATE TABLE IF NOT EXISTS groupMembers(
|
||||
groupId VARCHAR(128) NOT NULL,
|
||||
userId VARCHAR(128) NOT NULL,
|
||||
FOREIGN KEY(groupId) REFERENCES groups(id),
|
||||
FOREIGN KEY(groupId) REFERENCES userGroups(id),
|
||||
FOREIGN KEY(userId) REFERENCES users(id));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tokens(
|
||||
@@ -71,8 +71,8 @@ CREATE TABLE IF NOT EXISTS apps(
|
||||
location VARCHAR(128) NOT NULL,
|
||||
domain VARCHAR(128) NOT NULL,
|
||||
accessRestrictionJson TEXT, // { users: [ ], groups: [ ] }
|
||||
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
|
||||
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
|
||||
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),
|
||||
@@ -80,6 +80,8 @@ 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
|
||||
@@ -126,7 +128,7 @@ CREATE TABLE IF NOT EXISTS appEnvVars(
|
||||
|
||||
CREATE TABLE IF NOT EXISTS backups(
|
||||
id VARCHAR(128) NOT NULL,
|
||||
creationTime TIMESTAMP,
|
||||
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_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 */
|
||||
@@ -141,7 +143,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 */
|
||||
createdAt TIMESTAMP(2) NOT NULL,
|
||||
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
PRIMARY KEY (id));
|
||||
|
||||
@@ -173,15 +175,17 @@ 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, /* app id or user id or group id */
|
||||
ownerType VARCHAR(16) NOT NULL, /* 'app' or 'user' or 'group' */
|
||||
ownerId VARCHAR(128) NOT NULL, /* user id */
|
||||
aliasTarget VARCHAR(128), /* the target name type is an alias */
|
||||
membersJson TEXT, /* members of a group */
|
||||
creationTime TIMESTAMP,
|
||||
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
domain VARCHAR(128),
|
||||
|
||||
FOREIGN KEY(domain) REFERENCES mail(domain),
|
||||
@@ -195,6 +199,31 @@ CREATE TABLE IF NOT EXISTS subdomains(
|
||||
|
||||
FOREIGN KEY(domain) REFERENCES domains(domain),
|
||||
FOREIGN KEY(appId) REFERENCES apps(id),
|
||||
UNIQUE (subdomain, domain))
|
||||
UNIQUE (subdomain, domain));
|
||||
|
||||
CHARACTER SET utf8 COLLATE utf8_bin;
|
||||
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) 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;
|
||||
|
||||
Generated
+1866
-1473
File diff suppressed because it is too large
Load Diff
+5
-5
@@ -27,7 +27,7 @@
|
||||
"connect-timeout": "^1.9.0",
|
||||
"cookie-parser": "^1.3.5",
|
||||
"cookie-session": "^1.3.2",
|
||||
"cron": "^1.3.0",
|
||||
"cron": "^1.5.1",
|
||||
"csurf": "^1.6.6",
|
||||
"db-migrate": "^0.11.1",
|
||||
"db-migrate-mysql": "^1.1.10",
|
||||
@@ -45,6 +45,7 @@
|
||||
"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",
|
||||
@@ -87,12 +88,11 @@
|
||||
"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"
|
||||
"recursive-readdir": "^2.2.2",
|
||||
"sinon": "^7.2.2"
|
||||
},
|
||||
"scripts": {
|
||||
"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",
|
||||
"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",
|
||||
"postmerge": "/bin/true",
|
||||
"precommit": "/bin/true",
|
||||
"prepush": "npm test",
|
||||
|
||||
+40
-19
@@ -2,37 +2,47 @@
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
|
||||
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400 --http1.1"
|
||||
|
||||
ip=""
|
||||
zone=""
|
||||
subdomain=""
|
||||
cloudflare_token=""
|
||||
cloudflare_email=""
|
||||
dns_config=""
|
||||
tls_cert_file=""
|
||||
tls_key_file=""
|
||||
appstore_id=""
|
||||
appstore_token=""
|
||||
license_file=""
|
||||
backup_config=""
|
||||
|
||||
args=$(getopt -o "" -l "subdomain:,zone:,ip:,cloudflare-token:,cloudflare-email:,tls-cert:,tls-key:,appstore-id:,appstore-token:" -n "$0" -- "$@")
|
||||
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;;
|
||||
--subdomain) subdomain="$2"; shift 2;;
|
||||
--zone) zone="$2"; shift 2;;
|
||||
--cloudflare-token) cloudflare_token="$2"; shift 2;;
|
||||
--cloudflare-email) cloudflare_email="$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;;
|
||||
--appstore-id) appstore_id="$2"; shift 2;;
|
||||
--appstore-token) appstore_token="$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
|
||||
@@ -63,23 +73,34 @@ function wait_for_status() {
|
||||
echo "=> Waiting for cloudron to be ready"
|
||||
wait_for_status "version" '*'
|
||||
|
||||
echo "Provisioning Cloudron ${subdomain}.${zone}"
|
||||
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=null
|
||||
fallback_cert=None
|
||||
fi
|
||||
|
||||
setupData=$(printf '{ "dnsConfig": { "domain": "%s", "provider": "cloudflare", "config": { "token": "%s", "email": "%s", "hyphenatedSubdomains": true }, "tlsConfig": { "provider": "fallback" }, "fallbackCertificate": %s }, "autoconf": { "appstoreConfig": { "userId": "%s", "token": "%s" } } }' "${subdomain}.${zone}" "${cloudflare_token}" "${cloudflare_email}" "${fallback_cert}" "${appstore_id}" "${appstore_token}")
|
||||
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}"
|
||||
echo "Failed to setup with ${setupData} ${setupResult}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
wait_for_status "webadminStatus" '*"tls": true*'
|
||||
|
||||
echo "Cloudron is ready at https://my-${subdomain}.${zone}"
|
||||
echo "Cloudron is ready at https://my-${domain}"
|
||||
|
||||
|
||||
+25
-6
@@ -94,13 +94,14 @@ 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, ovh, rosehosting, scaleway, vultr or generic)"
|
||||
echo "--provider is required (azure, digitalocean, ec2, exoscale, gce, hetzner, lightsail, linode, netcup, ovh, rosehosting, scaleway, vultr or generic)"
|
||||
exit 1
|
||||
elif [[ \
|
||||
"${provider}" != "ami" && \
|
||||
"${provider}" != "azure" && \
|
||||
"${provider}" != "caas" && \
|
||||
"${provider}" != "cloudscale" && \
|
||||
"${provider}" != "contabo" && \
|
||||
"${provider}" != "digitalocean" && \
|
||||
"${provider}" != "ec2" && \
|
||||
"${provider}" != "exoscale" && \
|
||||
@@ -110,13 +111,14 @@ elif [[ \
|
||||
"${provider}" != "hetzner" && \
|
||||
"${provider}" != "lightsail" && \
|
||||
"${provider}" != "linode" && \
|
||||
"${provider}" != "netcup" && \
|
||||
"${provider}" != "ovh" && \
|
||||
"${provider}" != "rosehosting" && \
|
||||
"${provider}" != "scaleway" && \
|
||||
"${provider}" != "vultr" && \
|
||||
"${provider}" != "generic" \
|
||||
]]; then
|
||||
echo "--provider must be one of: azure, cloudscale.ch, digitalocean, ec2, exoscale, galaxygate, gce, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic"
|
||||
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"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -137,6 +139,18 @@ 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}"
|
||||
@@ -231,10 +245,15 @@ while true; do
|
||||
sleep 10
|
||||
done
|
||||
|
||||
echo -e "\n\n${GREEN}Visit https://<IP> and accept the self-signed certificate to finish setup.${DONE}"
|
||||
echo -e "\n\n${GREEN}Visit https://<IP> and accept the self-signed certificate to finish setup.${DONE}\n"
|
||||
|
||||
if [[ "${rebootServer}" == "true" ]]; then
|
||||
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
|
||||
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
|
||||
fi
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
#!/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
|
||||
@@ -11,13 +21,45 @@ if [[ ${EUID} -ne 0 ]]; then
|
||||
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
|
||||
if [[ $SUDO_USER == "" ]]; then
|
||||
ssh_user="root"
|
||||
ssh_folder="/root/.ssh/"
|
||||
authorized_key_file="${ssh_folder}/authorized_keys"
|
||||
@@ -35,7 +77,9 @@ echo -e $LINE"cloudron.conf"$LINE >> $OUT
|
||||
cat /etc/cloudron/cloudron.conf &>> $OUT
|
||||
|
||||
echo -e $LINE"Docker container"$LINE >> $OUT
|
||||
docker ps -a &>> $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
|
||||
@@ -43,6 +87,9 @@ 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
|
||||
|
||||
@@ -53,14 +100,16 @@ echo -n "Uploading information..."
|
||||
paste_key=$(curl -X POST ${PASTEBIN}/documents --silent -d "$(cat $OUT)" | python3 -c "import sys, json; print(json.load(sys.stdin)['key'])")
|
||||
echo "Done"
|
||||
|
||||
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"
|
||||
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 send the following link to support@cloudron.io"
|
||||
echo "Please email the following link to support@cloudron.io"
|
||||
echo ""
|
||||
echo "${PASTEBIN}/${paste_key}"
|
||||
|
||||
@@ -86,8 +86,14 @@ images=$(node -e "var i = require('${box_src_tmp_dir}/src/infra_version.js'); co
|
||||
|
||||
echo -e "\tPulling docker images: ${images}"
|
||||
for image in ${images}; do
|
||||
docker pull "${image}" # this pulls the image using the sha256
|
||||
docker pull "${image%@sha256:*}" # this will tag the image for readability
|
||||
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"
|
||||
|
||||
+13
-24
@@ -13,10 +13,12 @@ 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 script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly get_config="$(realpath ${script_dir}/../node_modules/.bin/json) -f /etc/cloudron/cloudron.conf"
|
||||
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
|
||||
|
||||
echo "==> Configuring docker"
|
||||
cp "${script_dir}/start/docker-cloudron-app.apparmor" /etc/apparmor.d/docker-cloudron-app
|
||||
@@ -58,7 +60,7 @@ 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"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/logs/backup" "${PLATFORM_DATA_DIR}/logs/updater" "${PLATFORM_DATA_DIR}/logs/tasks"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/update"
|
||||
|
||||
mkdir -p "${BOX_DATA_DIR}/appicons"
|
||||
@@ -88,28 +90,19 @@ 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}"
|
||||
|
||||
# migration for cloudron.conf file. Can be removed after 3.3
|
||||
if [[ ! -d /etc/cloudron ]]; then
|
||||
echo "==> Migrating existing cloudron.conf to new location"
|
||||
mkdir -p /etc/cloudron
|
||||
cp "${CONFIG_DIR}/cloudron.conf" /etc/cloudron/cloudron.conf
|
||||
fi
|
||||
chown -R "${USER}" /etc/cloudron
|
||||
|
||||
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
|
||||
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
|
||||
# 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
|
||||
# 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
|
||||
@@ -142,8 +135,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/app-logrotate" "${PLATFORM_DATA_DIR}/logrotate.d/app-logrotate"
|
||||
chown root:root "${PLATFORM_DATA_DIR}/logrotate.d/app-logrotate"
|
||||
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"
|
||||
|
||||
echo "==> Adding motd message for admins"
|
||||
cp "${script_dir}/start/cloudron-motd" /etc/update-motd.d/92-cloudron
|
||||
@@ -186,11 +179,7 @@ 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"
|
||||
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
|
||||
(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)
|
||||
|
||||
if [[ ! -f "${BOX_DATA_DIR}/dhparams.pem" ]]; then
|
||||
echo "==> Generating dhparams (takes forever)"
|
||||
@@ -201,8 +190,8 @@ else
|
||||
fi
|
||||
|
||||
echo "==> Changing ownership"
|
||||
chown "${USER}:${USER}" -R "${CONFIG_DIR}"
|
||||
# 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 "${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}"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# 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
|
||||
}
|
||||
+20
-8
@@ -1,11 +1,17 @@
|
||||
# sudo logging breaks journalctl output with very long urls (systemd bug)
|
||||
Defaults !syslog
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/rmvolume.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmvolume.sh
|
||||
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/rmaddon.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmaddon.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/reloadnginx.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/reloadnginx.sh
|
||||
@@ -25,15 +31,21 @@ 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/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/remotesupport.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/remotesupport.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/backuptask.js env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD:SETENV: /home/yellowtent/box/src/backuptask.js
|
||||
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/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
|
||||
|
||||
@@ -12,13 +12,17 @@ Wants=cloudron-resize-fs.service
|
||||
Type=idle
|
||||
WorkingDirectory=/home/yellowtent/box
|
||||
Restart=always
|
||||
ExecStart=/usr/bin/node --max_old_space_size=150 /home/yellowtent/box/box.js
|
||||
; 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'
|
||||
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
|
||||
MemoryLimit=200M
|
||||
; OOM killer is invoked in this unit beyond this. The start script replaces this with MemoryLimit for Ubuntu 16
|
||||
MemoryMax=400M
|
||||
TimeoutStopSec=5s
|
||||
StartLimitInterval=1
|
||||
StartLimitBurst=60
|
||||
|
||||
+567
-247
File diff suppressed because it is too large
Load Diff
@@ -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:8000;
|
||||
# proxy_pass http://127.0.0.1:8417;
|
||||
# client_max_body_size 1m;
|
||||
# }
|
||||
|
||||
+28
-21
@@ -18,6 +18,7 @@ exports = module.exports = {
|
||||
getAddonConfigByName: getAddonConfigByName,
|
||||
unsetAddonConfig: unsetAddonConfig,
|
||||
unsetAddonConfigByAppId: unsetAddonConfigByAppId,
|
||||
getAppIdByAddonConfigValue: getAppIdByAddonConfigValue,
|
||||
|
||||
setHealth: setHealth,
|
||||
setInstallationCommand: setInstallationCommand,
|
||||
@@ -61,7 +62,6 @@ 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,7 +69,8 @@ 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.ts' ].join(',');
|
||||
'apps.creationTime', 'apps.updateTime', 'apps.ownerId', 'apps.mailboxName', 'apps.enableAutomaticUpdate',
|
||||
'apps.dataDir', 'apps.ts' ].join(',');
|
||||
|
||||
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId' ].join(',');
|
||||
|
||||
@@ -120,6 +121,7 @@ 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);
|
||||
@@ -138,6 +140,9 @@ function postProcess(result) {
|
||||
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) {
|
||||
@@ -276,13 +281,14 @@ function add(id, appStoreId, manifest, location, domain, ownerId, portBindings,
|
||||
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) ' +
|
||||
' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
args: [ id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId ]
|
||||
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 ]
|
||||
});
|
||||
|
||||
queries.push({
|
||||
@@ -304,14 +310,6 @@ function add(id, appStoreId, manifest, location, domain, ownerId, portBindings,
|
||||
});
|
||||
});
|
||||
|
||||
// only allocate a mailbox if mailboxName is set
|
||||
if (data.mailboxName) {
|
||||
queries.push({
|
||||
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) {
|
||||
queries.push({
|
||||
@@ -376,7 +374,6 @@ 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 ] }
|
||||
@@ -384,7 +381,7 @@ function del(id, callback) {
|
||||
|
||||
database.transaction(queries, function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (results[4].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
if (results[3].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -442,12 +439,8 @@ function updateWithConstraints(id, app, constraints, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
if ('location' in app) {
|
||||
queries.push({ query: 'UPDATE subdomains SET subdomain = ? WHERE appId = ? AND type = ?', args: [ app.location, 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 ('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 ('alternateDomains' in app) {
|
||||
@@ -621,6 +614,20 @@ 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');
|
||||
|
||||
+34
-26
@@ -7,7 +7,7 @@ var appdb = require('./appdb.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:apphealthmonitor'),
|
||||
docker = require('./docker.js').connection,
|
||||
mailer = require('./mailer.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util');
|
||||
|
||||
@@ -15,11 +15,12 @@ exports = module.exports = {
|
||||
run: run
|
||||
};
|
||||
|
||||
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 }
|
||||
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
|
||||
let gHealthInfo = { }; // { time, emailSent }
|
||||
|
||||
const NOOP_CALLBACK = function (error) { if (error) console.error(error); };
|
||||
const OOM_MAIL_LIMIT = 60 * 60 * 1000; // 60 minutes
|
||||
let gLastOomMailTime = Date.now() - (5 * 60 * 1000); // pretend we sent email 5 minutes ago
|
||||
|
||||
function debugApp(app) {
|
||||
assert(typeof app === 'object');
|
||||
@@ -45,7 +46,9 @@ function setHealth(app, health, callback) {
|
||||
|
||||
debugApp(app, 'marking as unhealthy since not seen for more than %s minutes', UNHEALTHY_THRESHOLD/(60 * 1000));
|
||||
|
||||
if (!app.debugMode) mailer.appDied(app); // do not send mails for dev apps
|
||||
// do not send mails for dev apps
|
||||
if (!app.debugMode) eventlog.add(eventlog.ACTION_APP_DOWN, { app: app }, {});
|
||||
|
||||
gHealthInfo[app.id].emailSent = true;
|
||||
} else {
|
||||
debugApp(app, 'waiting for sometime to update the app health');
|
||||
@@ -118,13 +121,11 @@ function checkAppHealth(app, callback) {
|
||||
apt-get update && apt-get install stress
|
||||
stress --vm 1 --vm-bytes 200M --vm-hang 0
|
||||
*/
|
||||
function processDockerEvents(interval, callback) {
|
||||
assert.strictEqual(typeof interval, 'number');
|
||||
function processDockerEvents(intervalSecs, callback) {
|
||||
assert.strictEqual(typeof intervalSecs, 'number');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const OOM_MAIL_LIMIT = 60 * 60 * 1000; // 60 minutes
|
||||
let lastOomMailTime = new Date(new Date() - OOM_MAIL_LIMIT);
|
||||
const since = ((new Date().getTime() / 1000) - interval).toFixed(0);
|
||||
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) {
|
||||
@@ -133,18 +134,26 @@ function processDockerEvents(interval, callback) {
|
||||
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';
|
||||
var containerId = ev.id;
|
||||
|
||||
debug('OOM Context: %s', context);
|
||||
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 ((!app || !app.debugMode) && (now - lastOomMailTime > OOM_MAIL_LIMIT)) {
|
||||
mailer.oomEvent(program, context); // app can be null if it's an addon crash
|
||||
lastOomMailTime = now;
|
||||
if (notifyUser) {
|
||||
var auditSource = {
|
||||
containerId: containerId,
|
||||
app: app || null // app can be null if it's an addon crash
|
||||
};
|
||||
|
||||
eventlog.add(eventlog.ACTION_APP_OOM, auditSource, ev);
|
||||
|
||||
gLastOomMailTime = now;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -181,14 +190,13 @@ function processApp(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function run(interval, callback) {
|
||||
assert.strictEqual(typeof interval, 'number');
|
||||
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
function run(intervalSecs, callback) {
|
||||
assert.strictEqual(typeof intervalSecs, 'number');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
async.series([
|
||||
processDockerEvents.bind(null, interval),
|
||||
processApp
|
||||
processApp, // this is first because docker.getEvents seems to get 'stuck' sometimes
|
||||
processDockerEvents.bind(null, intervalSecs)
|
||||
], function (error) {
|
||||
if (error) debug(error);
|
||||
|
||||
|
||||
+133
-92
@@ -38,6 +38,7 @@ exports = module.exports = {
|
||||
configureInstalledApps: configureInstalledApps,
|
||||
|
||||
getAppConfig: getAppConfig,
|
||||
getDataDir: getDataDir,
|
||||
|
||||
downloadFile: downloadFile,
|
||||
uploadFile: uploadFile,
|
||||
@@ -72,7 +73,6 @@ 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,7 +154,8 @@ function validatePortBindings(portBindings, manifest) {
|
||||
config.get('ldapPort'), /* ldap server (lo) */
|
||||
3306, /* mysql (lo) */
|
||||
4190, /* managesieve */
|
||||
8000, /* graphite (lo) */
|
||||
8000, /* ESXi monitoring */
|
||||
8417, /* graphite (lo) */
|
||||
];
|
||||
|
||||
if (!portBindings) return null;
|
||||
@@ -301,26 +302,50 @@ function validateEnv(env) {
|
||||
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);
|
||||
return new AppsError(AppsError.INTERNAL_ERROR, error);
|
||||
}
|
||||
|
||||
// check if the location conflicts
|
||||
if (match[1] === location) return new AppsError(AppsError.ALREADY_EXISTS);
|
||||
if (match[1] === location) return new AppsError(AppsError.ALREADY_EXISTS, `${match[2]} '${match[1]}' is in use`);
|
||||
|
||||
// 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);
|
||||
return new AppsError(AppsError.ALREADY_EXISTS, `${match[2]} '${match[1]}' is in use`);
|
||||
}
|
||||
|
||||
// app configs that is useful for 'archival' into the app backup config.json
|
||||
@@ -329,6 +354,7 @@ function getAppConfig(app) {
|
||||
manifest: app.manifest,
|
||||
location: app.location,
|
||||
domain: app.domain,
|
||||
fqdn: app.fqdn,
|
||||
accessRestriction: app.accessRestriction,
|
||||
portBindings: app.portBindings,
|
||||
memoryLimit: app.memoryLimit,
|
||||
@@ -336,17 +362,22 @@ function getAppConfig(app) {
|
||||
robotsTxt: app.robotsTxt,
|
||||
sso: app.sso,
|
||||
alternateDomains: app.alternateDomains || [],
|
||||
env: app.env
|
||||
env: app.env,
|
||||
dataDir: app.dataDir
|
||||
};
|
||||
}
|
||||
|
||||
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');
|
||||
'alternateDomains', 'ownerId', 'env', 'enableAutomaticUpdate', 'dataDir');
|
||||
}
|
||||
|
||||
function removeRestrictedFields(app) {
|
||||
@@ -399,13 +430,7 @@ function get(appId, callback) {
|
||||
app.fqdn = domains.fqdn(app.location, domainObjectMap[app.domain]);
|
||||
app.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
|
||||
|
||||
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);
|
||||
});
|
||||
callback(null, app);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -439,13 +464,7 @@ function getByIpAddress(ip, callback) {
|
||||
app.fqdn = domains.fqdn(app.location, domainObjectMap[app.domain]);
|
||||
app.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
|
||||
|
||||
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);
|
||||
});
|
||||
callback(null, app);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -471,13 +490,7 @@ function getAll(callback) {
|
||||
app.fqdn = domains.fqdn(app.location, domainObjectMap[app.domain]);
|
||||
app.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
|
||||
|
||||
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return iteratorDone(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (!error) app.mailboxName = mailboxes[0].name;
|
||||
|
||||
iteratorDone(null, app);
|
||||
});
|
||||
iteratorDone(null, app);
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
@@ -543,11 +556,13 @@ 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 || {};
|
||||
env = data.env || {},
|
||||
mailboxName = data.mailboxName || '';
|
||||
|
||||
assert(data.appStoreId || data.manifest); // atleast one of them is required
|
||||
|
||||
@@ -588,6 +603,13 @@ function install(data, user, auditSource, callback) {
|
||||
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) {
|
||||
@@ -621,9 +643,10 @@ function install(data, user, auditSource, callback) {
|
||||
xFrameOptions: xFrameOptions,
|
||||
sso: sso,
|
||||
debugMode: debugMode,
|
||||
mailboxName: mailboxNameForLocation(location, manifest),
|
||||
mailboxName: mailboxName,
|
||||
restoreConfig: backupId ? { backupId: backupId, backupFormat: backupFormat } : null,
|
||||
enableBackup: enableBackup,
|
||||
enableAutomaticUpdate: enableAutomaticUpdate,
|
||||
robotsTxt: robotsTxt,
|
||||
alternateDomains: alternateDomains,
|
||||
env: env
|
||||
@@ -682,12 +705,14 @@ function configure(appId, data, user, auditSource, callback) {
|
||||
get(appId, function (error, app) {
|
||||
if (error) return callback(error);
|
||||
|
||||
let domain, location, portBindings, values = { }, mailboxName;
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
if ('accessRestriction' in data) {
|
||||
values.accessRestriction = data.accessRestriction;
|
||||
@@ -729,15 +754,15 @@ function configure(appId, data, user, auditSource, callback) {
|
||||
}
|
||||
|
||||
if ('mailboxName' in data) {
|
||||
if (data.mailboxName === '') { // special case to reset back to .app
|
||||
mailboxName = mailboxNameForLocation(location, app.manifest);
|
||||
} else {
|
||||
if (data.mailboxName) {
|
||||
error = mail.validateName(data.mailboxName);
|
||||
if (error) return callback(error);
|
||||
mailboxName = data.mailboxName;
|
||||
values.mailboxName = data.mailboxName;
|
||||
} else {
|
||||
values.mailboxName = mailboxNameForLocation(location, app.manifest);
|
||||
}
|
||||
} else { // keep existing name or follow the new location
|
||||
mailboxName = app.mailboxName.endsWith('.app') ? mailboxNameForLocation(location, app.manifest) : app.mailboxName;
|
||||
values.mailboxName = app.mailboxName.endsWith('.app') ? mailboxNameForLocation(location, app.manifest) : app.mailboxName;
|
||||
}
|
||||
|
||||
if ('alternateDomains' in data) {
|
||||
@@ -752,6 +777,12 @@ function configure(appId, data, user, auditSource, callback) {
|
||||
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));
|
||||
@@ -773,34 +804,26 @@ function configure(appId, data, user, auditSource, callback) {
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// make the mailbox name follow the apps new location, if the user did not set it explicitly
|
||||
mailboxdb.updateName(app.mailboxName /* old */, values.oldConfig.domain, mailboxName, domain, function (error) {
|
||||
if (mailboxName.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'));
|
||||
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));
|
||||
|
||||
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));
|
||||
taskmanager.restartAppTask(appId);
|
||||
|
||||
// fetch fresh app object for eventlog
|
||||
get(appId, function (error, result) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
taskmanager.restartAppTask(appId);
|
||||
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: result });
|
||||
|
||||
// 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);
|
||||
});
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -815,34 +838,22 @@ function update(appId, data, auditSource, callback) {
|
||||
|
||||
debug('Will update app with id:%s', appId);
|
||||
|
||||
downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) {
|
||||
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;
|
||||
|
||||
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) {
|
||||
downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) {
|
||||
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) {
|
||||
@@ -851,6 +862,22 @@ function update(appId, data, auditSource, callback) {
|
||||
updateConfig.appStoreId = '';
|
||||
}
|
||||
|
||||
if (app.appStoreId !== '' && semver.lte(updateConfig.manifest.version, app.manifest.version)) {
|
||||
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'));
|
||||
|
||||
@@ -882,16 +909,19 @@ 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) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var lines = options.lines || 100,
|
||||
var lines = options.lines === -1 ? '+1' : options.lines,
|
||||
format = options.format || 'json',
|
||||
follow = !!options.follow;
|
||||
follow = options.follow;
|
||||
|
||||
assert.strictEqual(typeof lines, 'number');
|
||||
assert.strictEqual(typeof format, 'string');
|
||||
|
||||
var args = [ '--lines=' + lines ];
|
||||
@@ -966,7 +996,7 @@ function restore(appId, data, auditSource, callback) {
|
||||
|
||||
taskmanager.restartAppTask(appId);
|
||||
|
||||
eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { appId: appId, app: app });
|
||||
eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { app: app, backupId: backupInfo.id, fromManifest: app.manifest, toManifest: backupInfo.manifest });
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -987,7 +1017,8 @@ function clone(appId, data, user, auditSource, callback) {
|
||||
domain = data.domain.toLowerCase(),
|
||||
portBindings = data.portBindings || null,
|
||||
backupId = data.backupId,
|
||||
ownerId = data.ownerId;
|
||||
ownerId = data.ownerId,
|
||||
mailboxName = data.mailboxName || '';
|
||||
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
@@ -1005,13 +1036,22 @@ 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(backupInfo.manifest);
|
||||
error = checkManifestConstraints(manifest);
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validatePortBindings(portBindings, backupInfo.manifest);
|
||||
error = validatePortBindings(portBindings, 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));
|
||||
@@ -1020,7 +1060,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(), manifest = backupInfo.manifest;
|
||||
var newAppId = uuid.v4();
|
||||
|
||||
var data = {
|
||||
installationState: appdb.ISTATE_PENDING_CLONE,
|
||||
@@ -1029,7 +1069,7 @@ function clone(appId, data, user, auditSource, callback) {
|
||||
xFrameOptions: app.xFrameOptions,
|
||||
restoreConfig: { backupId: backupId, backupFormat: backupInfo.format },
|
||||
sso: !!app.sso,
|
||||
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app',
|
||||
mailboxName: mailboxName,
|
||||
enableBackup: app.enableBackup,
|
||||
robotsTxt: app.robotsTxt,
|
||||
env: app.env
|
||||
@@ -1220,6 +1260,7 @@ 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 || { };
|
||||
|
||||
+45
-34
@@ -52,9 +52,12 @@ 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); };
|
||||
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -133,27 +136,14 @@ function createContainer(app, 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 callback, 'function');
|
||||
|
||||
debugApp(app, 'deleting main app container');
|
||||
|
||||
docker.deleteContainer(app.containerId, function (error) {
|
||||
if (error) return callback(new Error('Error deleting container: ' + error));
|
||||
|
||||
updateApp(app, { containerId: null }, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function deleteContainers(app, callback) {
|
||||
function deleteContainers(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'deleting app containers (app, scheduler)');
|
||||
|
||||
docker.deleteContainers(app.id, function (error) {
|
||||
docker.deleteContainers(app.id, options, function (error) {
|
||||
if (error) return callback(new Error('Error deleting container: ' + error));
|
||||
|
||||
updateApp(app, { containerId: null }, callback);
|
||||
@@ -185,6 +175,7 @@ function deleteAppDir(app, options, callback) {
|
||||
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));
|
||||
@@ -210,7 +201,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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -220,7 +211,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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -239,7 +230,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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -248,7 +239,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) {
|
||||
@@ -467,6 +458,19 @@ function waitForDnsPropagation(app, 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.
|
||||
@@ -495,14 +499,14 @@ function install(app, callback) {
|
||||
removeCollectdProfile.bind(null, app),
|
||||
removeLogrotateConfig.bind(null, app),
|
||||
stopApp.bind(null, app),
|
||||
deleteMainContainer.bind(null, app),
|
||||
deleteContainers.bind(null, app, { managedOnly: true }),
|
||||
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));
|
||||
|
||||
addons.teardownAddons(app, addonsToRemove, next);
|
||||
},
|
||||
deleteAppDir.bind(null, app, { removeDirectory: false }), // do not remove any symlinked volume
|
||||
deleteAppDir.bind(null, app, { removeDirectory: false }), // do not remove any symlinked appdata dir
|
||||
|
||||
// for restore case
|
||||
function deleteImageIfChanged(done) {
|
||||
@@ -525,7 +529,7 @@ function install(app, callback) {
|
||||
updateApp.bind(null, app, { installationProgress: '40, Downloading image' }),
|
||||
docker.downloadImage.bind(null, app.manifest),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '50, Creating volume' }),
|
||||
updateApp.bind(null, app, { installationProgress: '50, Creating app data directory' }),
|
||||
createAppDir.bind(null, app),
|
||||
|
||||
function restoreFromBackup(next) {
|
||||
@@ -536,10 +540,10 @@ function install(app, callback) {
|
||||
], next);
|
||||
} else {
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '60, Download backup and restoring addons' }),
|
||||
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),
|
||||
backups.restoreApp.bind(null, app, app.manifest.addons, restoreConfig, (progress) => updateApp(app, { installationProgress: `65, Restore - ${progress.message}` }, NOOP_CALLBACK))
|
||||
], next);
|
||||
}
|
||||
},
|
||||
@@ -581,7 +585,7 @@ function backup(app, callback) {
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '10, Backing up' }),
|
||||
backups.backupApp.bind(null, app),
|
||||
backups.backupApp.bind(null, app, (progress) => updateApp(app, { installationProgress: `30, ${progress.message}` }, NOOP_CALLBACK)),
|
||||
|
||||
// done!
|
||||
function (callback) {
|
||||
@@ -603,7 +607,8 @@ function configure(app, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// oldConfig can be null during an infra update
|
||||
var locationChanged = app.oldConfig && (app.oldConfig.fqdn !== app.fqdn);
|
||||
const locationChanged = app.oldConfig && (app.oldConfig.fqdn !== app.fqdn);
|
||||
const dataDirChanged = app.oldConfig && (app.oldConfig.dataDir !== app.dataDir);
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
|
||||
@@ -611,7 +616,7 @@ function configure(app, callback) {
|
||||
removeCollectdProfile.bind(null, app),
|
||||
removeLogrotateConfig.bind(null, app),
|
||||
stopApp.bind(null, app),
|
||||
deleteMainContainer.bind(null, app),
|
||||
deleteContainers.bind(null, app, { managedOnly: true }),
|
||||
unregisterAlternateDomains.bind(null, app, false /* all */),
|
||||
function (next) {
|
||||
if (!locationChanged) return next();
|
||||
@@ -619,7 +624,6 @@ 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' }),
|
||||
@@ -634,13 +638,20 @@ 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 volume' }),
|
||||
updateApp.bind(null, app, { installationProgress: '45, Ensuring app data directory' }),
|
||||
createAppDir.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),
|
||||
|
||||
@@ -694,7 +705,7 @@ function update(app, callback) {
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '15, Backing up app' }),
|
||||
backups.backupApp.bind(null, app)
|
||||
backups.backupApp.bind(null, app, (progress) => updateApp(app, { installationProgress: `15, Backup - ${progress.message}` }, NOOP_CALLBACK))
|
||||
], function (error) {
|
||||
if (error) error.backupError = true;
|
||||
next(error);
|
||||
@@ -712,7 +723,7 @@ function update(app, callback) {
|
||||
removeCollectdProfile.bind(null, app),
|
||||
removeLogrotateConfig.bind(null, app),
|
||||
stopApp.bind(null, app),
|
||||
deleteMainContainer.bind(null, app),
|
||||
deleteContainers.bind(null, app, { managedOnly: true }),
|
||||
function deleteImageIfChanged(done) {
|
||||
if (app.manifest.dockerImage === app.updateConfig.manifest.dockerImage) return done();
|
||||
|
||||
@@ -798,12 +809,12 @@ 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 volume' }),
|
||||
updateApp.bind(null, app, { installationProgress: '40, Deleting app data directory' }),
|
||||
deleteAppDir.bind(null, app, { removeDirectory: true }),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '50, Deleting image' }),
|
||||
|
||||
+301
-248
File diff suppressed because it is too large
Load Diff
+1
-137
@@ -4,29 +4,18 @@ 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'),
|
||||
_ = require('underscore');
|
||||
|
||||
const RETIRE_CMD = path.join(__dirname, 'scripts/retire.sh');
|
||||
util = require('util');
|
||||
|
||||
function CaasError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
@@ -53,20 +42,6 @@ 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');
|
||||
|
||||
@@ -117,96 +92,6 @@ 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');
|
||||
@@ -223,27 +108,6 @@ 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');
|
||||
|
||||
@@ -1,481 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
crypto = require('crypto'),
|
||||
debug = require('debug')('box:cert/acme1'),
|
||||
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 Acme1Error(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(Acme1Error, Error);
|
||||
Acme1Error.INTERNAL_ERROR = 'Internal Error';
|
||||
Acme1Error.EXTERNAL_ERROR = 'External Error';
|
||||
Acme1Error.ALREADY_EXISTS = 'Already Exists';
|
||||
Acme1Error.NOT_COMPLETED = 'Not Completed';
|
||||
Acme1Error.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 Acme1(options) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
|
||||
this.caOrigin = options.prod ? CA_PROD : CA_STAGING;
|
||||
this.accountKeyPem = null; // Buffer
|
||||
this.email = options.email;
|
||||
}
|
||||
|
||||
Acme1.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');
|
||||
}
|
||||
|
||||
Acme1.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);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Acme1.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 Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
|
||||
if (result.statusCode !== 202) return callback(new Acme1Error(Acme1Error.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();
|
||||
});
|
||||
};
|
||||
|
||||
Acme1.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 Acme1Error(Acme1Error.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 Acme1Error(Acme1Error.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);
|
||||
});
|
||||
};
|
||||
|
||||
Acme1.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 Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when registering domain: ' + error.message));
|
||||
if (result.statusCode === 403) return callback(new Acme1Error(Acme1Error.FORBIDDEN, result.body.detail));
|
||||
if (result.statusCode !== 201) return callback(new Acme1Error(Acme1Error.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);
|
||||
});
|
||||
};
|
||||
|
||||
Acme1.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 Acme1Error(Acme1Error.INTERNAL_ERROR, error));
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
Acme1.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 Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when notifying challenge: ' + error.message));
|
||||
if (result.statusCode !== 202) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to notify challenge. Expecting 202, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
Acme1.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 Acme1Error(Acme1Error.EXTERNAL_ERROR, error.message)); // network error
|
||||
}
|
||||
if (result.statusCode !== 202) {
|
||||
debug('waitForChallenge: invalid response code getting uri %s', result.statusCode);
|
||||
return retryCallback(new Acme1Error(Acme1Error.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 Acme1Error(Acme1Error.NOT_COMPLETED));
|
||||
else if (result.body.status === 'valid') return retryCallback();
|
||||
else return retryCallback(new Acme1Error(Acme1Error.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
|
||||
Acme1.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 Acme1Error(Acme1Error.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 Acme1Error(Acme1Error.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 Acme1Error(Acme1Error.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);
|
||||
});
|
||||
};
|
||||
|
||||
Acme1.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 Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
|
||||
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new Acme1Error(Acme1Error.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 Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
|
||||
if (!safe.fs.writeFileSync(csrFile, csrDer)) return callback(new Acme1Error(Acme1Error.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
|
||||
Acme1.prototype.downloadChain = function (linkHeader, callback) {
|
||||
if (!linkHeader) return new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Empty link header when downloading certificate chain');
|
||||
|
||||
debug('downloadChain: linkHeader %s', linkHeader);
|
||||
|
||||
var linkInfo = parseLinks(linkHeader);
|
||||
if (!linkInfo || !linkInfo.up) return new Acme1Error(Acme1Error.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 Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when downloading certificate'));
|
||||
if (result.statusCode !== 200) return callback(new Acme1Error(Acme1Error.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 Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
|
||||
|
||||
callback(null, chainPem);
|
||||
});
|
||||
};
|
||||
|
||||
Acme1.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 Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when downloading certificate'));
|
||||
if (result.statusCode === 202) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, 'Retry not implemented yet'));
|
||||
if (result.statusCode !== 200) return callback(new Acme1Error(Acme1Error.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 Acme1Error(Acme1Error.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 Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
|
||||
|
||||
debug('downloadCertificate: cert file for %s saved at %s', domain, certificateFile);
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Acme1.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 Acme1Error(Acme1Error.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 Acme1Error(Acme1Error.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);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Acme1.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 %s from %s', hostname, this.caOrigin);
|
||||
this.acmeFlow(hostname, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var outdir = paths.APP_CERTS_DIR;
|
||||
callback(null, path.join(outdir, hostname + '.cert'), path.join(outdir, hostname + '.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 Acme1(options || { });
|
||||
acme.getCertificate(hostname, domain, callback);
|
||||
}
|
||||
+5
-6
@@ -5,7 +5,6 @@ var assert = require('assert'),
|
||||
crypto = require('crypto'),
|
||||
debug = require('debug')('box:cert/acme2'),
|
||||
domains = require('../domains.js'),
|
||||
execSync = require('safetydance').child_process.execSync,
|
||||
fs = require('fs'),
|
||||
path = require('path'),
|
||||
paths = require('../paths.js'),
|
||||
@@ -88,7 +87,7 @@ function b64(str) {
|
||||
function getModulus(pem) {
|
||||
assert(util.isBuffer(pem));
|
||||
|
||||
var stdout = execSync('openssl rsa -modulus -noout', { input: pem, encoding: 'utf8' });
|
||||
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;
|
||||
@@ -227,7 +226,7 @@ Acme2.prototype.waitForOrder = function (orderUrl, callback) {
|
||||
|
||||
debug(`waitForOrder: ${orderUrl}`);
|
||||
|
||||
async.retry({ times: 10, interval: 5000 }, function (retryCallback) {
|
||||
async.retry({ times: 15, interval: 20000 }, function (retryCallback) {
|
||||
debug('waitForOrder: getting status');
|
||||
|
||||
superagent.get(orderUrl).timeout(30 * 1000).end(function (error, result) {
|
||||
@@ -291,7 +290,7 @@ Acme2.prototype.waitForChallenge = function (challenge, callback) {
|
||||
|
||||
debug('waitingForChallenge: %j', challenge);
|
||||
|
||||
async.retry({ times: 10, interval: 5000 }, function (retryCallback) {
|
||||
async.retry({ times: 15, interval: 20000 }, function (retryCallback) {
|
||||
debug('waitingForChallenge: getting status');
|
||||
|
||||
superagent.get(challenge.url).timeout(30 * 1000).end(function (error, result) {
|
||||
@@ -351,14 +350,14 @@ Acme2.prototype.createKeyAndCsr = function (hostname, callback) {
|
||||
// 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');
|
||||
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 = execSync(`openssl req -new -key ${privateKeyFile} -outform DER -subj /CN=${hostname}`);
|
||||
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
|
||||
|
||||
|
||||
+116
-68
@@ -10,30 +10,39 @@ exports = module.exports = {
|
||||
getLogs: getLogs,
|
||||
|
||||
reboot: reboot,
|
||||
isRebootRequired: isRebootRequired,
|
||||
|
||||
onActivated: onActivated,
|
||||
|
||||
prepareDashboardDomain: prepareDashboardDomain,
|
||||
setDashboardDomain: setDashboardDomain,
|
||||
renewCerts: renewCerts,
|
||||
|
||||
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'),
|
||||
fs = require('fs'),
|
||||
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');
|
||||
|
||||
@@ -65,18 +74,13 @@ 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');
|
||||
|
||||
async.series([
|
||||
settings.initialize,
|
||||
reverseProxy.configureDefaultServer,
|
||||
cron.startPreActivationJobs,
|
||||
onActivated
|
||||
], callback);
|
||||
cron.startPreActivationJobs(callback);
|
||||
|
||||
runStartupTasks();
|
||||
}
|
||||
|
||||
function uninitialize(callback) {
|
||||
@@ -84,25 +88,36 @@ function uninitialize(callback) {
|
||||
|
||||
async.series([
|
||||
cron.stopJobs,
|
||||
platform.stop,
|
||||
settings.uninitialize
|
||||
platform.stop
|
||||
], callback);
|
||||
}
|
||||
|
||||
function onActivated(callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// 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)
|
||||
users.count(function (error, count) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
if (!count) return callback(); // not activated
|
||||
async.series([
|
||||
platform.start,
|
||||
cron.startPostActivationJobs
|
||||
], callback);
|
||||
}
|
||||
|
||||
async.series([
|
||||
platform.start,
|
||||
cron.startPostActivationJobs
|
||||
], callback);
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -146,7 +161,6 @@ function getConfig(callback) {
|
||||
adminFqdn: config.adminFqdn(),
|
||||
mailFqdn: config.mailFqdn(),
|
||||
version: config.version(),
|
||||
progress: progress.getAll(),
|
||||
isDemo: config.isDemo(),
|
||||
edition: config.edition(),
|
||||
memory: os.totalmem(),
|
||||
@@ -157,7 +171,14 @@ function getConfig(callback) {
|
||||
}
|
||||
|
||||
function reboot(callback) {
|
||||
shell.sudo('reboot', [ REBOOT_CMD ], 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'));
|
||||
}
|
||||
|
||||
function checkDiskSpace(callback) {
|
||||
@@ -207,61 +228,38 @@ function getLogs(unit, options, callback) {
|
||||
assert(options && typeof options === 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var lines = options.lines || 100,
|
||||
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,
|
||||
format = options.format || 'json',
|
||||
follow = !!options.follow;
|
||||
|
||||
assert.strictEqual(typeof lines, 'number');
|
||||
assert.strictEqual(typeof format, 'string');
|
||||
|
||||
assert.strictEqual(typeof lines, 'number');
|
||||
assert.strictEqual(typeof format, 'string');
|
||||
follow = options.follow;
|
||||
|
||||
debug('Getting logs for %s as %s', unit, format);
|
||||
|
||||
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);
|
||||
let args = [ '--lines=' + lines ];
|
||||
if (follow) args.push('--follow');
|
||||
|
||||
transformStream = split(function mapper(line) {
|
||||
if (format !== 'json') return line + '\n';
|
||||
// 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'));
|
||||
|
||||
var obj = safe.JSON.parse(line);
|
||||
if (!obj) return undefined;
|
||||
var cp = spawn('/usr/bin/tail', args);
|
||||
|
||||
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 transformStream = split(function mapper(line) {
|
||||
if (format !== 'json') return line + '\n';
|
||||
|
||||
cp = spawn('/usr/bin/tail', args);
|
||||
var data = line.split(' '); // logs are <ISOtimestamp> <msg>
|
||||
var timestamp = (new Date(data[0])).getTime();
|
||||
if (isNaN(timestamp)) timestamp = 0;
|
||||
|
||||
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';
|
||||
});
|
||||
}
|
||||
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
|
||||
|
||||
@@ -269,3 +267,53 @@ 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, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
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));
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
+5
-3
@@ -119,8 +119,9 @@ function initConfig() {
|
||||
if (exports.TEST) {
|
||||
data.port = 5454;
|
||||
data.apiServerOrigin = 'http://localhost:6060'; // hock doesn't support https
|
||||
data.database.password = '';
|
||||
data.database.name = 'boxtest';
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
// overwrite defaults with saved config
|
||||
@@ -231,7 +232,8 @@ function isManaged() {
|
||||
|
||||
function hasIPv6() {
|
||||
const IPV6_PROC_FILE = '/proc/net/if_inet6';
|
||||
return fs.existsSync(IPV6_PROC_FILE);
|
||||
// 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;
|
||||
}
|
||||
|
||||
// it has to change with the adminLocation so that multiple cloudrons
|
||||
|
||||
+2
-2
@@ -17,9 +17,9 @@ exports = module.exports = {
|
||||
'admins', 'users' // ldap code uses 'users' pseudo group
|
||||
],
|
||||
|
||||
ADMIN_NAME: 'Settings',
|
||||
ADMIN_LOCATION: 'my',
|
||||
|
||||
NGINX_ADMIN_CONFIG_FILE_NAME: 'admin.conf',
|
||||
NGINX_DEFAULT_CONFIG_FILE_NAME: 'default.conf',
|
||||
|
||||
GHOST_USER_FILE: '/tmp/cloudron_ghost.json',
|
||||
|
||||
|
||||
+16
-17
@@ -22,7 +22,6 @@ 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'),
|
||||
@@ -43,12 +42,12 @@ var gJobs = {
|
||||
cleanupTokens: null,
|
||||
digestEmail: null,
|
||||
dockerVolumeCleaner: null,
|
||||
dynamicDNS: null,
|
||||
dynamicDns: null,
|
||||
schedulerSync: null,
|
||||
appHealthMonitor: null
|
||||
};
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
var AUDIT_SOURCE = { userId: null, username: 'cron' };
|
||||
|
||||
// cron format
|
||||
@@ -86,10 +85,10 @@ function startPostActivationJobs(callback) {
|
||||
start: true
|
||||
});
|
||||
|
||||
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.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.getAll(function (error, allSettings) {
|
||||
if (error) return callback(error);
|
||||
@@ -154,7 +153,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.cleanup.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
|
||||
onTick: backups.startCleanupTask.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
|
||||
start: true,
|
||||
timeZone: tz
|
||||
});
|
||||
@@ -186,7 +185,7 @@ function recreateJobs(tz) {
|
||||
if (gJobs.certificateRenew) gJobs.certificateRenew.stop();
|
||||
gJobs.certificateRenew = new CronJob({
|
||||
cronTime: '00 00 */12 * * *', // every 12 hours
|
||||
onTick: reverseProxy.renewAll.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
|
||||
onTick: cloudron.renewCerts.bind(null, {}, AUDIT_SOURCE, NOOP_CALLBACK),
|
||||
start: true,
|
||||
timeZone: tz
|
||||
});
|
||||
@@ -202,7 +201,7 @@ function recreateJobs(tz) {
|
||||
if (gJobs.appHealthMonitor) gJobs.appHealthMonitor.stop();
|
||||
gJobs.appHealthMonitor = new CronJob({
|
||||
cronTime: '*/10 * * * * *', // every 10 seconds
|
||||
onTick: appHealthMonitor.run.bind(null, 10),
|
||||
onTick: appHealthMonitor.run.bind(null, 10, NOOP_CALLBACK),
|
||||
start: true,
|
||||
timeZone: tz
|
||||
});
|
||||
@@ -267,25 +266,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) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
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);
|
||||
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);
|
||||
|
||||
for (var job in gJobs) {
|
||||
if (!gJobs[job]) continue;
|
||||
|
||||
+3
-7
@@ -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,9 +177,7 @@ function importFromFile(file, callback) {
|
||||
assert.strictEqual(typeof file, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
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}`;
|
||||
var cmd = `/usr/bin/mysql -h "${config.database().hostname}" -u ${config.database().username} -p${config.database().password} ${config.database().name} < ${file}`;
|
||||
|
||||
async.series([
|
||||
query.bind(null, 'CREATE DATABASE IF NOT EXISTS box'),
|
||||
@@ -191,9 +189,7 @@ function exportToFile(file, callback) {
|
||||
assert.strictEqual(typeof file, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
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}"`;
|
||||
var cmd = `/usr/bin/mysqldump -h "${config.database().hostname}" -u root -p${config.database().password} --single-transaction --routines --triggers ${config.database().name} > "${file}"`;
|
||||
|
||||
child_process.exec(cmd, callback);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
'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;
|
||||
+49
-44
@@ -4,35 +4,38 @@ exports = module.exports = {
|
||||
upsert: upsert,
|
||||
get: get,
|
||||
del: del,
|
||||
waitForDns: require('./waitfordns.js'),
|
||||
wait: wait,
|
||||
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');
|
||||
util = require('util'),
|
||||
waitForDns = require('./waitfordns.js');
|
||||
|
||||
function getFqdn(subdomain, domain) {
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
function getFqdn(location, domain) {
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
|
||||
return (subdomain === '') ? domain : subdomain + '-' + domain;
|
||||
return (location === '') ? domain : location + '-' + domain;
|
||||
}
|
||||
|
||||
function add(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
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');
|
||||
|
||||
var fqdn = subdomain !== '' && type === 'TXT' ? subdomain + '.' + dnsConfig.fqdn : getFqdn(subdomain, dnsConfig.fqdn);
|
||||
const dnsConfig = domainObject.config;
|
||||
|
||||
debug('add: %s for zone %s of type %s with values %j', subdomain, dnsConfig.fqdn, type, values);
|
||||
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);
|
||||
|
||||
var data = {
|
||||
type: type,
|
||||
@@ -54,16 +57,16 @@ function 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');
|
||||
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');
|
||||
|
||||
var fqdn = subdomain !== '' && type === 'TXT' ? subdomain + '.' + dnsConfig.fqdn : getFqdn(subdomain, dnsConfig.fqdn);
|
||||
const dnsConfig = domainObject.config;
|
||||
const fqdn = location !== '' && type === 'TXT' ? location + '.' + domainObject.domain : getFqdn(location, domainObject.domain);
|
||||
|
||||
debug('get: zoneName: %s subdomain: %s type: %s fqdn: %s', dnsConfig.fqdn, subdomain, type, fqdn);
|
||||
debug('get: zoneName: %s subdomain: %s type: %s fqdn: %s', domainObject.domain, location, type, fqdn);
|
||||
|
||||
superagent
|
||||
.get(config.apiServerOrigin() + '/api/v1/domains/' + fqdn)
|
||||
@@ -77,26 +80,15 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
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');
|
||||
|
||||
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);
|
||||
const dnsConfig = domainObject.config;
|
||||
debug('del: %s for zone %s of type %s with values %j', location, domainObject.domain, type, values);
|
||||
|
||||
var data = {
|
||||
type: type,
|
||||
@@ -104,7 +96,7 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
};
|
||||
|
||||
superagent
|
||||
.del(config.apiServerOrigin() + '/api/v1/domains/' + getFqdn(subdomain, dnsConfig.fqdn))
|
||||
.del(config.apiServerOrigin() + '/api/v1/domains/' + getFqdn(location, domainObject.domain))
|
||||
.query({ token: dnsConfig.token })
|
||||
.send(data)
|
||||
.timeout(30 * 1000)
|
||||
@@ -119,29 +111,42 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
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');
|
||||
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 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 testSubdomain = 'cloudrontestdns';
|
||||
const location = 'cloudrontestdns';
|
||||
|
||||
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
|
||||
upsert(domainObject, location, 'A', [ ip ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
|
||||
debug('verifyDnsConfig: Test A record added');
|
||||
|
||||
del(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error) {
|
||||
del(domainObject, location, 'A', [ ip ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record removed again');
|
||||
|
||||
+106
-62
@@ -4,7 +4,7 @@ exports = module.exports = {
|
||||
upsert: upsert,
|
||||
get: get,
|
||||
del: del,
|
||||
waitForDns: require('./waitfordns.js'),
|
||||
wait: wait,
|
||||
verifyDnsConfig: verifyDnsConfig
|
||||
};
|
||||
|
||||
@@ -12,9 +12,11 @@ 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
|
||||
@@ -53,16 +55,14 @@ function getZoneByName(dnsConfig, zoneName, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getDnsRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, callback) {
|
||||
// gets records filtered by zone, type and fqdn
|
||||
function getDnsRecords(dnsConfig, zoneId, fqdn, type, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneId, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof fqdn, '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)
|
||||
@@ -78,32 +78,30 @@ function getDnsRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, cal
|
||||
});
|
||||
}
|
||||
|
||||
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
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');
|
||||
|
||||
var fqdn = subdomain === '' ? zoneName : subdomain + '.' + zoneName;
|
||||
const dnsConfig = domainObject.config,
|
||||
zoneName = domainObject.zoneName,
|
||||
fqdn = domains.fqdn(location, domainObject);
|
||||
|
||||
debug('upsert: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
|
||||
debug('upsert: %s for zone %s of type %s with values %j', fqdn, zoneName, type, values);
|
||||
|
||||
getZoneByName(dnsConfig, zoneName, function(error, result){
|
||||
getZoneByName(dnsConfig, zoneName, function(error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var zoneId = result.id;
|
||||
let zoneId = result.id;
|
||||
|
||||
getDnsRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, function (error, result) {
|
||||
getDnsRecords(dnsConfig, zoneId, fqdn, type, function (error, dnsRecords) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var dnsRecords = result;
|
||||
let i = 0; // // used to track available records to update instead of create
|
||||
|
||||
// used to track available records to update instead of create
|
||||
var i = 0;
|
||||
|
||||
async.eachSeries(values, function (value, callback) {
|
||||
async.eachSeries(values, function (value, iteratorCallback) {
|
||||
var priority = null;
|
||||
|
||||
if (type === 'MX') {
|
||||
@@ -116,35 +114,41 @@ function upsert(dnsConfig, zoneName, subdomain, 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) {
|
||||
superagent.post(CLOUDFLARE_ENDPOINT + '/zones/'+ zoneId + '/dns_records')
|
||||
.set('X-Auth-Key',dnsConfig.token)
|
||||
.set('X-Auth-Email',dnsConfig.email)
|
||||
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)
|
||||
.send(data)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
|
||||
if (error && !error.response) return iteratorCallback(error);
|
||||
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, iteratorCallback);
|
||||
|
||||
callback(null);
|
||||
iteratorCallback(null);
|
||||
});
|
||||
} else {
|
||||
superagent.put(CLOUDFLARE_ENDPOINT + '/zones/'+ zoneId + '/dns_records/' + dnsRecords[i].id)
|
||||
.set('X-Auth-Key',dnsConfig.token)
|
||||
.set('X-Auth-Email',dnsConfig.email)
|
||||
} 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)
|
||||
.send(data)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
// increment, as we have consumed the record
|
||||
++i;
|
||||
++i; // increment, as we have consumed the record
|
||||
|
||||
if (error && !error.response) return callback(error);
|
||||
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
|
||||
if (error && !error.response) return iteratorCallback(error);
|
||||
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, iteratorCallback);
|
||||
|
||||
callback(null);
|
||||
iteratorCallback(null);
|
||||
});
|
||||
}
|
||||
}, callback);
|
||||
@@ -152,17 +156,20 @@ function upsert(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');
|
||||
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');
|
||||
|
||||
getZoneByName(dnsConfig, zoneName, function(error, result){
|
||||
const dnsConfig = domainObject.config,
|
||||
zoneName = domainObject.zoneName,
|
||||
fqdn = domains.fqdn(location, domainObject);
|
||||
|
||||
getZoneByName(dnsConfig, zoneName, function(error, zone) {
|
||||
if (error) return callback(error);
|
||||
|
||||
getDnsRecordsByZoneId(dnsConfig, result.id, zoneName, subdomain, type, function(error, result) {
|
||||
getDnsRecords(dnsConfig, zone.id, fqdn, type, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var tmp = result.map(function (record) { return record.content; });
|
||||
@@ -173,18 +180,21 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
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');
|
||||
|
||||
getZoneByName(dnsConfig, zoneName, function(error, result){
|
||||
const dnsConfig = domainObject.config,
|
||||
zoneName = domainObject.zoneName,
|
||||
fqdn = domains.fqdn(location, domainObject);
|
||||
|
||||
getZoneByName(dnsConfig, zoneName, function(error, zone) {
|
||||
if (error) return callback(error);
|
||||
|
||||
getDnsRecordsByZoneId(dnsConfig, result.id, zoneName, subdomain, type, function(error, result) {
|
||||
getDnsRecords(dnsConfig, zone.id, fqdn, type, function(error, result) {
|
||||
if (error) return callback(error);
|
||||
if (result.length === 0) return callback(null);
|
||||
|
||||
@@ -197,8 +207,8 @@ function del(dnsConfig, zoneName, subdomain, 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);
|
||||
@@ -217,16 +227,50 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
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');
|
||||
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 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';
|
||||
|
||||
var credentials = {
|
||||
token: dnsConfig.token,
|
||||
email: dnsConfig.email
|
||||
@@ -238,22 +282,22 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, 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, result) {
|
||||
getZoneByName(dnsConfig, zoneName, function(error, zone) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (!_.isEqual(result.name_servers.sort(), nameservers.sort())) {
|
||||
debug('verifyDnsConfig: %j and %j do not match', nameservers, result.name_servers);
|
||||
if (!_.isEqual(zone.name_servers.sort(), nameservers.sort())) {
|
||||
debug('verifyDnsConfig: %j and %j do not match', nameservers, zone.name_servers);
|
||||
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Cloudflare'));
|
||||
}
|
||||
|
||||
const testSubdomain = 'cloudrontestdns';
|
||||
const location = 'cloudrontestdns';
|
||||
|
||||
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
|
||||
upsert(domainObject, location, 'A', [ ip ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
|
||||
debug('verifyDnsConfig: Test A record added');
|
||||
|
||||
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
|
||||
del(domainObject, location, 'A', [ ip ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record removed again');
|
||||
|
||||
+54
-34
@@ -4,7 +4,7 @@ exports = module.exports = {
|
||||
upsert: upsert,
|
||||
get: get,
|
||||
del: del,
|
||||
waitForDns: require('./waitfordns.js'),
|
||||
wait: wait,
|
||||
verifyDnsConfig: verifyDnsConfig
|
||||
};
|
||||
|
||||
@@ -12,10 +12,12 @@ 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');
|
||||
util = require('util'),
|
||||
waitForDns = require('./waitfordns.js');
|
||||
|
||||
var DIGITALOCEAN_ENDPOINT = 'https://api.digitalocean.com';
|
||||
|
||||
@@ -23,10 +25,10 @@ function formatError(response) {
|
||||
return util.format('DigitalOcean DNS error [%s] %j', response.statusCode, response.body);
|
||||
}
|
||||
|
||||
function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
function getInternal(dnsConfig, zoneName, name, type, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -45,7 +47,7 @@ function getInternal(dnsConfig, zoneName, subdomain, 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 === subdomain);
|
||||
return (record.type === type && record.name === name);
|
||||
}));
|
||||
|
||||
nextPage = (result.body.links && result.body.links.pages) ? result.body.links.pages.next : null;
|
||||
@@ -61,19 +63,20 @@ function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
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');
|
||||
|
||||
subdomain = subdomain || '@';
|
||||
const dnsConfig = domainObject.config,
|
||||
zoneName = domainObject.zoneName,
|
||||
name = domains.getName(domainObject, location, type) || '@';
|
||||
|
||||
debug('upsert: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
|
||||
debug('upsert: %s for zone %s of type %s with values %j', name, zoneName, type, values);
|
||||
|
||||
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
|
||||
getInternal(dnsConfig, zoneName, name, type, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
// used to track available records to update instead of create
|
||||
@@ -89,7 +92,7 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
|
||||
var data = {
|
||||
type: type,
|
||||
name: subdomain,
|
||||
name: name,
|
||||
data: value,
|
||||
priority: priority,
|
||||
ttl: 1
|
||||
@@ -133,16 +136,17 @@ function upsert(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');
|
||||
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');
|
||||
|
||||
subdomain = subdomain || '@';
|
||||
const dnsConfig = domainObject.config,
|
||||
zoneName = domainObject.zoneName,
|
||||
name = domains.getName(domainObject, location, type) || '@';
|
||||
|
||||
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
|
||||
getInternal(dnsConfig, zoneName, name, type, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
// We only return the value string
|
||||
@@ -154,17 +158,18 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
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');
|
||||
|
||||
subdomain = subdomain || '@';
|
||||
const dnsConfig = domainObject.config,
|
||||
zoneName = domainObject.zoneName,
|
||||
name = domains.getName(domainObject, location, type) || '@';
|
||||
|
||||
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
|
||||
getInternal(dnsConfig, zoneName, name, type, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (result.length === 0) return callback(null);
|
||||
@@ -193,15 +198,30 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
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');
|
||||
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 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';
|
||||
|
||||
var credentials = {
|
||||
token: dnsConfig.token
|
||||
};
|
||||
@@ -217,14 +237,14 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Digital Ocean'));
|
||||
}
|
||||
|
||||
const testSubdomain = 'cloudrontestdns';
|
||||
const location = 'cloudrontestdns';
|
||||
|
||||
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
|
||||
upsert(domainObject, location, 'A', [ ip ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
|
||||
debug('verifyDnsConfig: Test A record added');
|
||||
|
||||
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
|
||||
del(domainObject, location, 'A', [ ip ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record removed again');
|
||||
|
||||
+52
-32
@@ -4,16 +4,18 @@ exports = module.exports = {
|
||||
upsert: upsert,
|
||||
get: get,
|
||||
del: del,
|
||||
waitForDns: require('./waitfordns.js'),
|
||||
wait: wait,
|
||||
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');
|
||||
util = require('util'),
|
||||
waitForDns = require('./waitfordns.js');
|
||||
|
||||
var GANDI_API = 'https://dns.api.gandi.net/api/v5';
|
||||
|
||||
@@ -21,24 +23,25 @@ function formatError(response) {
|
||||
return util.format(`Gandi DNS error [${response.statusCode}] ${response.body.message}`);
|
||||
}
|
||||
|
||||
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
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');
|
||||
|
||||
subdomain = subdomain || '@';
|
||||
const dnsConfig = domainObject.config,
|
||||
zoneName = domainObject.zoneName,
|
||||
name = domains.getName(domainObject, location, type) || '@';
|
||||
|
||||
debug(`upsert: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
||||
debug(`upsert: ${name} 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/${subdomain}/${type}`)
|
||||
superagent.put(`${GANDI_API}/domains/${zoneName}/records/${name}/${type}`)
|
||||
.set('X-Api-Key', dnsConfig.token)
|
||||
.timeout(30 * 1000)
|
||||
.send(data)
|
||||
@@ -52,18 +55,19 @@ function upsert(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');
|
||||
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');
|
||||
|
||||
subdomain = subdomain || '@';
|
||||
const dnsConfig = domainObject.config,
|
||||
zoneName = domainObject.zoneName,
|
||||
name = domains.getName(domainObject, location, type) || '@';
|
||||
|
||||
debug(`get: ${subdomain} in zone ${zoneName} of type ${type}`);
|
||||
debug(`get: ${name} in zone ${zoneName} of type ${type}`);
|
||||
|
||||
superagent.get(`${GANDI_API}/domains/${zoneName}/records/${subdomain}/${type}`)
|
||||
superagent.get(`${GANDI_API}/domains/${zoneName}/records/${name}/${type}`)
|
||||
.set('X-Api-Key', dnsConfig.token)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
@@ -78,19 +82,20 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
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');
|
||||
|
||||
subdomain = subdomain || '@';
|
||||
const dnsConfig = domainObject.config,
|
||||
zoneName = domainObject.zoneName,
|
||||
name = domains.getName(domainObject, location, type) || '@';
|
||||
|
||||
debug(`del: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
||||
debug(`del: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
||||
|
||||
superagent.del(`${GANDI_API}/domains/${zoneName}/records/${subdomain}/${type}`)
|
||||
superagent.del(`${GANDI_API}/domains/${zoneName}/records/${name}/${type}`)
|
||||
.set('X-Api-Key', dnsConfig.token)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
@@ -105,19 +110,34 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
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');
|
||||
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 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'));
|
||||
|
||||
var credentials = {
|
||||
token: dnsConfig.token
|
||||
};
|
||||
|
||||
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) {
|
||||
@@ -129,14 +149,14 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Gandi'));
|
||||
}
|
||||
|
||||
const testSubdomain = 'cloudrontestdns';
|
||||
const location = 'cloudrontestdns';
|
||||
|
||||
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
|
||||
upsert(domainObject, location, 'A', [ ip ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
|
||||
debug('verifyDnsConfig: Test A record added');
|
||||
|
||||
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
|
||||
del(domainObject, location, 'A', [ ip ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record removed again');
|
||||
|
||||
+55
-32
@@ -4,16 +4,18 @@ exports = module.exports = {
|
||||
upsert: upsert,
|
||||
get: get,
|
||||
del: del,
|
||||
waitForDns: require('./waitfordns.js'),
|
||||
wait: wait,
|
||||
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 getDnsCredentials(dnsConfig) {
|
||||
@@ -55,22 +57,23 @@ function getZoneByName(dnsConfig, zoneName, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
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('add: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
|
||||
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);
|
||||
|
||||
getZoneByName(getDnsCredentials(dnsConfig), zoneName, function (error, zone) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var domain = (subdomain ? subdomain + '.' : '') + zoneName + '.';
|
||||
|
||||
zone.getRecords({ type: type, name: domain }, function (error, oldRecords) {
|
||||
zone.getRecords({ type: type, name: fqdn + '.' }, function (error, oldRecords) {
|
||||
if (error && error.code === 403) return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
|
||||
if (error) {
|
||||
debug('upsert->zone.getRecords', error);
|
||||
@@ -78,12 +81,12 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
}
|
||||
|
||||
var newRecord = zone.record(type, {
|
||||
name: domain,
|
||||
name: fqdn + '.',
|
||||
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) {
|
||||
@@ -97,18 +100,21 @@ function upsert(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');
|
||||
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');
|
||||
|
||||
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: (subdomain ? subdomain + '.' : '') + zoneName + '.',
|
||||
name: fqdn + '.',
|
||||
type: type
|
||||
};
|
||||
|
||||
@@ -122,20 +128,21 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
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');
|
||||
|
||||
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 domain = (subdomain ? subdomain + '.' : '') + zoneName + '.';
|
||||
|
||||
zone.getRecords({ type: type, name: domain }, function(error, oldRecords) {
|
||||
zone.getRecords({ type: type, name: fqdn + '.' }, function(error, oldRecords) {
|
||||
if (error && error.code === 403) return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
|
||||
if (error) {
|
||||
debug('del->zone.getRecords', error);
|
||||
@@ -156,19 +163,35 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
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');
|
||||
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 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'));
|
||||
|
||||
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) {
|
||||
@@ -184,14 +207,14 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Google Cloud DNS'));
|
||||
}
|
||||
|
||||
const testSubdomain = 'cloudrontestdns';
|
||||
const location = 'cloudrontestdns';
|
||||
|
||||
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
|
||||
upsert(domainObject, location, 'A', [ ip ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
|
||||
debug('verifyDnsConfig: Test A record added');
|
||||
|
||||
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
|
||||
del(domainObject, location, 'A', [ ip ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record removed again');
|
||||
|
||||
+53
-33
@@ -4,16 +4,18 @@ exports = module.exports = {
|
||||
upsert: upsert,
|
||||
get: get,
|
||||
del: del,
|
||||
waitForDns: require('./waitfordns.js'),
|
||||
wait: wait,
|
||||
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');
|
||||
util = require('util'),
|
||||
waitForDns = require('./waitfordns.js');
|
||||
|
||||
// const GODADDY_API_OTE = 'https://api.ote-godaddy.com/v1/domains';
|
||||
const GODADDY_API = 'https://api.godaddy.com/v1/domains';
|
||||
@@ -27,17 +29,18 @@ function formatError(response) {
|
||||
return util.format(`GoDaddy DNS error [${response.statusCode}] ${response.body.message}`);
|
||||
}
|
||||
|
||||
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
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');
|
||||
|
||||
subdomain = subdomain || '@';
|
||||
const dnsConfig = domainObject.config,
|
||||
zoneName = domainObject.zoneName,
|
||||
name = domains.getName(domainObject, location, type) || '@';
|
||||
|
||||
debug(`upsert: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
||||
debug(`upsert: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
||||
|
||||
var records = [ ];
|
||||
values.forEach(function (value) {
|
||||
@@ -53,7 +56,7 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
records.push(record);
|
||||
});
|
||||
|
||||
superagent.put(`${GODADDY_API}/${zoneName}/records/${type}/${subdomain}`)
|
||||
superagent.put(`${GODADDY_API}/${zoneName}/records/${type}/${name}`)
|
||||
.set('Authorization', `sso-key ${dnsConfig.apiKey}:${dnsConfig.apiSecret}`)
|
||||
.timeout(30 * 1000)
|
||||
.send(records)
|
||||
@@ -68,18 +71,19 @@ function upsert(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');
|
||||
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');
|
||||
|
||||
subdomain = subdomain || '@';
|
||||
const dnsConfig = domainObject.config,
|
||||
zoneName = domainObject.zoneName,
|
||||
name = domains.getName(domainObject, location, type) || '@';
|
||||
|
||||
debug(`get: ${subdomain} in zone ${zoneName} of type ${type}`);
|
||||
debug(`get: ${name} in zone ${zoneName} of type ${type}`);
|
||||
|
||||
superagent.get(`${GODADDY_API}/${zoneName}/records/${type}/${subdomain}`)
|
||||
superagent.get(`${GODADDY_API}/${zoneName}/records/${type}/${name}`)
|
||||
.set('Authorization', `sso-key ${dnsConfig.apiKey}:${dnsConfig.apiSecret}`)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
@@ -98,22 +102,23 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
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');
|
||||
|
||||
subdomain = subdomain || '@';
|
||||
const dnsConfig = domainObject.config,
|
||||
zoneName = domainObject.zoneName,
|
||||
name = domains.getName(domainObject, location, type) || '@';
|
||||
|
||||
debug(`get: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
||||
debug(`get: ${name} 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')));
|
||||
|
||||
// check if the record exists at all so that we don't insert the "Dead" record for no reason
|
||||
get(dnsConfig, zoneName, subdomain, type, function (error, values) {
|
||||
get(domainObject, location, type, function (error, values) {
|
||||
if (error) return callback(error);
|
||||
if (values.length === 0) return callback();
|
||||
|
||||
@@ -123,7 +128,7 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
data: type === 'A' ? GODADDY_INVALID_IP : GODADDY_INVALID_TXT
|
||||
}];
|
||||
|
||||
superagent.put(`${GODADDY_API}/${zoneName}/records/${type}/${subdomain}`)
|
||||
superagent.put(`${GODADDY_API}/${zoneName}/records/${type}/${name}`)
|
||||
.set('Authorization', `sso-key ${dnsConfig.apiKey}:${dnsConfig.apiSecret}`)
|
||||
.send(records)
|
||||
.timeout(30 * 1000)
|
||||
@@ -140,16 +145,31 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
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');
|
||||
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 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';
|
||||
|
||||
var credentials = {
|
||||
apiKey: dnsConfig.apiKey,
|
||||
apiSecret: dnsConfig.apiSecret
|
||||
@@ -166,14 +186,14 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to GoDaddy'));
|
||||
}
|
||||
|
||||
const testSubdomain = 'cloudrontestdns';
|
||||
const location = 'cloudrontestdns';
|
||||
|
||||
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
|
||||
upsert(domainObject, location, 'A', [ ip ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
|
||||
debug('verifyDnsConfig: Test A record added');
|
||||
|
||||
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
|
||||
del(domainObject, location, 'A', [ ip ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record removed again');
|
||||
|
||||
+23
-19
@@ -10,18 +10,16 @@ exports = module.exports = {
|
||||
upsert: upsert,
|
||||
get: get,
|
||||
del: del,
|
||||
waitForDns: require('./waitfordns.js'),
|
||||
wait: wait,
|
||||
verifyDnsConfig: verifyDnsConfig
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
DomainsError = require('../domains.js').DomainsError,
|
||||
util = require('util');
|
||||
|
||||
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
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');
|
||||
@@ -31,10 +29,9 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
callback(new Error('not implemented'));
|
||||
}
|
||||
|
||||
function get(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
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');
|
||||
|
||||
@@ -43,10 +40,9 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
callback(new Error('not implemented'));
|
||||
}
|
||||
|
||||
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
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');
|
||||
@@ -56,11 +52,19 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
callback(new Error('not implemented'));
|
||||
}
|
||||
|
||||
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');
|
||||
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');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// Result: dnsConfig object
|
||||
|
||||
+31
-20
@@ -4,43 +4,42 @@ exports = module.exports = {
|
||||
upsert: upsert,
|
||||
get: get,
|
||||
del: del,
|
||||
waitForDns: require('./waitfordns.js'),
|
||||
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,
|
||||
util = require('util');
|
||||
util = require('util'),
|
||||
waitForDns = require('./waitfordns.js');
|
||||
|
||||
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
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', subdomain, zoneName, type, values);
|
||||
debug('upsert: %s for zone %s of type %s with values %j', location, domainObject.zoneName, type, values);
|
||||
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
function get(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
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(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
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');
|
||||
@@ -48,13 +47,25 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
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');
|
||||
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'));
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
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');
|
||||
|
||||
var namecheap;
|
||||
|
||||
function formatError(response) {
|
||||
return util.format('NameCheap DNS error [%s] %j', response.code, response.message);
|
||||
}
|
||||
|
||||
// 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 init(dnsConfig, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (namecheap) return callback();
|
||||
|
||||
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
|
||||
namecheap = new Namecheap(dnsConfig.username, dnsConfig.apiKey, ip);
|
||||
namecheap.setUsername(dnsConfig.username);
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
init(dnsConfig, function (error) {
|
||||
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(zoneName, hosts, callback) {
|
||||
let mappedHosts = mapHosts(hosts);
|
||||
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(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(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.apiKey || typeof dnsConfig.apiKey !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'apiKey must be a non-empty string'));
|
||||
|
||||
var credentials = {
|
||||
username: dnsConfig.username,
|
||||
apKey: dnsConfig.apiKey
|
||||
};
|
||||
|
||||
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, dnsConfig);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
+70
-49
@@ -4,16 +4,19 @@ exports = module.exports = {
|
||||
upsert: upsert,
|
||||
get: get,
|
||||
del: del,
|
||||
waitForDns: require('./waitfordns.js'),
|
||||
wait: wait,
|
||||
verifyDnsConfig: verifyDnsConfig
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
debug = require('debug')('box:dns/namecom'),
|
||||
dns = require('../native-dns.js'),
|
||||
safe = require('safetydance'),
|
||||
domains = require('../domains.js'),
|
||||
DomainsError = require('../domains.js').DomainsError,
|
||||
superagent = require('superagent');
|
||||
safe = require('safetydance'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util'),
|
||||
waitForDns = require('./waitfordns.js');
|
||||
|
||||
const NAMECOM_API = 'https://api.name.com/v4';
|
||||
|
||||
@@ -21,18 +24,18 @@ function formatError(response) {
|
||||
return `Name.com DNS error [${response.statusCode}] ${response.text}`;
|
||||
}
|
||||
|
||||
function addRecord(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
function addRecord(dnsConfig, zoneName, name, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(Array.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`add: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
||||
debug(`add: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
||||
|
||||
var data = {
|
||||
host: subdomain,
|
||||
host: name,
|
||||
type: type,
|
||||
ttl: 300 // 300 is the lowest
|
||||
};
|
||||
@@ -57,19 +60,19 @@ function addRecord(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function updateRecord(dnsConfig, zoneName, recordId, subdomain, type, values, callback) {
|
||||
function updateRecord(dnsConfig, zoneName, recordId, name, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof recordId, 'number');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(Array.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`update:${recordId} on ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
||||
debug(`update:${recordId} on ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
||||
|
||||
var data = {
|
||||
host: subdomain,
|
||||
host: name,
|
||||
type: type,
|
||||
ttl: 300 // 300 is the lowest
|
||||
};
|
||||
@@ -94,16 +97,14 @@ function updateRecord(dnsConfig, zoneName, recordId, subdomain, type, values, ca
|
||||
});
|
||||
}
|
||||
|
||||
function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
function getInternal(dnsConfig, zoneName, name, type, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
subdomain = subdomain || '@';
|
||||
|
||||
debug(`getInternal: ${subdomain} in zone ${zoneName} of type ${type}`);
|
||||
debug(`getInternal: ${name} in zone ${zoneName} of type ${type}`);
|
||||
|
||||
superagent.get(`${NAMECOM_API}/domains/${zoneName}/records`)
|
||||
.auth(dnsConfig.username, dnsConfig.token)
|
||||
@@ -123,7 +124,7 @@ function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
});
|
||||
|
||||
var results = result.body.records.filter(function (r) {
|
||||
return (r.host === subdomain && r.type === type);
|
||||
return (r.host === name && r.type === type);
|
||||
});
|
||||
|
||||
debug('getInternal: %j', results);
|
||||
@@ -132,35 +133,39 @@ function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
function upsert(domainObject, location, type, values, callback) {
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(Array.isArray(values));
|
||||
assert(util.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
subdomain = subdomain || '@';
|
||||
const dnsConfig = domainObject.config,
|
||||
zoneName = domainObject.zoneName,
|
||||
name = domains.getName(domainObject, location, type) || '@';
|
||||
|
||||
debug(`upsert: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
||||
debug(`upsert: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
||||
|
||||
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
|
||||
getInternal(dnsConfig, zoneName, name, type, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (result.length === 0) return addRecord(dnsConfig, zoneName, subdomain, type, values, callback);
|
||||
if (result.length === 0) return addRecord(dnsConfig, zoneName, name, type, values, callback);
|
||||
|
||||
return updateRecord(dnsConfig, zoneName, result[0].id, subdomain, type, values, callback);
|
||||
return updateRecord(dnsConfig, zoneName, result[0].id, name, 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');
|
||||
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');
|
||||
|
||||
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
|
||||
const dnsConfig = domainObject.config,
|
||||
zoneName = domainObject.zoneName,
|
||||
name = domains.getName(domainObject, location, type) || '@';
|
||||
|
||||
getInternal(dnsConfig, zoneName, name, type, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var tmp = result.map(function (record) { return record.answer; });
|
||||
@@ -171,19 +176,20 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
function del(domainObject, location, type, values, callback) {
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(Array.isArray(values));
|
||||
assert(util.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
subdomain = subdomain || '@';
|
||||
const dnsConfig = domainObject.config,
|
||||
zoneName = domainObject.zoneName,
|
||||
name = domains.getName(domainObject, location, type) || '@';
|
||||
|
||||
debug(`del: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
||||
debug(`del: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
||||
|
||||
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
|
||||
getInternal(dnsConfig, zoneName, name, type, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (result.length === 0) return callback();
|
||||
@@ -201,13 +207,26 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
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');
|
||||
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 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'));
|
||||
|
||||
@@ -216,6 +235,8 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
token: dnsConfig.token
|
||||
};
|
||||
|
||||
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) {
|
||||
@@ -227,14 +248,14 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Name.com'));
|
||||
}
|
||||
|
||||
const testSubdomain = 'cloudrontestdns';
|
||||
const location = 'cloudrontestdns';
|
||||
|
||||
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
|
||||
upsert(domainObject, location, 'A', [ ip ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
|
||||
debug('verifyDnsConfig: Test A record added');
|
||||
|
||||
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
|
||||
del(domainObject, location, 'A', [ ip ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record removed again');
|
||||
|
||||
+16
-22
@@ -4,7 +4,7 @@ exports = module.exports = {
|
||||
upsert: upsert,
|
||||
get: get,
|
||||
del: del,
|
||||
waitForDns: waitForDns,
|
||||
wait: wait,
|
||||
verifyDnsConfig: verifyDnsConfig
|
||||
};
|
||||
|
||||
@@ -12,33 +12,30 @@ var assert = require('assert'),
|
||||
debug = require('debug')('box:dns/noop'),
|
||||
util = require('util');
|
||||
|
||||
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
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', subdomain, zoneName, type, values);
|
||||
debug('upsert: %s for zone %s of type %s with values %j', location, domainObject.zoneName, type, values);
|
||||
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
function get(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
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(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
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');
|
||||
@@ -46,9 +43,9 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
function waitForDns(domain, zoneName, type, value, options, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
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 }
|
||||
@@ -57,11 +54,8 @@ function waitForDns(domain, zoneName, type, value, options, callback) {
|
||||
callback();
|
||||
}
|
||||
|
||||
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');
|
||||
function verifyDnsConfig(domainObject, callback) {
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
return callback(null, { });
|
||||
|
||||
+51
-41
@@ -4,19 +4,18 @@ exports = module.exports = {
|
||||
upsert: upsert,
|
||||
get: get,
|
||||
del: del,
|
||||
waitForDns: require('./waitfordns.js'),
|
||||
verifyDnsConfig: verifyDnsConfig,
|
||||
|
||||
// not part of "dns" interface
|
||||
getHostedZone: getHostedZone
|
||||
wait: wait,
|
||||
verifyDnsConfig: verifyDnsConfig
|
||||
};
|
||||
|
||||
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 getDnsCredentials(dnsConfig) {
|
||||
@@ -82,20 +81,22 @@ function getHostedZone(dnsConfig, zoneName, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function add(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
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('add: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
|
||||
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);
|
||||
|
||||
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 = {
|
||||
@@ -126,31 +127,23 @@ function add(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
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');
|
||||
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');
|
||||
|
||||
const dnsConfig = domainObject.config,
|
||||
zoneName = domainObject.zoneName,
|
||||
fqdn = domains.fqdn(location, domainObject);
|
||||
|
||||
getZoneByName(dnsConfig, zoneName, function (error, zone) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var params = {
|
||||
HostedZoneId: zone.Id,
|
||||
MaxItems: '1',
|
||||
StartRecordName: (subdomain ? subdomain + '.' : '') + zoneName + '.',
|
||||
StartRecordName: fqdn + '.',
|
||||
StartRecordType: type
|
||||
};
|
||||
|
||||
@@ -169,18 +162,20 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
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');
|
||||
|
||||
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 = {
|
||||
@@ -226,13 +221,26 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
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');
|
||||
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 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'));
|
||||
|
||||
@@ -244,6 +252,8 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
listHostedZonesByName: true, // new/updated creds require this perm
|
||||
};
|
||||
|
||||
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) {
|
||||
@@ -258,14 +268,14 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Route53'));
|
||||
}
|
||||
|
||||
const testSubdomain = 'cloudrontestdns';
|
||||
const location = 'cloudrontestdns';
|
||||
|
||||
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
|
||||
upsert(domainObject, location, 'A', [ ip ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
|
||||
debug('verifyDnsConfig: Test A record added');
|
||||
|
||||
del(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error) {
|
||||
del(domainObject, location, 'A', [ ip ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record removed again');
|
||||
|
||||
+13
-13
@@ -30,8 +30,8 @@ function resolveIp(hostname, options, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function isChangeSynced(domain, type, value, nameserver, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
function isChangeSynced(hostname, type, value, nameserver, callback) {
|
||||
assert.strictEqual(typeof hostname, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof value, 'string');
|
||||
assert.strictEqual(typeof nameserver, 'string');
|
||||
@@ -46,16 +46,16 @@ function isChangeSynced(domain, type, value, nameserver, callback) {
|
||||
|
||||
async.every(nsIps, function (nsIp, iteratorCallback) {
|
||||
const resolveOptions = { server: nsIp, timeout: 5000 };
|
||||
const resolver = type === 'A' ? resolveIp.bind(null, domain) : dns.resolve.bind(null, domain, 'TXT');
|
||||
const resolver = type === 'A' ? resolveIp.bind(null, hostname) : dns.resolve.bind(null, hostname, 'TXT');
|
||||
|
||||
resolver(resolveOptions, function (error, answer) {
|
||||
if (error && error.code === 'TIMEOUT') {
|
||||
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) timed out when resolving ${domain} (${type})`);
|
||||
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) timed out when resolving ${hostname} (${type})`);
|
||||
return iteratorCallback(null, true); // should be ok if dns server is down
|
||||
}
|
||||
|
||||
if (error) {
|
||||
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) errored when resolve ${domain} (${type}): ${error}`);
|
||||
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) errored when resolve ${hostname} (${type}): ${error}`);
|
||||
return iteratorCallback(null, false);
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ function isChangeSynced(domain, type, value, nameserver, callback) {
|
||||
match = answer.some(function (a) { return value === a.join(''); });
|
||||
}
|
||||
|
||||
debug(`isChangeSynced: ${domain} (${type}) was resolved to ${answer} at NS ${nameserver} (${nsIp}). Expecting ${value}. Match ${match}`);
|
||||
debug(`isChangeSynced: ${hostname} (${type}) was resolved to ${answer} at NS ${nameserver} (${nsIp}). Expecting ${value}. Match ${match}`);
|
||||
|
||||
iteratorCallback(null, match);
|
||||
});
|
||||
@@ -76,26 +76,26 @@ function isChangeSynced(domain, type, value, nameserver, callback) {
|
||||
}
|
||||
|
||||
// check if IP change has propagated to every nameserver
|
||||
function waitForDns(domain, zoneName, type, value, options, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
function waitForDns(hostname, zoneName, type, value, options, callback) {
|
||||
assert.strictEqual(typeof hostname, '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: domain %s to be %s in zone %s.', domain, value, zoneName);
|
||||
debug('waitForDns: hostname %s to be %s in zone %s.', hostname, value, zoneName);
|
||||
|
||||
var attempt = 0;
|
||||
async.retry(options, function (retryCallback) {
|
||||
++attempt;
|
||||
debug(`waitForDns (try ${attempt}): ${domain} to be ${value} in zone ${zoneName}`);
|
||||
debug(`waitForDns (try ${attempt}): ${hostname} 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, domain, type, value), function (error, synced) {
|
||||
debug('waitForDns: %s %s ns: %j', domain, synced ? 'done' : 'not done', 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);
|
||||
|
||||
retryCallback(synced ? null : new DomainsError(DomainsError.EXTERNAL_ERROR, 'ETRYAGAIN'));
|
||||
});
|
||||
@@ -103,7 +103,7 @@ function waitForDns(domain, zoneName, type, value, options, callback) {
|
||||
}, function retryDone(error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug(`waitForDns: ${domain} has propagated`);
|
||||
debug(`waitForDns: ${hostname} has propagated`);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
|
||||
+34
-22
@@ -4,44 +4,43 @@ exports = module.exports = {
|
||||
upsert: upsert,
|
||||
get: get,
|
||||
del: del,
|
||||
waitForDns: require('./waitfordns.js'),
|
||||
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');
|
||||
util = require('util'),
|
||||
waitForDns = require('./waitfordns.js');
|
||||
|
||||
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
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', subdomain, zoneName, type, values);
|
||||
debug('upsert: %s for zone %s of type %s with values %j', location, domainObject.zoneName, type, values);
|
||||
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
function get(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
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(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
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');
|
||||
@@ -49,20 +48,33 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
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');
|
||||
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 separator = dnsConfig.hyphenatedSubdomains ? '-' : '.';
|
||||
const fqdn = `cloudrontest${separator}${domain}`;
|
||||
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}`));
|
||||
|
||||
+74
-31
@@ -1,9 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
DockerError: DockerError,
|
||||
|
||||
connection: connectionInstance(),
|
||||
setRegistryConfig: setRegistryConfig,
|
||||
|
||||
ping: ping,
|
||||
|
||||
downloadImage: downloadImage,
|
||||
createContainer: createContainer,
|
||||
startContainer: startContainer,
|
||||
@@ -18,24 +22,26 @@ exports = module.exports = {
|
||||
getContainerIdByIp: getContainerIdByIp,
|
||||
inspect: inspect,
|
||||
inspectByName: inspect,
|
||||
memoryUsage: memoryUsage,
|
||||
execContainer: execContainer,
|
||||
createVolume: createVolume,
|
||||
removeVolume: removeVolume,
|
||||
clearVolume: clearVolume
|
||||
};
|
||||
|
||||
function connectionInstance() {
|
||||
// timeout is optional
|
||||
function connectionInstance(timeout) {
|
||||
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 });
|
||||
docker = new Docker({ host: 'http://localhost', port: 5687, timeout: timeout });
|
||||
|
||||
// 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' });
|
||||
docker = new Docker({ socketPath: '/var/run/docker.sock', timeout: timeout });
|
||||
}
|
||||
|
||||
return docker;
|
||||
@@ -48,17 +54,15 @@ var addons = require('./addons.js'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
debug = require('debug')('box:docker.js'),
|
||||
mkdirp = require('mkdirp'),
|
||||
once = require('once'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
shell = require('./shell.js'),
|
||||
spawn = child_process.spawn,
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
const RMVOLUME_CMD = path.join(__dirname, 'scripts/rmvolume.sh');
|
||||
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');
|
||||
@@ -80,6 +84,7 @@ function DockerError(reason, errorOrMessage) {
|
||||
}
|
||||
util.inherits(DockerError, Error);
|
||||
DockerError.INTERNAL_ERROR = 'Internal Error';
|
||||
DockerError.NOT_FOUND = 'Not found';
|
||||
DockerError.BAD_FIELD = 'Bad field';
|
||||
|
||||
function debugApp(app, args) {
|
||||
@@ -104,12 +109,26 @@ function setRegistryConfig(auth, 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.exec('pullImage', '/usr/bin/docker', [ 'pull', manifest.dockerImage ], { }, function (error) {
|
||||
shell.spawn('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'));
|
||||
@@ -161,8 +180,10 @@ 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,
|
||||
@@ -204,9 +225,6 @@ 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));
|
||||
|
||||
@@ -230,7 +248,8 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
Labels: {
|
||||
'fqdn': app.fqdn,
|
||||
'appId': app.id,
|
||||
'isSubcontainer': String(!isAppContainer)
|
||||
'isSubcontainer': String(!isAppContainer),
|
||||
'isCloudronManaged': String(true)
|
||||
},
|
||||
HostConfig: {
|
||||
Mounts: addons.getMountsSync(app, app.manifest.addons),
|
||||
@@ -256,7 +275,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
NetworkMode: 'cloudron',
|
||||
Dns: ['172.18.0.1'], // use internal dns
|
||||
DnsSearch: ['.'], // use internal dns
|
||||
SecurityOpt: enableSecurityOpt ? [ 'apparmor=docker-cloudron-app' ] : null // profile available only on cloudron
|
||||
SecurityOpt: [ 'apparmor=docker-cloudron-app' ]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -352,15 +371,19 @@ function deleteContainer(containerId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function deleteContainers(appId, callback) {
|
||||
function deleteContainers(appId, options, 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);
|
||||
|
||||
docker.listContainers({ all: 1, filters: JSON.stringify({ label: [ 'appId=' + appId ] }) }, function (error, containers) {
|
||||
let labels = [ 'appId=' + appId ];
|
||||
if (options.managedOnly) labels.push('isCloudronManaged=true');
|
||||
|
||||
docker.listContainers({ all: 1, filters: JSON.stringify({ label: labels }) }, function (error, containers) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.eachSeries(containers, function (container, iteratorDone) {
|
||||
@@ -444,7 +467,23 @@ function inspect(containerId, callback) {
|
||||
var container = exports.connection.getContainer(containerId);
|
||||
|
||||
container.inspect(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
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));
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
@@ -480,16 +519,14 @@ function execContainer(containerId, cmd, options, callback) {
|
||||
if (options.stdin) options.stdin.pipe(cp.stdin).on('error', callback);
|
||||
}
|
||||
|
||||
function createVolume(app, name, subdir, callback) {
|
||||
function createVolume(app, name, volumeDataDir, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof subdir, 'string');
|
||||
assert.strictEqual(typeof volumeDataDir, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
let docker = exports.connection;
|
||||
|
||||
const volumeDataDir = path.join(paths.APPS_DATA_DIR, app.id, subdir);
|
||||
|
||||
const volumeOptions = {
|
||||
Name: name,
|
||||
Driver: 'local',
|
||||
@@ -504,7 +541,8 @@ function createVolume(app, name, subdir, callback) {
|
||||
},
|
||||
};
|
||||
|
||||
mkdirp(volumeDataDir, function (error) {
|
||||
// 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) {
|
||||
@@ -515,30 +553,35 @@ function createVolume(app, name, subdir, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function clearVolume(app, name, subdir, callback) {
|
||||
function clearVolume(app, name, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof subdir, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
shell.sudo('removeVolume', [ RMVOLUME_CMD, app.id, subdir ], callback);
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
function removeVolume(app, name, subdir, 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 subdir, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
let docker = exports.connection;
|
||||
|
||||
let volume = docker.getVolume(name);
|
||||
volume.remove(function (error) {
|
||||
if (error && error.statusCode !== 404) {
|
||||
debug(`removeVolume: Error removing volume of ${app.id} ${error}`);
|
||||
callback(error);
|
||||
}
|
||||
if (error && error.statusCode !== 404) return callback(new Error(`removeVolume: Error removing volume of ${app.id} ${error.message}`));
|
||||
|
||||
shell.sudo('removeVolume', [ RMVOLUME_CMD, app.id, subdir ], callback);
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
+2
-2
@@ -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 })); // 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, isCloudronManaged: String(false) })); // 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) {
|
||||
console.log('Proxying: ' + req.method, req.url);
|
||||
debug('proxying: ' + req.method, req.url);
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
+1
-2
@@ -8,8 +8,7 @@ exports = module.exports = {
|
||||
getAll: getAll,
|
||||
update: update,
|
||||
del: del,
|
||||
|
||||
_clear: clear
|
||||
clear: clear
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
|
||||
+109
-115
@@ -6,12 +6,11 @@ module.exports = exports = {
|
||||
getAll: getAll,
|
||||
update: update,
|
||||
del: del,
|
||||
clear: clear,
|
||||
isLocked: isLocked,
|
||||
|
||||
renewCerts: renewCerts,
|
||||
|
||||
fqdn: fqdn,
|
||||
setAdmin: setAdmin,
|
||||
getName: getName,
|
||||
|
||||
getDnsRecords: getDnsRecords,
|
||||
upsertDnsRecords: upsertDnsRecords,
|
||||
@@ -28,32 +27,27 @@ module.exports = exports = {
|
||||
|
||||
parentDomain: parentDomain,
|
||||
|
||||
DomainsError: DomainsError,
|
||||
prepareDashboardDomain: prepareDashboardDomain,
|
||||
|
||||
// exported for testing
|
||||
_getName: getName
|
||||
DomainsError: DomainsError
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
caas = require('./caas.js'),
|
||||
async = require('async'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:domains'),
|
||||
domaindb = require('./domaindb.js'),
|
||||
path = require('path'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
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');
|
||||
@@ -97,6 +91,7 @@ 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');
|
||||
@@ -109,18 +104,18 @@ function parentDomain(domain) {
|
||||
return domain.replace(/^\S+?\./, ''); // +? means non-greedy
|
||||
}
|
||||
|
||||
function verifyDnsConfig(dnsConfig, domain, zoneName, provider, ip, callback) {
|
||||
function verifyDnsConfig(dnsConfig, domain, zoneName, provider, callback) {
|
||||
assert(dnsConfig && typeof dnsConfig === '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'));
|
||||
|
||||
api(provider).verifyDnsConfig(dnsConfig, domain, zoneName, ip, function (error, result) {
|
||||
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));
|
||||
@@ -199,15 +194,17 @@ function validateTlsConfig(tlsConfig, dnsProvider) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function add(domain, zoneName, provider, dnsConfig, fallbackCertificate, tlsConfig, callback) {
|
||||
function add(domain, data, auditSource, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
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 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 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'));
|
||||
|
||||
@@ -219,31 +216,29 @@ function add(domain, zoneName, provider, dnsConfig, fallbackCertificate, tlsConf
|
||||
}
|
||||
|
||||
if (fallbackCertificate) {
|
||||
let error = reverseProxy.validateCertificate('test', { domain: domain, config: dnsConfig }, fallbackCertificate);
|
||||
let error = reverseProxy.validateCertificate('test', { domain, config }, fallbackCertificate);
|
||||
if (error) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
|
||||
} else {
|
||||
fallbackCertificate = reverseProxy.generateFallbackCertificateSync({ domain: domain, config: dnsConfig });
|
||||
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);
|
||||
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
|
||||
verifyDnsConfig(config, domain, zoneName, provider, function (error, sanitizedConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
verifyDnsConfig(dnsConfig, domain, zoneName, provider, ip, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
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));
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, 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));
|
||||
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
|
||||
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
eventlog.add(eventlog.ACTION_DOMAIN_ADD, auditSource, { domain, zoneName, provider });
|
||||
|
||||
callback();
|
||||
});
|
||||
callback();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -291,15 +286,18 @@ function getAll(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function update(domain, zoneName, provider, dnsConfig, fallbackCertificate, tlsConfig, callback) {
|
||||
function update(domain, data, auditSource, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
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 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 callback, 'function');
|
||||
|
||||
let { zoneName, provider, config, fallbackCertificate, tlsConfig } = data;
|
||||
|
||||
domaindb.get(domain, function (error, domainObject) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainsError(DomainsError.NOT_FOUND));
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
@@ -318,31 +316,30 @@ function update(domain, zoneName, provider, dnsConfig, fallbackCertificate, tlsC
|
||||
error = validateTlsConfig(tlsConfig, provider);
|
||||
if (error) return callback(error);
|
||||
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
|
||||
verifyDnsConfig(config, domain, zoneName, provider, function (error, sanitizedConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
verifyDnsConfig(dnsConfig, domain, zoneName, provider, ip, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
domaindb.update(domain, { zoneName: zoneName, provider: provider, config: sanitizedConfig, 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));
|
||||
|
||||
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 (!fallbackCertificate) return callback();
|
||||
|
||||
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (!fallbackCertificate) return callback();
|
||||
eventlog.add(eventlog.ACTION_DOMAIN_UPDATE, auditSource, { domain, zoneName, provider });
|
||||
|
||||
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback();
|
||||
});
|
||||
callback();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function del(domain, callback) {
|
||||
function del(domain, auditSource, 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));
|
||||
@@ -352,44 +349,53 @@ function del(domain, 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, subdomain, type) {
|
||||
// hack for supporting special caas domains. if we want to remove this, we have to fix the appstore domain API first
|
||||
if (domain.provider === 'caas') return subdomain;
|
||||
|
||||
function getName(domain, location, type) {
|
||||
const part = domain.domain.slice(0, -domain.zoneName.length - 1);
|
||||
|
||||
if (subdomain === '') return part;
|
||||
if (location === '') return part;
|
||||
|
||||
if (!domain.config.hyphenatedSubdomains) return part ? `${subdomain}.${part}` : subdomain;
|
||||
if (!domain.config.hyphenatedSubdomains) return part ? `${location}.${part}` : location;
|
||||
|
||||
// hyphenatedSubdomains
|
||||
if (type !== 'TXT') return `${subdomain}-${part}`;
|
||||
if (type !== 'TXT') return `${location}-${part}`;
|
||||
|
||||
if (subdomain.startsWith('_acme-challenge.')) {
|
||||
return `${subdomain}-${part}`;
|
||||
} else if (subdomain === '_acme-challenge') {
|
||||
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 ? `${subdomain}.${up}` : subdomain;
|
||||
return up ? `${location}.${up}` : location;
|
||||
} else {
|
||||
return `${subdomain}.${part}`;
|
||||
return `${location}.${part}`;
|
||||
}
|
||||
}
|
||||
|
||||
function getDnsRecords(subdomain, domain, type, callback) {
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
function getDnsRecords(location, domain, type, callback) {
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
get(domain, function (error, result) {
|
||||
get(domain, function (error, domainObject) {
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
|
||||
api(result.provider).get(result.config, result.zoneName, getName(result, subdomain, type), type, function (error, values) {
|
||||
api(domainObject.provider).get(domainObject, location, type, function (error, values) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, values);
|
||||
@@ -398,19 +404,19 @@ function getDnsRecords(subdomain, domain, type, callback) {
|
||||
}
|
||||
|
||||
// note: for TXT records the values must be quoted
|
||||
function upsertDnsRecords(subdomain, domain, type, values, callback) {
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
function upsertDnsRecords(location, domain, type, values, callback) {
|
||||
assert.strictEqual(typeof location, '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', subdomain, domain, type, values);
|
||||
debug('upsertDNSRecord: %s on %s type %s values', location, domain, type, values);
|
||||
|
||||
get(domain, function (error, result) {
|
||||
get(domain, function (error, domainObject) {
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
|
||||
api(result.provider).upsert(result.config, result.zoneName, getName(result, subdomain, type), type, values, function (error) {
|
||||
api(domainObject.provider).upsert(domainObject, location, type, values, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null);
|
||||
@@ -418,19 +424,19 @@ function upsertDnsRecords(subdomain, domain, type, values, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function removeDnsRecords(subdomain, domain, type, values, callback) {
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
function removeDnsRecords(location, domain, type, values, callback) {
|
||||
assert.strictEqual(typeof location, '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', subdomain, domain, type, values);
|
||||
debug('removeDNSRecord: %s on %s type %s values', location, domain, type, values);
|
||||
|
||||
get(domain, function (error, result) {
|
||||
get(domain, function (error, domainObject) {
|
||||
if (error) return callback(error);
|
||||
|
||||
api(result.provider).del(result.config, result.zoneName, getName(result, subdomain, type), type, values, function (error) {
|
||||
api(domainObject.provider).del(domainObject, location, type, values, function (error) {
|
||||
if (error && error.reason !== DomainsError.NOT_FOUND) return callback(error);
|
||||
|
||||
callback(null);
|
||||
@@ -438,8 +444,8 @@ function removeDnsRecords(subdomain, domain, type, values, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function waitForDnsRecord(subdomain, domain, type, value, options, callback) {
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
function waitForDnsRecord(location, domain, type, value, options, callback) {
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert(type === 'A' || type === 'TXT');
|
||||
assert.strictEqual(typeof value, 'string');
|
||||
@@ -449,34 +455,7 @@ function waitForDnsRecord(subdomain, domain, type, value, options, callback) {
|
||||
get(domain, function (error, domainObject) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const hostname = fqdn(subdomain, domainObject);
|
||||
|
||||
api(domainObject.provider).waitForDns(hostname, domainObject.zoneName, type, 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);
|
||||
});
|
||||
api(domainObject.provider).wait(domainObject, location, type, value, options, callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -505,15 +484,30 @@ function makeWildcard(hostname) {
|
||||
return parts.join('.');
|
||||
}
|
||||
|
||||
function renewCerts(domain, auditSource, callback) {
|
||||
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');
|
||||
|
||||
// trigger renewal in the background
|
||||
reverseProxy.renewCerts({ domain: domain }, auditSource, function (error) {
|
||||
debug('renewCerts', error);
|
||||
});
|
||||
get(domain, function (error, domainObject) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback();
|
||||
}
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
+16
-3
@@ -10,6 +10,9 @@ 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); };
|
||||
@@ -21,12 +24,18 @@ function sync(callback) {
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('refreshDNS: current ip %s', ip);
|
||||
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}`);
|
||||
|
||||
domains.upsertDnsRecords(config.adminLocation(), config.adminDomain(), 'A', [ ip ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('refreshDNS: done for admin location');
|
||||
debug('refreshDNS: updated admin location');
|
||||
|
||||
apps.getAll(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
@@ -39,7 +48,11 @@ function sync(callback) {
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('refreshDNS: done for apps');
|
||||
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');
|
||||
|
||||
callback();
|
||||
});
|
||||
|
||||
+47
-1
@@ -18,23 +18,50 @@ 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_DOWN: 'app.down',
|
||||
ACTION_APP_TASK_CRASH: 'app.task.crash',
|
||||
|
||||
ACTION_BACKUP_FINISH: 'backup.finish',
|
||||
ACTION_BACKUP_START: 'backup.start',
|
||||
ACTION_BACKUP_CLEANUP: 'backup.cleanup',
|
||||
ACTION_BACKUP_CLEANUP_START: 'backup.cleanup.start',
|
||||
ACTION_BACKUP_CLEANUP_FINISH: 'backup.cleanup.finish',
|
||||
|
||||
ACTION_CERTIFICATE_NEW: 'certificate.new',
|
||||
ACTION_CERTIFICATE_RENEWAL: 'certificate.renew',
|
||||
|
||||
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');
|
||||
|
||||
@@ -75,6 +102,25 @@ function add(action, source, data, callback) {
|
||||
eventlogdb.add(id, action, source, data, function (error) {
|
||||
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
|
||||
|
||||
// decide if we want to add notifications as well
|
||||
if (action === exports.ACTION_USER_ADD) {
|
||||
notifications.userAdded(source.userId, id, data.user);
|
||||
} if (action === exports.ACTION_USER_REMOVE) {
|
||||
notifications.userRemoved(source.userId, id, data.user);
|
||||
} if (action === exports.ACTION_USER_UPDATE && data.adminStatusChanged) {
|
||||
notifications.adminChanged(source.userId, id, data.user);
|
||||
} if (action === exports.ACTION_APP_OOM) {
|
||||
notifications.oomEvent(id, source.app ? source.app.id : source.containerId, { app: source.app, details: data });
|
||||
} if (action === exports.ACTION_APP_DOWN) {
|
||||
notifications.appDied(id, source.app);
|
||||
} if (action === exports.ACTION_APP_TASK_CRASH) {
|
||||
notifications.apptaskCrash(id, source.appId, data.crashLogFile);
|
||||
} if (action === exports.ACTION_PROCESS_CRASH) {
|
||||
notifications.processCrash(id, source.processName, data.crashLogFile);
|
||||
} else {
|
||||
// no notification
|
||||
}
|
||||
|
||||
callback(null, { id: id });
|
||||
});
|
||||
}
|
||||
|
||||
+13
-4
@@ -12,6 +12,7 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
database = require('./database.js'),
|
||||
DatabaseError = require('./databaseerror'),
|
||||
mysql = require('mysql'),
|
||||
@@ -125,11 +126,19 @@ function delByCreationTime(creationTime, callback) {
|
||||
assert(util.isDate(creationTime));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var query = 'DELETE FROM eventlog WHERE creationTime < ?';
|
||||
|
||||
database.query(query, [ creationTime ], function (error) {
|
||||
// since notifications reference eventlog items, we have to clean them up as well
|
||||
database.query('SELECT * FROM eventlog WHERE creationTime < ?', [ creationTime ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(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);
|
||||
});
|
||||
}
|
||||
|
||||
+3
-4
@@ -31,11 +31,10 @@ function startGraphite(existingInfra, callback) {
|
||||
--dns-search=. \
|
||||
-p 127.0.0.1:2003:2003 \
|
||||
-p 127.0.0.1:2004:2004 \
|
||||
-p 127.0.0.1:8000:8000 \
|
||||
-p 127.0.0.1:8417:8000 \
|
||||
-v "${dataDir}/graphite:/var/lib/graphite" \
|
||||
--label isCloudronManaged=true \
|
||||
--read-only -v /tmp -v /run "${tag}"`;
|
||||
|
||||
shell.execSync('startGraphite', cmd);
|
||||
|
||||
callback();
|
||||
shell.exec('startGraphite', cmd, callback);
|
||||
}
|
||||
|
||||
+14
-14
@@ -34,7 +34,7 @@ function get(groupId, callback) {
|
||||
assert.strictEqual(typeof groupId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + GROUPS_FIELDS + ' FROM groups WHERE id = ? ORDER BY name', [ groupId ], function (error, result) {
|
||||
database.query('SELECT ' + GROUPS_FIELDS + ' FROM userGroups 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 groups LEFT OUTER JOIN groupMembers ON groups.id = groupMembers.groupId ' +
|
||||
' WHERE groups.id = ? ' +
|
||||
' GROUP BY groups.id', [ groupId ], function (error, results) {
|
||||
' FROM userGroups LEFT OUTER JOIN groupMembers ON userGroups.id = groupMembers.groupId ' +
|
||||
' WHERE userGroups.id = ? ' +
|
||||
' GROUP BY userGroups.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 groups', function (error, results) {
|
||||
database.query('SELECT ' + GROUPS_FIELDS + ' FROM userGroups', 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 groups LEFT OUTER JOIN groupMembers ON groups.id = groupMembers.groupId ' +
|
||||
' GROUP BY groups.id', function (error, results) {
|
||||
' FROM userGroups LEFT OUTER JOIN groupMembers ON userGroups.id = groupMembers.groupId ' +
|
||||
' GROUP BY userGroups.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 groups (id, name) VALUES (?, ?)', [ id, name ], function (error, result) {
|
||||
database.query('INSERT INTO userGroups (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 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'));
|
||||
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'));
|
||||
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 groups WHERE id = ?', args: [ id ] });
|
||||
queries.push({ query: 'DELETE FROM userGroups 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 groups', function (error, result) {
|
||||
database.query('SELECT COUNT(*) AS total FROM userGroups', 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 groups', function (error) {
|
||||
database.query('DELETE FROM userGroups', 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 groups INNER JOIN groupMembers ON groups.id = groupMembers.groupId AND groupMembers.userId = ?', [ userId ], function (error, results) {
|
||||
' FROM userGroups INNER JOIN groupMembers ON userGroups.id = groupMembers.groupId AND groupMembers.userId = ?', [ userId ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, results);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
exports = module.exports = {
|
||||
// a version change recreates all containers with latest docker config
|
||||
'version': '48.12.0',
|
||||
'version': '48.12.1',
|
||||
|
||||
'baseImages': [
|
||||
{ repo: 'cloudron/base', tag: 'cloudron/base:1.0.0@sha256:147a648a068a2e746644746bbfb42eb7a50d682437cead3c67c933c546357617' }
|
||||
@@ -17,9 +17,9 @@ exports = module.exports = {
|
||||
'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.1@sha256:0f320ba40080943840fadb3e66b98066fc4f3dc98b96638e3067a8a5ab84bcee' },
|
||||
'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.0.0@sha256:3c0fbb2a042ac471940ac3e9f6ffa900c8a294941fb7de509b2e3309b09fbffd' },
|
||||
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.0.0@sha256:454f035d60b768153d4f31210380271b5ba1c09367c9d95c7fa37f9e39d2f59c' }
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.1.0@sha256:131db42dcb90111f679ab1f0f37c552f93f797d9b803b2346c7c202daf86ac36' },
|
||||
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.0.2@sha256:454f035d60b768153d4f31210380271b5ba1c09367c9d95c7fa37f9e39d2f59c' }
|
||||
}
|
||||
};
|
||||
|
||||
+60
-36
@@ -47,7 +47,7 @@ function getUsersWithAccessToApp(req, callback) {
|
||||
assert.strictEqual(typeof req.app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
users.list(function (error, result) {
|
||||
users.getAll(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,7 +271,6 @@ 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',
|
||||
@@ -297,9 +296,6 @@ 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`);
|
||||
@@ -311,8 +307,7 @@ function mailboxSearch(req, res, next) {
|
||||
objectcategory: 'mailbox',
|
||||
cn: `${mailbox.name}@${domain}`,
|
||||
uid: `${mailbox.name}@${domain}`,
|
||||
mail: `${mailbox.name}@${domain}`,
|
||||
ownerType: mailbox.ownerType
|
||||
mail: `${mailbox.name}@${domain}`
|
||||
}
|
||||
};
|
||||
|
||||
@@ -455,8 +450,8 @@ function authorizeUserForApp(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function authenticateMailbox(req, res, next) {
|
||||
debug('mailbox auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
|
||||
function authenticateUserMailbox(req, res, next) {
|
||||
debug('user 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()));
|
||||
|
||||
@@ -464,30 +459,61 @@ function authenticateMailbox(req, res, next) {
|
||||
var parts = email.split('@');
|
||||
if (parts.length !== 2) 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));
|
||||
|
||||
mail.getDomain(parts[1], function (error, domain) {
|
||||
if (error && error.reason === MailError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
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()));
|
||||
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'));
|
||||
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));
|
||||
|
||||
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()));
|
||||
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) });
|
||||
res.end();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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()));
|
||||
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));
|
||||
|
||||
users.verify(mailbox.ownerId, req.credentials || '', function (error, result) {
|
||||
if (error && error.reason === UsersError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
@@ -497,9 +523,7 @@ function authenticateMailbox(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'));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -523,13 +547,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);
|
||||
gServer.search('ou=mailaliases,dc=cloudron', mailAliasSearch);
|
||||
gServer.search('ou=mailinglists,dc=cloudron', mailingListSearch);
|
||||
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.bind('ou=mailboxes,dc=cloudron', authenticateMailbox);
|
||||
gServer.bind('ou=recvmail,dc=cloudron', authenticateMailbox);
|
||||
gServer.bind('ou=sendmail,dc=cloudron', authenticateMailbox);
|
||||
gServer.bind('ou=recvmail,dc=cloudron', authenticateMailAddon); // dovecot
|
||||
gServer.bind('ou=sendmail,dc=cloudron', authenticateMailAddon); // haraka
|
||||
|
||||
gServer.compare('cn=users,ou=groups,dc=cloudron', authenticateApp, groupUsersCompare);
|
||||
gServer.compare('cn=admins,ou=groups,dc=cloudron', authenticateApp, groupAdminsCompare);
|
||||
|
||||
@@ -18,7 +18,6 @@ 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');
|
||||
|
||||
+13
-30
@@ -5,7 +5,7 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
mailer = require('./mailer.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
safe = require('safetydance'),
|
||||
path = require('path'),
|
||||
util = require('util');
|
||||
@@ -15,62 +15,45 @@ 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';
|
||||
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';
|
||||
|
||||
callback(null, logs);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
function sendFailureLogs(unitName) {
|
||||
assert.strictEqual(typeof unitName, 'string');
|
||||
|
||||
// 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) {
|
||||
collectLogs(unitName, function (error, logs) {
|
||||
if (error) {
|
||||
console.error('Failed to collect logs.', error);
|
||||
newLogs = util.format('Failed to collect logs.', error);
|
||||
logs = util.format('Failed to collect logs.', error);
|
||||
}
|
||||
|
||||
console.log('Sending failure logs for', processName);
|
||||
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}`);
|
||||
|
||||
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 stashLogs(newLogs);
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
eventlog.add(eventlog.ACTION_PROCESS_CRASH, { processName: unitName }, { crashLogFile: CRASH_LOG_STASH_FILE }, function (error) {
|
||||
if (error) console.log(`Error sending crashlog. Logs stashed at ${CRASH_LOG_STASH_FILE}`);
|
||||
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+76
-52
@@ -8,6 +8,7 @@ exports = module.exports = {
|
||||
getDomain: getDomain,
|
||||
addDomain: addDomain,
|
||||
removeDomain: removeDomain,
|
||||
clearDomains: clearDomains,
|
||||
|
||||
setDnsRecords: setDnsRecords,
|
||||
|
||||
@@ -22,11 +23,11 @@ exports = module.exports = {
|
||||
|
||||
sendTestMail: sendTestMail,
|
||||
|
||||
getMailboxes: getMailboxes,
|
||||
listMailboxes: listMailboxes,
|
||||
removeMailboxes: removeMailboxes,
|
||||
getMailbox: getMailbox,
|
||||
addMailbox: addMailbox,
|
||||
updateMailbox: updateMailbox,
|
||||
updateMailboxOwner: updateMailboxOwner,
|
||||
removeMailbox: removeMailbox,
|
||||
|
||||
listAliases: listAliases,
|
||||
@@ -51,13 +52,14 @@ 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'),
|
||||
@@ -108,9 +110,6 @@ 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 +141,13 @@ function checkOutboundPort25(callback) {
|
||||
});
|
||||
client.on('timeout', function () {
|
||||
relay.status = false;
|
||||
relay.value = 'Connect to ' + smtpServer + ' timed out';
|
||||
relay.value = `Connect to ${smtpServer} timed out. Please check if port 25 is blocked`;
|
||||
client.destroy();
|
||||
callback(new Error('Timeout'), relay);
|
||||
});
|
||||
client.on('error', function (error) {
|
||||
relay.status = false;
|
||||
relay.value = 'Connect to ' + smtpServer + ' failed: ' + error.message;
|
||||
relay.value = `Connect to ${smtpServer} failed: ${error.message}. Please check if port 25 is blocked`;
|
||||
client.destroy();
|
||||
callback(error, relay);
|
||||
});
|
||||
@@ -494,8 +493,7 @@ function createMailConfig(callback) {
|
||||
|
||||
users.getOwner(function (error, owner) {
|
||||
const mailFqdn = config.mailFqdn();
|
||||
const defaultDomain = config.adminDomain();
|
||||
const alertsFrom = `no-reply@${defaultDomain}`;
|
||||
const alertsFrom = `no-reply@${config.adminDomain()}`;
|
||||
|
||||
const alertsTo = config.provider() === 'caas' ? [ 'support@cloudron.io' ] : [ ];
|
||||
alertsTo.concat(error ? [] : owner.email).join(','); // owner may not exist yet
|
||||
@@ -504,7 +502,7 @@ function createMailConfig(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_default_domain=${defaultDomain}\nmail_server_name=${mailFqdn}\nalerts_from=${alertsFrom}\nalerts_to=${alertsTo}\n\n`, 'utf8')) {
|
||||
`mail_in_domains=${mailInDomains}\nmail_out_domains=${mailOutDomains}\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));
|
||||
}
|
||||
|
||||
@@ -553,10 +551,11 @@ function restartMail(callback) {
|
||||
if (process.env.BOX_ENV === 'test' && !process.env.TEST_CREATE_INFRA) return callback();
|
||||
|
||||
const tag = infra.images.mail.tag;
|
||||
const memoryLimit = Math.max((1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 128, 256);
|
||||
const memoryLimit = 4 * 256;
|
||||
const cloudronToken = hat(8 * 128), relayToken = hat(8 * 128);
|
||||
|
||||
// admin and mail share the same certificate
|
||||
reverseProxy.getCertificate({ fqdn: config.adminFqdn(), domain: config.adminDomain() }, function (error, bundle) {
|
||||
reverseProxy.getCertificate(config.adminFqdn(), config.adminDomain(), function (error, bundle) {
|
||||
if (error) return callback(error);
|
||||
|
||||
// the setup script copies dhparams.pem to /addons/mail
|
||||
@@ -566,33 +565,36 @@ function restartMail(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.execSync('startMail', 'docker rm -f mail || true');
|
||||
|
||||
createMailConfig(function (error, allowInbound) {
|
||||
shell.exec('startMail', 'docker rm -f mail || true', function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var ports = allowInbound ? '-p 587:2525 -p 993:9993 -p 4190:4190 -p 25:2525' : '';
|
||||
createMailConfig(function (error, allowInbound) {
|
||||
if (error) return callback(error);
|
||||
|
||||
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}`;
|
||||
var ports = allowInbound ? '-p 587:2525 -p 993:9993 -p 4190:4190 -p 25:2525' : '';
|
||||
|
||||
shell.execSync('startMail', cmd);
|
||||
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}`;
|
||||
|
||||
callback();
|
||||
shell.exec('startMail', cmd, callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -600,9 +602,9 @@ function restartMail(callback) {
|
||||
function restartMailIfActivated(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
users.count(function (error, count) {
|
||||
users.isActivated(function (error, activated) {
|
||||
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
|
||||
if (!count) {
|
||||
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
|
||||
}
|
||||
@@ -626,7 +628,7 @@ function getDomain(domain, callback) {
|
||||
function getDomains(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
maildb.getAll(function (error, results) {
|
||||
maildb.list(function (error, results) {
|
||||
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null, results);
|
||||
@@ -803,6 +805,16 @@ 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');
|
||||
@@ -852,9 +864,10 @@ function setMailRelay(domain, relay, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function setMailEnabled(domain, enabled, callback) {
|
||||
function setMailEnabled(domain, enabled, auditSource, 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) {
|
||||
@@ -863,6 +876,8 @@ function setMailEnabled(domain, enabled, callback) {
|
||||
|
||||
restartMail(NOOP_CALLBACK);
|
||||
|
||||
eventlog.add(enabled ? eventlog.ACTION_MAIL_ENABLED : eventlog.ACTION_MAIL_DISABLED, auditSource, { domain });
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
@@ -881,7 +896,7 @@ function sendTestMail(domain, to, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getMailboxes(domain, callback) {
|
||||
function listMailboxes(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -916,10 +931,11 @@ function getMailbox(name, domain, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function addMailbox(name, domain, userId, callback) {
|
||||
function addMailbox(name, domain, userId, auditSource, 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();
|
||||
@@ -927,15 +943,17 @@ function addMailbox(name, domain, userId, callback) {
|
||||
var error = validateName(name);
|
||||
if (error) return callback(error);
|
||||
|
||||
mailboxdb.addMailbox(name, domain, userId, mailboxdb.OWNER_TYPE_USER, function (error) {
|
||||
mailboxdb.addMailbox(name, domain, userId, 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 updateMailbox(name, domain, userId, callback) {
|
||||
function updateMailboxOwner(name, domain, userId, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
@@ -943,10 +961,7 @@ function updateMailbox(name, domain, userId, callback) {
|
||||
|
||||
name = name.toLowerCase();
|
||||
|
||||
var error = validateName(name);
|
||||
if (error) return callback(error);
|
||||
|
||||
mailboxdb.updateMailbox(name, domain, userId, mailboxdb.OWNER_TYPE_USER, function (error) {
|
||||
mailboxdb.updateMailboxOwner(name, domain, userId, 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));
|
||||
|
||||
@@ -954,15 +969,18 @@ function updateMailbox(name, domain, userId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function removeMailbox(name, domain, callback) {
|
||||
function removeMailbox(name, domain, auditSource, 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);
|
||||
});
|
||||
}
|
||||
@@ -1047,10 +1065,11 @@ function getList(domain, listName, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function addList(name, domain, members, callback) {
|
||||
function addList(name, domain, members, auditSource, 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();
|
||||
@@ -1069,6 +1088,8 @@ function addList(name, domain, members, 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();
|
||||
});
|
||||
}
|
||||
@@ -1099,15 +1120,18 @@ function updateList(name, domain, members, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function removeList(domain, listName, callback) {
|
||||
function removeList(name, domain, auditSource, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof listName, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
mailboxdb.del(listName, domain, function (error) {
|
||||
mailboxdb.del(name, 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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Dear <%= cloudronName %> Admin,
|
||||
|
||||
Unfortunately <%= program %> exited unexpectedly!
|
||||
<%= subject %>
|
||||
|
||||
Please see some excerpt of the logs below:
|
||||
|
||||
|
||||
+11
-17
@@ -4,7 +4,7 @@ exports = module.exports = {
|
||||
addMailbox: addMailbox,
|
||||
addGroup: addGroup,
|
||||
|
||||
updateMailbox: updateMailbox,
|
||||
updateMailboxOwner: updateMailboxOwner,
|
||||
updateList: updateList,
|
||||
del: del,
|
||||
|
||||
@@ -29,11 +29,7 @@ exports = module.exports = {
|
||||
|
||||
TYPE_MAILBOX: 'mailbox',
|
||||
TYPE_LIST: 'list',
|
||||
TYPE_ALIAS: 'alias',
|
||||
|
||||
OWNER_TYPE_USER: 'user',
|
||||
OWNER_TYPE_APP: 'app',
|
||||
OWNER_TYPE_GROUP: 'group' // obsolete
|
||||
TYPE_ALIAS: 'alias'
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
@@ -42,7 +38,7 @@ var assert = require('assert'),
|
||||
safe = require('safetydance'),
|
||||
util = require('util');
|
||||
|
||||
var MAILBOX_FIELDS = [ 'name', 'type', 'ownerId', 'ownerType', 'aliasTarget', 'creationTime', 'membersJson', 'domain' ].join(',');
|
||||
var MAILBOX_FIELDS = [ 'name', 'type', 'ownerId', 'aliasTarget', 'creationTime', 'membersJson', 'domain' ].join(',');
|
||||
|
||||
function postProcess(data) {
|
||||
data.members = safe.JSON.parse(data.membersJson) || [ ];
|
||||
@@ -51,14 +47,13 @@ function postProcess(data) {
|
||||
return data;
|
||||
}
|
||||
|
||||
function addMailbox(name, domain, ownerId, ownerType, callback) {
|
||||
function addMailbox(name, domain, ownerId, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof ownerId, 'string');
|
||||
assert.strictEqual(typeof ownerType, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('INSERT INTO mailboxes (name, type, domain, ownerId, ownerType) VALUES (?, ?, ?, ?, ?)', [ name, exports.TYPE_MAILBOX, domain, ownerId, ownerType ], function (error) {
|
||||
database.query('INSERT INTO mailboxes (name, type, domain, ownerId) VALUES (?, ?, ?, ?)', [ name, exports.TYPE_MAILBOX, domain, ownerId ], function (error) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, 'mailbox already exists'));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
@@ -66,14 +61,13 @@ function addMailbox(name, domain, ownerId, ownerType, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function updateMailbox(name, domain, ownerId, ownerType, callback) {
|
||||
function updateMailboxOwner(name, domain, ownerId, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof ownerId, 'string');
|
||||
assert.strictEqual(typeof ownerType, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('UPDATE mailboxes SET ownerId = ? WHERE name = ? AND domain = ? AND ownerType = ?', [ ownerId, name, domain, ownerType ], function (error, result) {
|
||||
database.query('UPDATE mailboxes SET ownerId = ? WHERE name = ? AND domain = ?', [ ownerId, name, domain ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.affectedRows === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
@@ -87,8 +81,8 @@ function addGroup(name, domain, members, callback) {
|
||||
assert(Array.isArray(members));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('INSERT INTO mailboxes (name, type, domain, ownerId, ownerType, membersJson) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[ name, exports.TYPE_LIST, domain, 'admin', exports.OWNER_TYPE_GROUP, JSON.stringify(members) ], function (error) {
|
||||
database.query('INSERT INTO mailboxes (name, type, domain, ownerId, membersJson) VALUES (?, ?, ?, ?, ?)',
|
||||
[ name, exports.TYPE_LIST, domain, 'admin', JSON.stringify(members) ], function (error) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, 'mailbox already exists'));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
@@ -259,8 +253,8 @@ function setAliasesForName(name, domain, aliases, callback) {
|
||||
// clear existing aliases
|
||||
queries.push({ query: 'DELETE FROM mailboxes WHERE aliasTarget = ? AND domain = ? AND type = ?', args: [ name, domain, exports.TYPE_ALIAS ] });
|
||||
aliases.forEach(function (alias) {
|
||||
queries.push({ query: 'INSERT INTO mailboxes (name, type, domain, aliasTarget, ownerId, ownerType) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
args: [ alias, exports.TYPE_ALIAS, domain, name, results[0].ownerId, results[0].ownerType ] });
|
||||
queries.push({ query: 'INSERT INTO mailboxes (name, type, domain, aliasTarget, ownerId) VALUES (?, ?, ?, ?, ?)',
|
||||
args: [ alias, exports.TYPE_ALIAS, domain, name, results[0].ownerId ] });
|
||||
});
|
||||
|
||||
database.transaction(queries, function (error) {
|
||||
|
||||
+5
-4
@@ -4,10 +4,10 @@ exports = module.exports = {
|
||||
add: add,
|
||||
del: del,
|
||||
get: get,
|
||||
getAll: getAll,
|
||||
list: list,
|
||||
update: update,
|
||||
|
||||
_clear: clear,
|
||||
clear: clear,
|
||||
|
||||
TYPE_USER: 'user',
|
||||
TYPE_APP: 'app',
|
||||
@@ -49,7 +49,8 @@ function add(domain, callback) {
|
||||
function clear(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('TRUNCATE TABLE mail', [], function (error) {
|
||||
// using TRUNCATE makes it fail foreign key check
|
||||
database.query('DELETE FROM mail', [], function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
callback(null);
|
||||
});
|
||||
@@ -81,7 +82,7 @@ function get(domain, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getAll(callback) {
|
||||
function list(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + MAILDB_FIELDS + ' FROM mail ORDER BY domain', function (error, results) {
|
||||
|
||||
+42
-31
@@ -40,8 +40,7 @@ var assert = require('assert'),
|
||||
showdown = require('showdown'),
|
||||
smtpTransport = require('nodemailer-smtp-transport'),
|
||||
users = require('./users.js'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
util = require('util');
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
@@ -122,9 +121,21 @@ function sendMails(queue, callback) {
|
||||
var mailServerIp = safe.query(data, 'NetworkSettings.Networks.cloudron.IPAddress');
|
||||
if (!mailServerIp) return callback('Error querying mail server IP');
|
||||
|
||||
// extract the relay token for auth
|
||||
const env = safe.query(data, 'Config.Env', null);
|
||||
if (!env) return callback(new Error('Error getting mail env'));
|
||||
const tmp = env.find(function (e) { return e.indexOf('CLOUDRON_RELAY_TOKEN') === 0; });
|
||||
if (!tmp) return callback(new Error('Error getting CLOUDRON_RELAY_TOKEN env var'));
|
||||
const relayToken = tmp.slice('CLOUDRON_RELAY_TOKEN'.length + 1); // +1 for the = sign
|
||||
if (!relayToken) return callback(new Error('Error parsing CLOUDRON_RELAY_TOKEN'));
|
||||
|
||||
var transport = nodemailer.createTransport(smtpTransport({
|
||||
host: mailServerIp,
|
||||
port: config.get('smtpPort')
|
||||
port: config.get('smtpPort'),
|
||||
auth: {
|
||||
user: `no-reply@${config.adminDomain()}`,
|
||||
pass: relayToken
|
||||
}
|
||||
}));
|
||||
|
||||
debug('Processing mail queue of size %d (through %s:2525)', queue.length, mailServerIp);
|
||||
@@ -170,18 +181,17 @@ function render(templateFile, params) {
|
||||
return content;
|
||||
}
|
||||
|
||||
function mailUserEventToAdmins(user, event) {
|
||||
function mailUserEvent(mailTo, user, event) {
|
||||
assert.strictEqual(typeof mailTo, 'string');
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof event, 'string');
|
||||
|
||||
getMailConfig(function (error, mailConfig) {
|
||||
if (error) return debug('Error getting mail details:', error);
|
||||
|
||||
var adminEmails = _.difference(mailConfig.adminEmails, [ user.email ]);
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig.notificationFrom,
|
||||
to: adminEmails.join(', '),
|
||||
to: mailTo,
|
||||
subject: util.format('[%s] %s %s', mailConfig.cloudronName, user.username || user.fallbackEmail || user.email, event),
|
||||
text: render('user_event.ejs', { user: user, event: event, format: 'text' }),
|
||||
};
|
||||
@@ -226,16 +236,15 @@ function sendInvite(user, invitor) {
|
||||
});
|
||||
}
|
||||
|
||||
function userAdded(user) {
|
||||
function userAdded(mailTo, user) {
|
||||
assert.strictEqual(typeof mailTo, 'string');
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
|
||||
debug('Sending mail for userAdded');
|
||||
debug(`userAdded: Sending mail for added users ${user.fallbackEmail} to ${mailTo}`);
|
||||
|
||||
getMailConfig(function (error, mailConfig) {
|
||||
if (error) return debug('Error getting mail details:', error);
|
||||
|
||||
var adminEmails = _.difference(mailConfig.adminEmails, [ user.email ]);
|
||||
|
||||
var templateData = {
|
||||
user: user,
|
||||
cloudronName: mailConfig.cloudronName,
|
||||
@@ -250,7 +259,7 @@ function userAdded(user) {
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig.notificationFrom,
|
||||
to: adminEmails.join(', '),
|
||||
to: mailTo,
|
||||
subject: util.format('[%s] User %s added', mailConfig.cloudronName, user.fallbackEmail),
|
||||
text: render('user_added.ejs', templateDataText),
|
||||
html: render('user_added.ejs', templateDataHTML)
|
||||
@@ -260,21 +269,23 @@ function userAdded(user) {
|
||||
});
|
||||
}
|
||||
|
||||
function userRemoved(user) {
|
||||
function userRemoved(mailTo, user) {
|
||||
assert.strictEqual(typeof mailTo, 'string');
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
|
||||
debug('Sending mail for userRemoved.', user.id, user.email);
|
||||
debug('Sending mail for userRemoved.', user.id, user.username, user.email);
|
||||
|
||||
mailUserEventToAdmins(user, 'was removed');
|
||||
mailUserEvent(mailTo, user, 'was removed');
|
||||
}
|
||||
|
||||
function adminChanged(user, admin) {
|
||||
function adminChanged(mailTo, user, isAdmin) {
|
||||
assert.strictEqual(typeof mailTo, 'string');
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof admin, 'boolean');
|
||||
assert.strictEqual(typeof isAdmin, 'boolean');
|
||||
|
||||
debug('Sending mail for adminChanged');
|
||||
|
||||
mailUserEventToAdmins(user, admin ? 'is now an admin' : 'is no more an admin');
|
||||
mailUserEvent(mailTo, user, isAdmin ? 'is now an admin' : 'is no more an admin');
|
||||
}
|
||||
|
||||
function passwordReset(user) {
|
||||
@@ -310,7 +321,8 @@ function passwordReset(user) {
|
||||
});
|
||||
}
|
||||
|
||||
function appDied(app) {
|
||||
function appDied(mailTo, app) {
|
||||
assert.strictEqual(typeof mailTo, 'string');
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
|
||||
debug('Sending mail for app %s @ %s died', app.id, app.fqdn);
|
||||
@@ -320,7 +332,7 @@ function appDied(app) {
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig.notificationFrom,
|
||||
to: config.provider() === 'caas' ? 'support@cloudron.io' : mailConfig.adminEmails.join(', '),
|
||||
to: mailTo,
|
||||
subject: util.format('[%s] App %s is down', mailConfig.cloudronName, app.fqdn),
|
||||
text: render('app_down.ejs', { title: app.manifest.title, appFqdn: app.fqdn, format: 'text' })
|
||||
};
|
||||
@@ -488,7 +500,8 @@ function certificateRenewalError(domain, message) {
|
||||
});
|
||||
}
|
||||
|
||||
function oomEvent(program, context) {
|
||||
function oomEvent(mailTo, program, context) {
|
||||
assert.strictEqual(typeof mailTo, 'string');
|
||||
assert.strictEqual(typeof program, 'string');
|
||||
assert.strictEqual(typeof context, 'string');
|
||||
|
||||
@@ -497,7 +510,7 @@ function oomEvent(program, context) {
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig.notificationFrom,
|
||||
to: config.provider() === 'caas' ? 'support@cloudron.io' : mailConfig.adminEmails.join(', '),
|
||||
to: mailTo,
|
||||
subject: util.format('[%s] %s exited unexpectedly', mailConfig.cloudronName, program),
|
||||
text: render('oom_event.ejs', { cloudronName: mailConfig.cloudronName, program: program, context: context, format: 'text' })
|
||||
};
|
||||
@@ -508,24 +521,22 @@ function oomEvent(program, context) {
|
||||
|
||||
// this function bypasses the queue intentionally. it is also expected to work without the mailer module initialized
|
||||
// NOTE: crashnotifier should ideally be able to send mail when there is no db, however we need the 'from' address domain from the db
|
||||
function unexpectedExit(program, context, callback) {
|
||||
assert.strictEqual(typeof program, 'string');
|
||||
function unexpectedExit(mailTo, subject, context) {
|
||||
assert.strictEqual(typeof mailTo, 'string');
|
||||
assert.strictEqual(typeof subject, 'string');
|
||||
assert.strictEqual(typeof context, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (config.provider() !== 'caas') return callback(); // no way to get admins without db access
|
||||
|
||||
getMailConfig(function (error, mailConfig) {
|
||||
if (error) return debug('Error getting mail details:', error);
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig.notificationFrom,
|
||||
to: 'support@cloudron.io',
|
||||
subject: util.format('[%s] %s exited unexpectedly', mailConfig.cloudronName, program),
|
||||
text: render('unexpected_exit.ejs', { cloudronName: mailConfig.cloudronName, program: program, context: context, format: 'text' })
|
||||
to: mailTo,
|
||||
subject: `[${mailConfig.cloudronName}] ${subject}`,
|
||||
text: render('unexpected_exit.ejs', { cloudronName: mailConfig.cloudronName, subject: subject, context: context, format: 'text' })
|
||||
};
|
||||
|
||||
sendMails([ mailOptions ], callback);
|
||||
sendMails([ mailOptions ]);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
get: get,
|
||||
add: add,
|
||||
update: update,
|
||||
del: del,
|
||||
listByUserIdPaged: listByUserIdPaged
|
||||
};
|
||||
|
||||
let assert = require('assert'),
|
||||
database = require('./database.js'),
|
||||
DatabaseError = require('./databaseerror');
|
||||
|
||||
const NOTIFICATION_FIELDS = [ 'id', 'userId', 'eventId', 'title', 'message', 'action', 'creationTime', 'acknowledged' ];
|
||||
|
||||
function postProcess(result) {
|
||||
assert.strictEqual(typeof result, 'object');
|
||||
result.id = String(result.id);
|
||||
|
||||
// convert to boolean
|
||||
result.acknowledged = !!result.acknowledged;
|
||||
}
|
||||
|
||||
function add(notification, callback) {
|
||||
assert.strictEqual(typeof notification, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const query = 'INSERT INTO notifications (userId, eventId, title, message, action) VALUES (?, ?, ?, ?, ?)';
|
||||
const args = [ notification.userId, notification.eventId, notification.title, notification.message, notification.action ];
|
||||
|
||||
database.query(query, args, function (error, result) {
|
||||
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'no such eventlog entry'));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, String(result.insertId));
|
||||
});
|
||||
}
|
||||
|
||||
function update(id, data, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof data, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
let args = [ ];
|
||||
let fields = [ ];
|
||||
for (let k in data) {
|
||||
fields.push(k + ' = ?');
|
||||
args.push(data[k]);
|
||||
}
|
||||
args.push(id);
|
||||
|
||||
database.query('UPDATE notifications SET ' + fields.join(', ') + ' WHERE id = ?', args, function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function get(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + NOTIFICATION_FIELDS + ' FROM notifications WHERE id = ?', [ id ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
postProcess(result[0]);
|
||||
|
||||
callback(null, result[0]);
|
||||
});
|
||||
}
|
||||
|
||||
function del(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM notifications WHERE id = ?', [ id ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function listByUserIdPaged(userId, page, perPage, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof page, 'number');
|
||||
assert.strictEqual(typeof perPage, 'number');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var data = [ userId ];
|
||||
var query = 'SELECT ' + NOTIFICATION_FIELDS + ' FROM notifications WHERE userId=?';
|
||||
|
||||
query += ' ORDER BY creationTime DESC LIMIT ?,?';
|
||||
|
||||
data.push((page-1)*perPage);
|
||||
data.push(perPage);
|
||||
|
||||
database.query(query, data, function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
results.forEach(postProcess);
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
NotificationsError: NotificationsError,
|
||||
|
||||
add: add,
|
||||
get: get,
|
||||
ack: ack,
|
||||
getAllPaged: getAllPaged,
|
||||
|
||||
// specialized notifications
|
||||
userAdded: userAdded,
|
||||
userRemoved: userRemoved,
|
||||
adminChanged: adminChanged,
|
||||
oomEvent: oomEvent,
|
||||
appDied: appDied,
|
||||
processCrash: processCrash,
|
||||
apptaskCrash: apptaskCrash
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
config = require('./config.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:notifications'),
|
||||
mailer = require('./mailer.js'),
|
||||
notificationdb = require('./notificationdb.js'),
|
||||
safe = require('safetydance'),
|
||||
users = require('./users.js'),
|
||||
util = require('util');
|
||||
|
||||
function NotificationsError(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(NotificationsError, Error);
|
||||
NotificationsError.INTERNAL_ERROR = 'Internal Error';
|
||||
NotificationsError.NOT_FOUND = 'Not Found';
|
||||
|
||||
function add(userId, eventId, title, message, action, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof eventId, 'string');
|
||||
assert.strictEqual(typeof title, 'string');
|
||||
assert.strictEqual(typeof message, 'string');
|
||||
assert.strictEqual(typeof action, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('add: ', userId, title, action);
|
||||
|
||||
notificationdb.add({
|
||||
userId: userId,
|
||||
eventId: eventId,
|
||||
title: title,
|
||||
message: message,
|
||||
action: action
|
||||
}, function (error, result) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new NotificationsError(NotificationsError.NOT_FOUND, error.message));
|
||||
if (error) return callback(new NotificationsError(NotificationsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, { id: result });
|
||||
});
|
||||
}
|
||||
|
||||
function get(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
notificationdb.get(id, function (error, result) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new NotificationsError(NotificationsError.NOT_FOUND));
|
||||
if (error) return callback(new NotificationsError(NotificationsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function ack(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
notificationdb.update(id, { acknowledged: true }, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new NotificationsError(NotificationsError.NOT_FOUND));
|
||||
if (error) return callback(new NotificationsError(NotificationsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
// if acknowledged === null we return all, otherwise yes or no based on acknowledged as a boolean
|
||||
function getAllPaged(userId, acknowledged, page, perPage, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert(acknowledged === null || typeof acknowledged === 'boolean');
|
||||
assert.strictEqual(typeof page, 'number');
|
||||
assert.strictEqual(typeof perPage, 'number');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
notificationdb.listByUserIdPaged(userId, page, perPage, function (error, result) {
|
||||
if (error) return callback(new NotificationsError(NotificationsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (acknowledged === null) return callback(null, result);
|
||||
|
||||
callback(null, result.filter(function (r) { return r.acknowledged === acknowledged; }));
|
||||
});
|
||||
}
|
||||
|
||||
// Calls iterator with (admin, callback)
|
||||
function actionForAllAdmins(skippingUserIds, iterator, callback) {
|
||||
assert(Array.isArray(skippingUserIds));
|
||||
assert.strictEqual(typeof iterator, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
users.getAllAdmins(function (error, result) {
|
||||
if (error) return callback(new NotificationsError(NotificationsError.INTERNAL_ERROR, error));
|
||||
|
||||
// filter out users we want to skip (like the user who did the action or the user the action was performed on)
|
||||
result = result.filter(function (r) { return skippingUserIds.indexOf(r.id) === -1; });
|
||||
|
||||
async.each(result, iterator, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function userAdded(performedBy, eventId, user) {
|
||||
assert.strictEqual(typeof performedBy, 'string');
|
||||
assert.strictEqual(typeof eventId, 'string');
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
|
||||
actionForAllAdmins([ performedBy, user.id ], function (admin, callback) {
|
||||
mailer.userAdded(admin.email, user);
|
||||
add(admin.id, eventId, 'User added', `User ${user.fallbackEmail} was added`, '/#/users', callback);
|
||||
}, function (error) {
|
||||
if (error) console.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
function userRemoved(performedBy, eventId, user) {
|
||||
assert.strictEqual(typeof performedBy, 'string');
|
||||
assert.strictEqual(typeof eventId, 'string');
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
|
||||
actionForAllAdmins([ performedBy, user.id ], function (admin, callback) {
|
||||
mailer.userRemoved(admin.email, user);
|
||||
add(admin.id, eventId, 'User removed', `User ${user.username || user.email || user.fallbackEmail} was removed`, '/#/users', callback);
|
||||
}, function (error) {
|
||||
if (error) console.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
function adminChanged(performedBy, eventId, user) {
|
||||
assert.strictEqual(typeof performedBy, 'string');
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
|
||||
actionForAllAdmins([ performedBy, user.id ], function (admin, callback) {
|
||||
mailer.adminChanged(admin.email, user, user.admin);
|
||||
add(admin.id, eventId, 'Admin status change', `User ${user.username || user.email || user.fallbackEmail} ${user.admin ? 'is now an admin' : 'is no more an admin'}`, '/#/users', callback);
|
||||
}, function (error) {
|
||||
if (error) console.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
function oomEvent(eventId, program, context) {
|
||||
assert.strictEqual(typeof eventId, 'string');
|
||||
assert.strictEqual(typeof program, 'string');
|
||||
assert.strictEqual(typeof context, 'object');
|
||||
|
||||
// also send us a notification mail
|
||||
if (config.provider() === 'caas') mailer.oomEvent('support@cloudron.io', program, JSON.stringify(context, null, 4));
|
||||
|
||||
actionForAllAdmins([], function (admin, callback) {
|
||||
mailer.oomEvent(admin.email, program, JSON.stringify(context, null, 4));
|
||||
|
||||
var message;
|
||||
if (context.app) message = `The application ${context.app.manifest.title} with id ${context.app.id} ran out of memory.`;
|
||||
else message = `The container with id ${context.details.id} ran out of memory`;
|
||||
|
||||
add(admin.id, eventId, 'Process died out-of-memory', message, context.app ? '/#/apps' : '', callback);
|
||||
}, function (error) {
|
||||
if (error) console.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
function appDied(eventId, app) {
|
||||
assert.strictEqual(typeof eventId, 'string');
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
|
||||
// also send us a notification mail
|
||||
if (config.provider() === 'caas') mailer.appDied('support@cloudron.io', app);
|
||||
|
||||
actionForAllAdmins([], function (admin, callback) {
|
||||
mailer.appDied(admin.email, app);
|
||||
add(admin.id, eventId, `App ${app.fqdn} is down`, `The application ${app.manifest.title} installed at ${app.fqdn} is not responding.`, '/#/apps', callback);
|
||||
}, function (error) {
|
||||
if (error) console.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
function processCrash(eventId, processName, crashLogFile) {
|
||||
assert.strictEqual(typeof eventId, 'string');
|
||||
assert.strictEqual(typeof processName, 'string');
|
||||
assert.strictEqual(typeof crashLogFile, 'string');
|
||||
|
||||
var subject = `${processName} exited unexpectedly`;
|
||||
var crashLogs = safe.fs.readFileSync(crashLogFile, 'utf8') || `No logs found at ${crashLogFile}`;
|
||||
|
||||
// also send us a notification mail
|
||||
if (config.provider() === 'caas') mailer.unexpectedExit('support@cloudron.io', subject, crashLogs);
|
||||
|
||||
actionForAllAdmins([], function (admin, callback) {
|
||||
mailer.unexpectedExit(admin.email, subject, crashLogs);
|
||||
add(admin.id, eventId, subject, 'Detailed logs have been sent to your email address.', '/#/system', callback);
|
||||
}, function (error) {
|
||||
if (error) console.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
function apptaskCrash(eventId, appId, crashLogFile) {
|
||||
assert.strictEqual(typeof eventId, 'string');
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof crashLogFile, 'string');
|
||||
|
||||
var subject = `Apptask for ${appId} crashed`;
|
||||
var crashLogs = safe.fs.readFileSync(crashLogFile, 'utf8') || `No logs found at ${crashLogFile}`;
|
||||
|
||||
// also send us a notification mail
|
||||
if (config.provider() === 'caas') mailer.unexpectedExit('support@cloudron.io', subject, crashLogs);
|
||||
|
||||
actionForAllAdmins([], function (admin, callback) {
|
||||
mailer.unexpectedExit(admin.email, subject, crashLogs);
|
||||
add(admin.id, eventId, subject, 'Detailed logs have been sent to your email address.', '/#/system', callback);
|
||||
}, function (error) {
|
||||
if (error) console.error(error);
|
||||
});
|
||||
}
|
||||
+4
-2
@@ -7,9 +7,7 @@ var config = require('./config.js'),
|
||||
exports = module.exports = {
|
||||
CLOUDRON_DEFAULT_AVATAR_FILE: path.join(__dirname + '/../assets/avatar.png'),
|
||||
INFRA_VERSION_FILE: path.join(config.baseDir(), 'platformdata/INFRA_VERSION'),
|
||||
BACKUP_RESULT_FILE: path.join(config.baseDir(), 'platformdata/backup/result.txt'),
|
||||
|
||||
OLD_DATA_DIR: path.join(config.baseDir(), 'data'),
|
||||
PLATFORM_DATA_DIR: path.join(config.baseDir(), 'platformdata'),
|
||||
APPS_DATA_DIR: path.join(config.baseDir(), 'appsdata'),
|
||||
BOX_DATA_DIR: path.join(config.baseDir(), 'boxdata'),
|
||||
@@ -24,6 +22,7 @@ exports = module.exports = {
|
||||
BACKUP_INFO_DIR: path.join(config.baseDir(), 'platformdata/backup'),
|
||||
UPDATE_DIR: path.join(config.baseDir(), 'platformdata/update'),
|
||||
SNAPSHOT_INFO_FILE: path.join(config.baseDir(), 'platformdata/backup/snapshot-info.json'),
|
||||
DYNDNS_INFO_FILE: path.join(config.baseDir(), 'platformdata/dyndns-info.json'),
|
||||
|
||||
// this is not part of appdata because an icon may be set before install
|
||||
APP_ICONS_DIR: path.join(config.baseDir(), 'boxdata/appicons'),
|
||||
@@ -34,6 +33,9 @@ exports = module.exports = {
|
||||
UPDATE_CHECKER_FILE: path.join(config.baseDir(), 'boxdata/updatechecker.json'),
|
||||
|
||||
LOG_DIR: path.join(config.baseDir(), 'platformdata/logs'),
|
||||
TASKS_LOG_DIR: path.join(config.baseDir(), 'platformdata/logs/tasks'),
|
||||
|
||||
// this pattern is for the cloudron logs API route to work
|
||||
BACKUP_LOG_FILE: path.join(config.baseDir(), 'platformdata/logs/backup/app.log'),
|
||||
UPDATER_LOG_FILE: path.join(config.baseDir(), 'platformdata/logs/updater/app.log')
|
||||
};
|
||||
|
||||
+48
-24
@@ -16,7 +16,6 @@ var addons = require('./addons.js'),
|
||||
async = require('async'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:platform'),
|
||||
execSync = require('child_process').execSync,
|
||||
fs = require('fs'),
|
||||
graphs = require('./graphs.js'),
|
||||
infra = require('./infra_version.js'),
|
||||
@@ -25,6 +24,7 @@ var addons = require('./addons.js'),
|
||||
paths = require('./paths.js'),
|
||||
reverseProxy = require('./reverseproxy.js'),
|
||||
safe = require('safetydance'),
|
||||
settings = require('./settings.js'),
|
||||
shell = require('./shell.js'),
|
||||
taskmanager = require('./taskmanager.js'),
|
||||
_ = require('underscore');
|
||||
@@ -60,10 +60,10 @@ function start(callback) {
|
||||
|
||||
async.series([
|
||||
stopContainers.bind(null, existingInfra),
|
||||
graphs.startGraphite.bind(null, existingInfra),
|
||||
addons.startAddons.bind(null, existingInfra),
|
||||
pruneInfraImages,
|
||||
// mark app state before we start addons. this gives the db import logic a chance to mark an app as errored
|
||||
startApps.bind(null, existingInfra),
|
||||
graphs.startGraphite.bind(null, existingInfra),
|
||||
addons.startServices.bind(null, existingInfra),
|
||||
fs.writeFile.bind(fs, paths.INFRA_VERSION_FILE, JSON.stringify(infra, null, 4))
|
||||
], function (error) {
|
||||
if (error) return callback(error);
|
||||
@@ -77,43 +77,66 @@ function start(callback) {
|
||||
}
|
||||
|
||||
function stop(callback) {
|
||||
exports.events = null;
|
||||
taskmanager.pauseTasks(callback);
|
||||
}
|
||||
|
||||
function onPlatformReady() {
|
||||
debug('onPlatformReady: resuming task manager');
|
||||
debug('onPlatformReady: platform is ready');
|
||||
exports._isReady = true;
|
||||
taskmanager.resumeTasks();
|
||||
|
||||
applyPlatformConfig(NOOP_CALLBACK);
|
||||
pruneInfraImages(NOOP_CALLBACK);
|
||||
}
|
||||
|
||||
function applyPlatformConfig(callback) {
|
||||
// scale back db containers, if possible. this is retried because updating memory constraints can fail
|
||||
// with failed to write to memory.memsw.limit_in_bytes: write /sys/fs/cgroup/memory/docker/xx/memory.memsw.limit_in_bytes: device or resource busy
|
||||
|
||||
async.retry({ times: 10, interval: 5 * 60 * 1000 }, function (retryCallback) {
|
||||
settings.getPlatformConfig(function (error, platformConfig) {
|
||||
if (error) return retryCallback(error);
|
||||
|
||||
addons.updateServiceConfig(platformConfig, function (error) {
|
||||
if (error) debug('Error updating services. Will rety in 5 minutes', platformConfig, error);
|
||||
|
||||
retryCallback(error);
|
||||
});
|
||||
});
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function pruneInfraImages(callback) {
|
||||
debug('pruneInfraImages: checking existing images');
|
||||
|
||||
// cannot blindly remove all unused images since redis image may not be used
|
||||
let images = infra.baseImages.concat(Object.keys(infra.images).map(function (addon) { return infra.images[addon]; }));
|
||||
const images = infra.baseImages.concat(Object.keys(infra.images).map(function (addon) { return infra.images[addon]; }));
|
||||
|
||||
async.eachSeries(images, function (image, iteratorCallback) {
|
||||
let output = safe.child_process.execSync(`docker images --digests ${image.repo} --format "{{.ID}} {{.Repository}}:{{.Tag}}@{{.Digest}}"`, { encoding: 'utf8' });
|
||||
if (output === null) return iteratorCallback(safe.error);
|
||||
|
||||
for (let image of images) {
|
||||
let output = execSync(`docker images --digests ${image.repo} --format "{{.ID}} {{.Repository}}:{{.Tag}}@{{.Digest}}"`, { encoding: 'utf8' });
|
||||
let lines = output.trim().split('\n');
|
||||
for (let line of lines) {
|
||||
if (!line) continue;
|
||||
let parts = line.split(' '); // [ ID, Repo:Tag@Digest ]
|
||||
if (image.tag === parts[1]) continue; // keep
|
||||
debug(`pruneInfraImages: removing unused image of ${image.repo}: ${line}`);
|
||||
shell.execSync('pruneInfraImages', `docker rmi ${parts[0]}`);
|
||||
}
|
||||
}
|
||||
|
||||
callback();
|
||||
shell.exec('pruneInfraImages', `docker rmi ${parts[0]}`, iteratorCallback);
|
||||
}
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function stopContainers(existingInfra, callback) {
|
||||
// always stop addons to restart them on any infra change, regardless of minor or major update
|
||||
if (existingInfra.version !== infra.version) {
|
||||
// TODO: only nuke containers with isCloudronManaged=true
|
||||
debug('stopping all containers for infra upgrade');
|
||||
shell.execSync('stopContainers', 'docker ps -qa | xargs --no-run-if-empty docker stop');
|
||||
shell.execSync('stopContainers', 'docker ps -qa | xargs --no-run-if-empty docker rm -f');
|
||||
async.series([
|
||||
shell.exec.bind(null, 'stopContainers', 'docker ps -qa | xargs --no-run-if-empty docker stop'),
|
||||
shell.exec.bind(null, 'stopContainers', 'docker ps -qa | xargs --no-run-if-empty docker rm -f')
|
||||
], callback);
|
||||
} else {
|
||||
assert(typeof infra.images, 'object');
|
||||
var changedAddons = [ ];
|
||||
@@ -124,15 +147,15 @@ function stopContainers(existingInfra, callback) {
|
||||
debug('stopContainer: stopping addons for incremental infra update: %j', changedAddons);
|
||||
let filterArg = changedAddons.map(function (c) { return `--filter 'name=${c}'`; }).join(' '); // name=c matches *c*. required for redis-{appid}
|
||||
// ignore error if container not found (and fail later) so that this code works across restarts
|
||||
shell.execSync('stopContainers', `docker ps -qa ${filterArg} | xargs --no-run-if-empty docker stop || true`);
|
||||
shell.execSync('stopContainers', `docker ps -qa ${filterArg} | xargs --no-run-if-empty docker rm -f || true`);
|
||||
async.series([
|
||||
shell.exec.bind(null, 'stopContainers', `docker ps -qa ${filterArg} | xargs --no-run-if-empty docker stop || true`),
|
||||
shell.exec.bind(null, 'stopContainers', `docker ps -qa ${filterArg} | xargs --no-run-if-empty docker rm -f || true`)
|
||||
], callback);
|
||||
}
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
function startApps(existingInfra, callback) {
|
||||
if (existingInfra.version === 'none') {
|
||||
if (existingInfra.version === 'none') { // cloudron is being restored from backup
|
||||
debug('startApps: restoring installed apps');
|
||||
apps.restoreInstalledApps(callback);
|
||||
} else if (existingInfra.version !== infra.version) {
|
||||
@@ -145,12 +168,13 @@ function startApps(existingInfra, callback) {
|
||||
}
|
||||
}
|
||||
|
||||
function handleCertChanged(cn) {
|
||||
function handleCertChanged(cn, callback) {
|
||||
assert.strictEqual(typeof cn, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('handleCertChanged', cn);
|
||||
|
||||
if (cn === '*.' + config.adminDomain() || cn === config.adminFqdn()) {
|
||||
mail.startMail(NOOP_CALLBACK);
|
||||
}
|
||||
if (cn === '*.' + config.adminDomain() || cn === config.adminFqdn()) return mail.startMail(callback);
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
set: set,
|
||||
setDetail: setDetail,
|
||||
clear: clear,
|
||||
getAll: getAll,
|
||||
|
||||
UPDATE: 'update',
|
||||
BACKUP: 'backup',
|
||||
MIGRATE: 'migrate'
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
debug = require('debug')('box:progress');
|
||||
|
||||
// if progress.update or progress.backup are object, they will contain 'percent' and 'message' properties
|
||||
// otherwise no such operation is currently ongoing
|
||||
var progress = {
|
||||
update: null,
|
||||
backup: null,
|
||||
migrate: null
|
||||
};
|
||||
|
||||
// We use -1 for percentage to indicate errors
|
||||
function set(tag, percent, message) {
|
||||
assert.strictEqual(typeof tag, 'string');
|
||||
assert.strictEqual(typeof percent, 'number');
|
||||
assert.strictEqual(typeof message, 'string');
|
||||
|
||||
progress[tag] = {
|
||||
percent: percent,
|
||||
message: message,
|
||||
detail: ''
|
||||
};
|
||||
|
||||
debug('%s: %s %s', tag, percent, message);
|
||||
}
|
||||
|
||||
function setDetail(tag, detail) {
|
||||
assert.strictEqual(typeof tag, 'string');
|
||||
assert.strictEqual(typeof detail, 'string');
|
||||
|
||||
if (!progress[tag]) return debug('[%s] %s', tag, detail);
|
||||
|
||||
progress[tag].detail = detail;
|
||||
}
|
||||
|
||||
function clear(tag) {
|
||||
assert.strictEqual(typeof tag, 'string');
|
||||
|
||||
progress[tag] = null;
|
||||
|
||||
debug('clearing %s', tag);
|
||||
}
|
||||
|
||||
function getAll() {
|
||||
return progress;
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
setup: setup,
|
||||
restore: restore,
|
||||
activate: activate,
|
||||
getStatus: getStatus,
|
||||
|
||||
ProvisionError: ProvisionError
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
backups = require('./backups.js'),
|
||||
BackupsError = require('./backups.js').BackupsError,
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
clients = require('./clients.js'),
|
||||
cloudron = require('./cloudron.js'),
|
||||
debug = require('debug')('box:provision'),
|
||||
domains = require('./domains.js'),
|
||||
DomainsError = domains.DomainsError,
|
||||
eventlog = require('./eventlog.js'),
|
||||
mail = require('./mail.js'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
settingsdb = require('./settingsdb.js'),
|
||||
settings = require('./settings.js'),
|
||||
superagent = require('superagent'),
|
||||
users = require('./users.js'),
|
||||
UsersError = users.UsersError,
|
||||
tld = require('tldjs'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
// we cannot use tasks since the tasks table gets overwritten when db is imported
|
||||
let gProvisionStatus = {
|
||||
setup: {
|
||||
active: false,
|
||||
message: '',
|
||||
errorMessage: null
|
||||
},
|
||||
restore: {
|
||||
active: false,
|
||||
message: '',
|
||||
errorMessage: null
|
||||
}
|
||||
};
|
||||
|
||||
function ProvisionError(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(ProvisionError, Error);
|
||||
ProvisionError.BAD_FIELD = 'Field error';
|
||||
ProvisionError.BAD_STATE = 'Bad State';
|
||||
ProvisionError.ALREADY_SETUP = 'Already Setup';
|
||||
ProvisionError.INTERNAL_ERROR = 'Internal Error';
|
||||
ProvisionError.EXTERNAL_ERROR = 'External Error';
|
||||
ProvisionError.ALREADY_PROVISIONED = 'Already Provisioned';
|
||||
|
||||
function setProgress(task, message, callback) {
|
||||
debug(`setProgress: ${task} - ${message}`);
|
||||
gProvisionStatus[task].message = message;
|
||||
callback();
|
||||
}
|
||||
|
||||
function autoprovision(autoconf, callback) {
|
||||
assert.strictEqual(typeof autoconf, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
async.eachSeries(Object.keys(autoconf), function (key, iteratorDone) {
|
||||
debug(`autoprovision: ${key}`);
|
||||
|
||||
switch (key) {
|
||||
case 'appstoreConfig':
|
||||
if (config.provider() === 'caas') { // skip registration
|
||||
settingsdb.set(settings.APPSTORE_CONFIG_KEY, JSON.stringify(autoconf[key]), iteratorDone);
|
||||
} else { // register cloudron
|
||||
settings.setAppstoreConfig(autoconf[key], iteratorDone);
|
||||
}
|
||||
break;
|
||||
case 'caasConfig':
|
||||
settingsdb.set(settings.CAAS_CONFIG_KEY, JSON.stringify(autoconf[key]), iteratorDone);
|
||||
break;
|
||||
case 'backupConfig':
|
||||
settings.setBackupConfig(autoconf[key], iteratorDone);
|
||||
break;
|
||||
default:
|
||||
debug(`autoprovision: ${key} ignored`);
|
||||
return iteratorDone();
|
||||
}
|
||||
}, function (error) {
|
||||
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function unprovision(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('unprovision');
|
||||
|
||||
config.setAdminDomain('');
|
||||
config.setAdminFqdn('');
|
||||
config.setAdminLocation(constants.ADMIN_LOCATION);
|
||||
|
||||
// TODO: also cancel any existing configureWebadmin task
|
||||
async.series([
|
||||
mail.clearDomains,
|
||||
domains.clear
|
||||
], callback);
|
||||
}
|
||||
|
||||
function setup(dnsConfig, autoconf, auditSource, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof autoconf, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (gProvisionStatus.setup.active || gProvisionStatus.restore.active) return callback(new ProvisionError(ProvisionError.BAD_STATE, 'Already setting up or restoring'));
|
||||
|
||||
gProvisionStatus.setup = { active: true, errorMessage: '', message: 'Adding domain' };
|
||||
|
||||
function done(error) {
|
||||
gProvisionStatus.setup.active = false;
|
||||
gProvisionStatus.setup.errorMessage = error ? error.message : '';
|
||||
callback(error);
|
||||
}
|
||||
|
||||
users.isActivated(function (error, activated) {
|
||||
if (error) return done(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
|
||||
if (activated) return done(new ProvisionError(ProvisionError.ALREADY_SETUP));
|
||||
|
||||
unprovision(function (error) {
|
||||
if (error) return done(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
|
||||
|
||||
const domain = dnsConfig.domain.toLowerCase();
|
||||
const zoneName = dnsConfig.zoneName ? dnsConfig.zoneName : (tld.getDomain(domain) || domain);
|
||||
|
||||
debug(`provision: Setting up Cloudron with domain ${domain} and zone ${zoneName}`);
|
||||
|
||||
let data = {
|
||||
zoneName: zoneName,
|
||||
provider: dnsConfig.provider,
|
||||
config: dnsConfig.config,
|
||||
fallbackCertificate: dnsConfig.fallbackCertificate || null,
|
||||
tlsConfig: dnsConfig.tlsConfig || { provider: 'letsencrypt-prod' }
|
||||
};
|
||||
|
||||
domains.add(domain, data, auditSource, function (error) {
|
||||
if (error && error.reason === DomainsError.BAD_FIELD) return done(new ProvisionError(ProvisionError.BAD_FIELD, error.message));
|
||||
if (error && error.reason === DomainsError.ALREADY_EXISTS) return done(new ProvisionError(ProvisionError.BAD_FIELD, error.message));
|
||||
if (error) return done(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(); // now that args are validated run the task in the background
|
||||
|
||||
async.series([
|
||||
domains.prepareDashboardDomain.bind(null, domain, auditSource, (progress) => setProgress('setup', progress.message, NOOP_CALLBACK)),
|
||||
cloudron.setDashboardDomain.bind(null, domain), // this sets up the config.fqdn()
|
||||
mail.addDomain.bind(null, domain), // this relies on config.mailFqdn()
|
||||
setProgress.bind(null, 'setup', 'Applying auto-configuration'),
|
||||
autoprovision.bind(null, autoconf),
|
||||
setProgress.bind(null, 'setup', 'Done'),
|
||||
eventlog.add.bind(null, eventlog.ACTION_PROVISION, auditSource, { })
|
||||
], function (error) {
|
||||
gProvisionStatus.setup.active = false;
|
||||
gProvisionStatus.setup.errorMessage = error ? error.message : '';
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setTimeZone(ip, callback) {
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('setTimeZone ip:%s', ip);
|
||||
|
||||
superagent.get('https://geolocation.cloudron.io/json').query({ ip: ip }).timeout(10 * 1000).end(function (error, result) {
|
||||
if ((error && !error.response) || result.statusCode !== 200) {
|
||||
debug('Failed to get geo location: %s', error.message);
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
var timezone = safe.query(result.body, 'location.time_zone');
|
||||
|
||||
if (!timezone || typeof timezone !== 'string') {
|
||||
debug('No timezone in geoip response : %j', result.body);
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
debug('Setting timezone to ', timezone);
|
||||
|
||||
settings.setTimeZone(timezone, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function activate(username, password, email, displayName, ip, auditSource, callback) {
|
||||
assert.strictEqual(typeof username, 'string');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
assert.strictEqual(typeof displayName, 'string');
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('activating user:%s email:%s', username, email);
|
||||
|
||||
setTimeZone(ip, function () { }); // TODO: get this from user. note that timezone is detected based on the browser location and not the cloudron region
|
||||
|
||||
users.createOwner(username, password, email, displayName, auditSource, function (error, userObject) {
|
||||
if (error && error.reason === UsersError.ALREADY_EXISTS) return callback(new ProvisionError(ProvisionError.ALREADY_PROVISIONED, 'Already activated'));
|
||||
if (error && error.reason === UsersError.BAD_FIELD) return callback(new ProvisionError(ProvisionError.BAD_FIELD, error.message));
|
||||
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
|
||||
|
||||
clients.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
|
||||
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
|
||||
|
||||
eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, { });
|
||||
|
||||
callback(null, {
|
||||
userId: userObject.id,
|
||||
token: result.accessToken,
|
||||
expires: result.expires
|
||||
});
|
||||
|
||||
setImmediate(cloudron.onActivated.bind(null, NOOP_CALLBACK)); // hack for now to not block the above http response
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function restore(backupConfig, backupId, version, autoconf, auditSource, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
assert.strictEqual(typeof version, 'string');
|
||||
assert.strictEqual(typeof autoconf, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!semver.valid(version)) return callback(new ProvisionError(ProvisionError.BAD_STATE, 'version is not a valid semver'));
|
||||
if (semver.major(config.version()) !== semver.major(version) || semver.minor(config.version()) !== semver.minor(version)) return callback(new ProvisionError(ProvisionError.BAD_STATE, `Run cloudron-setup with --version ${version} to restore from this backup`));
|
||||
|
||||
if (gProvisionStatus.setup.active || gProvisionStatus.restore.active) return callback(new ProvisionError(ProvisionError.BAD_STATE, 'Already setting up or restoring'));
|
||||
|
||||
gProvisionStatus.restore = { active: true, errorMessage: '', message: 'Testing backup config' };
|
||||
|
||||
function done(error) {
|
||||
gProvisionStatus.restore.active = false;
|
||||
gProvisionStatus.restore.errorMessage = error ? error.message : '';
|
||||
callback(error);
|
||||
}
|
||||
|
||||
users.isActivated(function (error, activated) {
|
||||
if (error) return done(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
|
||||
if (activated) return done(new ProvisionError(ProvisionError.ALREADY_PROVISIONED, 'Already activated'));
|
||||
|
||||
backups.testConfig(backupConfig, function (error) {
|
||||
if (error && error.reason === BackupsError.BAD_FIELD) return done(new ProvisionError(ProvisionError.BAD_FIELD, error.message));
|
||||
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return done(new ProvisionError(ProvisionError.EXTERNAL_ERROR, error.message));
|
||||
if (error) return done(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
|
||||
|
||||
debug(`restore: restoring from ${backupId} from provider ${backupConfig.provider} with format ${backupConfig.format}`);
|
||||
|
||||
callback(); // now that the fields are validated, continue task in the background
|
||||
|
||||
async.series([
|
||||
setProgress.bind(null, 'restore', 'Downloading backup'),
|
||||
backups.restore.bind(null, backupConfig, backupId, (progress) => setProgress('restore', progress.message, NOOP_CALLBACK)),
|
||||
setProgress.bind(null, 'restore', 'Applying auto-configuration'),
|
||||
autoprovision.bind(null, autoconf),
|
||||
// currently, our suggested restore flow is after a dnsSetup. The dnSetup creates DKIM keys and updates the DNS
|
||||
// for this reason, we have to re-setup DNS after a restore so it has DKIm from the backup
|
||||
// Once we have a 100% IP based restore, we can skip this
|
||||
mail.setDnsRecords.bind(null, config.adminDomain()),
|
||||
eventlog.add.bind(null, eventlog.ACTION_RESTORE, auditSource, { backupId }),
|
||||
], function (error) {
|
||||
gProvisionStatus.restore.active = false;
|
||||
gProvisionStatus.restore.errorMessage = error ? error.message : '';
|
||||
|
||||
if (!error) cloudron.onActivated(NOOP_CALLBACK);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getStatus(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
users.isActivated(function (error, activated) {
|
||||
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
|
||||
|
||||
settings.getCloudronName(function (error, cloudronName) {
|
||||
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, _.extend({
|
||||
version: config.version(),
|
||||
apiServerOrigin: config.apiServerOrigin(), // used by CaaS tool
|
||||
provider: config.provider(),
|
||||
cloudronName: cloudronName,
|
||||
adminFqdn: config.adminDomain() ? config.adminFqdn() : null,
|
||||
activated: activated,
|
||||
edition: config.edition()
|
||||
}, gProvisionStatus));
|
||||
});
|
||||
});
|
||||
}
|
||||
+164
-115
@@ -12,16 +12,19 @@ exports = module.exports = {
|
||||
validateCertificate: validateCertificate,
|
||||
|
||||
getCertificate: getCertificate,
|
||||
ensureCertificate: ensureCertificate,
|
||||
|
||||
renewAll: renewAll,
|
||||
renewCerts: renewCerts,
|
||||
|
||||
// the 'configure' functions always ensure a certificate
|
||||
configureDefaultServer: configureDefaultServer,
|
||||
|
||||
configureAdmin: configureAdmin,
|
||||
configureApp: configureApp,
|
||||
unconfigureApp: unconfigureApp,
|
||||
|
||||
writeAdminConfig: writeAdminConfig,
|
||||
|
||||
reload: reload,
|
||||
removeAppConfigs: removeAppConfigs,
|
||||
|
||||
@@ -54,9 +57,8 @@ var acme2 = require('./cert/acme2.js'),
|
||||
users = require('./users.js'),
|
||||
util = require('util');
|
||||
|
||||
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/../setup/start/nginx/appconfig.ejs', { encoding: 'utf8' }),
|
||||
RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh'),
|
||||
NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/appconfig.ejs', { encoding: 'utf8' }),
|
||||
RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh');
|
||||
|
||||
function ReverseProxyError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
@@ -81,33 +83,29 @@ ReverseProxyError.INTERNAL_ERROR = 'Internal Error';
|
||||
ReverseProxyError.INVALID_CERT = 'Invalid certificate';
|
||||
ReverseProxyError.NOT_FOUND = 'Not Found';
|
||||
|
||||
function getCertApi(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
function getCertApi(domainObject, callback) {
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
domains.get(domain, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
if (domainObject.tlsConfig.provider === 'fallback') return callback(null, fallback, { fallback: true });
|
||||
|
||||
if (result.tlsConfig.provider === 'fallback') return callback(null, fallback, { fallback: true });
|
||||
var api = domainObject.tlsConfig.provider === 'caas' ? caas : acme2;
|
||||
|
||||
var api = result.tlsConfig.provider === 'caas' ? caas : acme2;
|
||||
var options = { prod: false, performHttpAuthorization: false, wildcard: false, email: '' };
|
||||
if (domainObject.tlsConfig.provider !== 'caas') { // matches 'le-prod' or 'letsencrypt-prod'
|
||||
options.prod = domainObject.tlsConfig.provider.match(/.*-prod/) !== null;
|
||||
options.performHttpAuthorization = domainObject.provider.match(/noop|manual|wildcard/) !== null;
|
||||
options.wildcard = !!domainObject.tlsConfig.wildcard;
|
||||
}
|
||||
|
||||
var options = { prod: false, performHttpAuthorization: false, wildcard: false, email: '' };
|
||||
if (result.tlsConfig.provider !== 'caas') { // matches 'le-prod' or 'letsencrypt-prod'
|
||||
options.prod = result.tlsConfig.provider.match(/.*-prod/) !== null;
|
||||
options.performHttpAuthorization = result.provider.match(/noop|manual|wildcard/) !== null;
|
||||
options.wildcard = !!result.tlsConfig.wildcard;
|
||||
}
|
||||
// registering user with an email requires A or MX record (https://github.com/letsencrypt/boulder/issues/1197)
|
||||
// we cannot use admin@fqdn because the user might not have set it up.
|
||||
// we simply update the account with the latest email we have each time when getting letsencrypt certs
|
||||
// https://github.com/ietf-wg-acme/acme/issues/30
|
||||
users.getOwner(function (error, owner) {
|
||||
options.email = error ? 'support@cloudron.io' : (owner.fallbackEmail || owner.email); // can error if not activated yet
|
||||
|
||||
// registering user with an email requires A or MX record (https://github.com/letsencrypt/boulder/issues/1197)
|
||||
// we cannot use admin@fqdn because the user might not have set it up.
|
||||
// we simply update the account with the latest email we have each time when getting letsencrypt certs
|
||||
// https://github.com/ietf-wg-acme/acme/issues/30
|
||||
users.getOwner(function (error, owner) {
|
||||
options.email = error ? 'support@cloudron.io' : (owner.fallbackEmail || owner.email); // can error if not activated yet
|
||||
|
||||
callback(null, api, options);
|
||||
});
|
||||
callback(null, api, options);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -119,32 +117,39 @@ function isExpiringSync(certFilePath, hours) {
|
||||
|
||||
var result = safe.child_process.spawnSync('/usr/bin/openssl', [ 'x509', '-checkend', String(60 * 60 * hours), '-in', certFilePath ]);
|
||||
|
||||
if (!result) return 3; // some error
|
||||
|
||||
debug('isExpiringSync: %s %s %s', certFilePath, result.stdout.toString('utf8').trim(), result.status);
|
||||
|
||||
return result.status === 1; // 1 - expired 0 - not expired
|
||||
}
|
||||
|
||||
function providerMatchesSync(certFilePath, apiOptions) {
|
||||
// checks if the certificate matches the options provided by user (like wildcard, le-staging etc)
|
||||
function providerMatchesSync(domainObject, certFilePath, apiOptions) {
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert.strictEqual(typeof certFilePath, 'string');
|
||||
assert.strictEqual(typeof apiOptions, 'object');
|
||||
|
||||
if (!fs.existsSync(certFilePath)) return false; // not found
|
||||
|
||||
if (apiOptions.fallback) {
|
||||
return certFilePath.includes('.host.cert');
|
||||
}
|
||||
if (apiOptions.fallback) return certFilePath.includes('.host.cert');
|
||||
|
||||
const subjectAndIssuer = safe.child_process.execSync(`/usr/bin/openssl x509 -noout -subject -issuer -in "${certFilePath}"`, { encoding: 'utf8' });
|
||||
if (!subjectAndIssuer) return false; // something bad happenned
|
||||
|
||||
const isWildcardCert = subjectAndIssuer.match(/^subject=(.*)$/m)[1].includes('*');
|
||||
const isLetsEncryptProd = subjectAndIssuer.match(/^issuer=(.*)$/m)[1].includes('Let\'s Encrypt Authority');
|
||||
const subject = subjectAndIssuer.match(/^subject=(.*)$/m)[1];
|
||||
const domain = subject.substr(subject.indexOf('=') + 1).trim(); // subject can be /CN=, CN=, CN = and other forms
|
||||
const issuer = subjectAndIssuer.match(/^issuer=(.*)$/m)[1];
|
||||
const isWildcardCert = domain.includes('*');
|
||||
const isLetsEncryptProd = issuer.includes('Let\'s Encrypt Authority');
|
||||
|
||||
const mismatch = ((apiOptions.wildcard && !isWildcardCert)
|
||||
|| (!apiOptions.wildcard && isWildcardCert)
|
||||
|| (apiOptions.prod && !isLetsEncryptProd)
|
||||
|| (!apiOptions.prod && isLetsEncryptProd));
|
||||
const issuerMismatch = (apiOptions.prod && !isLetsEncryptProd) || (!apiOptions.prod && isLetsEncryptProd);
|
||||
// bare domain is not part of wildcard SAN
|
||||
const wildcardMismatch = (domain !== domainObject.domain) && (apiOptions.wildcard && !isWildcardCert) || (!apiOptions.wildcard && isWildcardCert);
|
||||
|
||||
debug(`providerMatchesSync: ${certFilePath} ${subjectAndIssuer.trim().replace('\n', ' ')} wildcard=${isWildcardCert}/${apiOptions.wildcard} prod=${isLetsEncryptProd}/${apiOptions.prod} match=${!mismatch}`);
|
||||
const mismatch = issuerMismatch || wildcardMismatch;
|
||||
|
||||
debug(`providerMatchesSync: ${certFilePath} subject=${subject} domain=${domain} issuer=${issuer} wildcard=${isWildcardCert}/${apiOptions.wildcard} prod=${isLetsEncryptProd}/${apiOptions.prod} match=${!mismatch}`);
|
||||
|
||||
return !mismatch;
|
||||
}
|
||||
@@ -166,13 +171,17 @@ function validateCertificate(location, domainObject, certificate) {
|
||||
const fqdn = domains.fqdn(location, domainObject);
|
||||
|
||||
var result = safe.child_process.execSync(`openssl x509 -noout -checkhost "${fqdn}"`, { encoding: 'utf8', input: cert });
|
||||
if (!result) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'Unable to get certificate subject.');
|
||||
if (result === null) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'Unable to get certificate subject:' + safe.error.message);
|
||||
|
||||
if (result.indexOf('does match certificate') === -1) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, `Certificate is not valid for this domain. Expecting ${fqdn}`);
|
||||
|
||||
// http://httpd.apache.org/docs/2.0/ssl/ssl_faq.html#verify
|
||||
var certModulus = safe.child_process.execSync('openssl x509 -noout -modulus', { encoding: 'utf8', input: cert });
|
||||
if (certModulus === null) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, `Unable to get cert modulus: ${safe.error.message}`);
|
||||
|
||||
var keyModulus = safe.child_process.execSync('openssl rsa -noout -modulus', { encoding: 'utf8', input: key });
|
||||
if (keyModulus === null) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, `Unable to get key modulus: ${safe.error.message}`);
|
||||
|
||||
if (certModulus !== keyModulus) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'Key does not match the certificate.');
|
||||
|
||||
// check expiration
|
||||
@@ -185,7 +194,7 @@ function validateCertificate(location, domainObject, certificate) {
|
||||
function reload(callback) {
|
||||
if (process.env.BOX_ENV === 'test') return callback();
|
||||
|
||||
shell.sudo('reload', [ RELOAD_NGINX_CMD ], callback);
|
||||
shell.sudo('reload', [ RELOAD_NGINX_CMD ], {}, callback);
|
||||
}
|
||||
|
||||
function generateFallbackCertificateSync(domainObject) {
|
||||
@@ -236,12 +245,14 @@ function setFallbackCertificate(domain, fallback, callback) {
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.key`), fallback.key)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
|
||||
}
|
||||
|
||||
platform.handleCertChanged('*.' + domain);
|
||||
|
||||
reload(function (error) {
|
||||
platform.handleCertChanged('*.' + domain, function (error) {
|
||||
if (error) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null);
|
||||
reload(function (error) {
|
||||
if (error) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -279,9 +290,9 @@ function setAppCertificateSync(location, domainObject, certificate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function getCertificateByHostname(hostname, domain, callback) {
|
||||
function getCertificateByHostname(hostname, domainObject, callback) {
|
||||
assert.strictEqual(typeof hostname, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
let certFilePath = path.join(paths.APP_CERTS_DIR, `${hostname}.user.cert`);
|
||||
@@ -289,34 +300,35 @@ function getCertificateByHostname(hostname, domain, callback) {
|
||||
|
||||
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
|
||||
|
||||
if (hostname !== domainObject.domain && domainObject.tlsConfig.wildcard) { // bare domain is not part of wildcard SAN
|
||||
let certName = domains.makeWildcard(hostname).replace('*.', '_.');
|
||||
certFilePath = path.join(paths.APP_CERTS_DIR, `${certName}.cert`);
|
||||
keyFilePath = path.join(paths.APP_CERTS_DIR, `${certName}.key`);
|
||||
|
||||
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
|
||||
} else {
|
||||
certFilePath = path.join(paths.APP_CERTS_DIR, `${hostname}.cert`);
|
||||
keyFilePath = path.join(paths.APP_CERTS_DIR, `${hostname}.key`);
|
||||
|
||||
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
|
||||
}
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function getCertificate(fqdn, domain, callback) {
|
||||
assert.strictEqual(typeof fqdn, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
domains.get(domain, function (error, domainObject) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (hostname !== domain && domainObject.tlsConfig.wildcard) { // bare domain is not part of wildcard SAN
|
||||
let certName = domains.makeWildcard(hostname).replace('*.', '_.');
|
||||
certFilePath = path.join(paths.APP_CERTS_DIR, `${certName}.cert`);
|
||||
keyFilePath = path.join(paths.APP_CERTS_DIR, `${certName}.key`);
|
||||
getCertificateByHostname(fqdn, domainObject, function (error, result) {
|
||||
if (error || result) return callback(error, result);
|
||||
|
||||
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
|
||||
} else {
|
||||
certFilePath = path.join(paths.APP_CERTS_DIR, `${hostname}.cert`);
|
||||
keyFilePath = path.join(paths.APP_CERTS_DIR, `${hostname}.key`);
|
||||
|
||||
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
|
||||
}
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function getCertificate(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getCertificateByHostname(app.fqdn, app.domain, function (error, result) {
|
||||
if (error || result) return callback(error, result);
|
||||
|
||||
return getFallbackCertificate(app.domain, callback);
|
||||
return getFallbackCertificate(domain, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -326,42 +338,46 @@ function ensureCertificate(vhost, domain, auditSource, callback) {
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getCertApi(domain, function (error, api, apiOptions) {
|
||||
domains.get(domain, function (error, domainObject) {
|
||||
if (error) return callback(error);
|
||||
|
||||
getCertificateByHostname(vhost, domain, function (error, result) {
|
||||
if (result) {
|
||||
debug(`ensureCertificate: ${vhost} certificate already exists at ${result.keyFilePath}`);
|
||||
getCertApi(domainObject, function (error, api, apiOptions) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (result.certFilePath.endsWith('.user.cert')) return callback(null, result); // user certs cannot be renewed
|
||||
if (!isExpiringSync(result.certFilePath, 24 * 30) && providerMatchesSync(result.certFilePath, apiOptions)) return callback(null, result);
|
||||
debug(`ensureCertificate: ${vhost} cert require renewal`);
|
||||
} else {
|
||||
debug(`ensureCertificate: ${vhost} cert does not exist`);
|
||||
}
|
||||
getCertificateByHostname(vhost, domainObject, function (error, currentBundle) {
|
||||
if (currentBundle) {
|
||||
debug(`ensureCertificate: ${vhost} certificate already exists at ${currentBundle.keyFilePath}`);
|
||||
|
||||
debug('ensureCertificate: getting certificate for %s with options %j', vhost, apiOptions);
|
||||
|
||||
api.getCertificate(vhost, domain, apiOptions, function (error, certFilePath, keyFilePath) {
|
||||
var errorMessage = error ? error.message : '';
|
||||
|
||||
if (error) {
|
||||
debug('ensureCertificate: could not get certificate. using fallback certs', error);
|
||||
mailer.certificateRenewalError(vhost, errorMessage);
|
||||
if (currentBundle.certFilePath.endsWith('.user.cert')) return callback(null, currentBundle); // user certs cannot be renewed
|
||||
if (!isExpiringSync(currentBundle.certFilePath, 24 * 30) && providerMatchesSync(domainObject, currentBundle.certFilePath, apiOptions)) return callback(null, currentBundle);
|
||||
debug(`ensureCertificate: ${vhost} cert require renewal`);
|
||||
} else {
|
||||
debug(`ensureCertificate: ${vhost} cert does not exist`);
|
||||
}
|
||||
|
||||
eventlog.add(eventlog.ACTION_CERTIFICATE_RENEWAL, auditSource, { domain: vhost, errorMessage: errorMessage });
|
||||
debug('ensureCertificate: getting certificate for %s with options %j', vhost, apiOptions);
|
||||
|
||||
// if no cert was returned use fallback. the fallback/caas provider will not provide any for example
|
||||
if (!certFilePath || !keyFilePath) return getFallbackCertificate(domain, callback);
|
||||
api.getCertificate(vhost, domain, apiOptions, function (error, certFilePath, keyFilePath) {
|
||||
var errorMessage = error ? error.message : '';
|
||||
|
||||
callback(null, { certFilePath, keyFilePath, type: 'new-le' });
|
||||
if (error) {
|
||||
debug('ensureCertificate: could not get certificate. using fallback certs', error);
|
||||
mailer.certificateRenewalError(vhost, errorMessage);
|
||||
}
|
||||
|
||||
eventlog.add(currentBundle ? eventlog.ACTION_CERTIFICATE_RENEWAL : eventlog.ACTION_CERTIFICATE_NEW, auditSource, { domain: vhost, errorMessage: errorMessage });
|
||||
|
||||
// if no cert was returned use fallback. the fallback/caas provider will not provide any for example
|
||||
if (!certFilePath || !keyFilePath) return getFallbackCertificate(domain, callback);
|
||||
|
||||
callback(null, { certFilePath, keyFilePath, type: 'new-le' });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function writeAdminConfig(bundle, configFileName, vhost, callback) {
|
||||
function writeAdminNginxConfig(bundle, configFileName, vhost, callback) {
|
||||
assert.strictEqual(typeof bundle, 'object');
|
||||
assert.strictEqual(typeof configFileName, 'string');
|
||||
assert.strictEqual(typeof vhost, 'string');
|
||||
@@ -383,21 +399,47 @@ function writeAdminConfig(bundle, configFileName, vhost, callback) {
|
||||
|
||||
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) return callback(safe.error);
|
||||
|
||||
if (vhost) safe.fs.unlinkSync(path.join(paths.NGINX_APPCONFIG_DIR, 'admin.conf')); // remove legacy admin.conf. remove after 3.5
|
||||
|
||||
reload(callback);
|
||||
}
|
||||
|
||||
function configureAdmin(auditSource, callback) {
|
||||
function configureAdmin(domain, auditSource, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
ensureCertificate(config.adminFqdn(), config.adminDomain(), auditSource, function (error, bundle) {
|
||||
domains.get(domain, function (error, domainObject) {
|
||||
if (error) return callback(error);
|
||||
|
||||
writeAdminConfig(bundle, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn(), callback);
|
||||
const adminFqdn = domains.fqdn(constants.ADMIN_LOCATION, domainObject);
|
||||
|
||||
ensureCertificate(adminFqdn, domainObject.domain, auditSource, function (error, bundle) {
|
||||
if (error) return callback(error);
|
||||
|
||||
writeAdminNginxConfig(bundle, `${adminFqdn}.conf`, adminFqdn, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function writeAppConfig(app, bundle, callback) {
|
||||
function writeAdminConfig(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
domains.get(domain, function (error, domainObject) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const adminFqdn = domains.fqdn(constants.ADMIN_LOCATION, domainObject);
|
||||
|
||||
getCertificate(adminFqdn, domainObject.domain, function (error, bundle) {
|
||||
if (error) return callback(error);
|
||||
|
||||
writeAdminNginxConfig(bundle, `${adminFqdn}.conf`, adminFqdn, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function writeAppNginxConfig(app, bundle, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof bundle, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -430,7 +472,7 @@ function writeAppConfig(app, bundle, callback) {
|
||||
reload(callback);
|
||||
}
|
||||
|
||||
function writeAppRedirectConfig(app, fqdn, bundle, callback) {
|
||||
function writeAppRedirectNginxConfig(app, fqdn, bundle, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof fqdn, 'string');
|
||||
assert.strictEqual(typeof bundle, 'object');
|
||||
@@ -469,14 +511,14 @@ function configureApp(app, auditSource, callback) {
|
||||
ensureCertificate(app.fqdn, app.domain, auditSource, function (error, bundle) {
|
||||
if (error) return callback(error);
|
||||
|
||||
writeAppConfig(app, bundle, function (error) {
|
||||
writeAppNginxConfig(app, bundle, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.eachSeries(app.alternateDomains, function (alternateDomain, callback) {
|
||||
ensureCertificate(alternateDomain.fqdn, alternateDomain.domain, auditSource, function (error, bundle) {
|
||||
if (error) return callback(error);
|
||||
|
||||
writeAppRedirectConfig(app, alternateDomain.fqdn, bundle, callback);
|
||||
writeAppRedirectNginxConfig(app, alternateDomain.fqdn, bundle, callback);
|
||||
});
|
||||
}, callback);
|
||||
});
|
||||
@@ -495,9 +537,10 @@ function unconfigureApp(app, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function renewCerts(options, auditSource, callback) {
|
||||
function renewCerts(options, auditSource, progressCallback, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
apps.getAll(function (error, allApps) {
|
||||
@@ -506,7 +549,7 @@ function renewCerts(options, auditSource, callback) {
|
||||
var appDomains = [];
|
||||
|
||||
// add webadmin domain
|
||||
appDomains.push({ domain: config.adminDomain(), fqdn: config.adminFqdn(), type: 'webadmin', nginxConfigFilename: path.join(paths.NGINX_APPCONFIG_DIR, constants.NGINX_ADMIN_CONFIG_FILE_NAME) });
|
||||
appDomains.push({ domain: config.adminDomain(), fqdn: config.adminFqdn(), type: 'webadmin', nginxConfigFilename: path.join(paths.NGINX_APPCONFIG_DIR, `${config.adminFqdn()}.conf`) });
|
||||
|
||||
// add app main
|
||||
allApps.forEach(function (app) {
|
||||
@@ -520,11 +563,15 @@ function renewCerts(options, auditSource, callback) {
|
||||
|
||||
if (options.domain) appDomains = appDomains.filter(function (appDomain) { return appDomain.domain === options.domain; });
|
||||
|
||||
let progress = 1;
|
||||
async.eachSeries(appDomains, function (appDomain, iteratorCallback) {
|
||||
progressCallback({ percent: progress, message: `Renewing certs of ${appDomain.fqdn}` });
|
||||
progress += Math.round(100/appDomains.length);
|
||||
|
||||
ensureCertificate(appDomain.fqdn, appDomain.domain, auditSource, function (error, bundle) {
|
||||
if (error) return iteratorCallback(error); // this can happen if cloudron is not setup yet
|
||||
|
||||
// hack to check if the app's cert changed or not
|
||||
// hack to check if the app's cert changed or not. this doesn't handle prod/staging le change since they use same file name
|
||||
let currentNginxConfig = safe.fs.readFileSync(appDomain.nginxConfigFilename, 'utf8') || '';
|
||||
if (currentNginxConfig.includes(bundle.certFilePath)) return iteratorCallback();
|
||||
|
||||
@@ -532,20 +579,18 @@ function renewCerts(options, auditSource, callback) {
|
||||
|
||||
// reconfigure since the cert changed
|
||||
var configureFunc;
|
||||
if (appDomain.type === 'webadmin') configureFunc = writeAdminConfig.bind(null, bundle, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn());
|
||||
else if (appDomain.type === 'main') configureFunc = writeAppConfig.bind(null, appDomain.app, bundle);
|
||||
else if (appDomain.type === 'alternate') configureFunc = writeAppRedirectConfig.bind(null, appDomain.app, appDomain.fqdn, bundle);
|
||||
else return callback(new Error(`Unknown domain type for ${appDomain.fqdn}. This should never happen`));
|
||||
if (appDomain.type === 'webadmin') configureFunc = writeAdminNginxConfig.bind(null, bundle, `${config.adminFqdn()}.conf`, config.adminFqdn());
|
||||
else if (appDomain.type === 'main') configureFunc = writeAppNginxConfig.bind(null, appDomain.app, bundle);
|
||||
else if (appDomain.type === 'alternate') configureFunc = writeAppRedirectNginxConfig.bind(null, appDomain.app, appDomain.fqdn, bundle);
|
||||
else return iteratorCallback(new Error(`Unknown domain type for ${appDomain.fqdn}. This should never happen`));
|
||||
|
||||
configureFunc(function (ignoredError) {
|
||||
if (ignoredError) debug('renewAll: error reconfiguring app', ignoredError);
|
||||
|
||||
platform.handleCertChanged(appDomain.fqdn);
|
||||
|
||||
iteratorCallback(); // move to next domain
|
||||
platform.handleCertChanged(appDomain.fqdn, iteratorCallback);
|
||||
});
|
||||
});
|
||||
});
|
||||
}, callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -559,13 +604,15 @@ function renewAll(auditSource, callback) {
|
||||
}
|
||||
|
||||
function removeAppConfigs() {
|
||||
for (var appConfigFile of fs.readdirSync(paths.NGINX_APPCONFIG_DIR)) {
|
||||
fs.unlinkSync(path.join(paths.NGINX_APPCONFIG_DIR, appConfigFile));
|
||||
for (let appConfigFile of fs.readdirSync(paths.NGINX_APPCONFIG_DIR)) {
|
||||
if (appConfigFile !== constants.NGINX_DEFAULT_CONFIG_FILE_NAME && !appConfigFile.startsWith(constants.ADMIN_LOCATION)) {
|
||||
fs.unlinkSync(path.join(paths.NGINX_APPCONFIG_DIR, appConfigFile));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function configureDefaultServer(callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var certFilePath = path.join(paths.NGINX_CERT_DIR, 'default.cert');
|
||||
var keyFilePath = path.join(paths.NGINX_CERT_DIR, 'default.key');
|
||||
@@ -574,11 +621,13 @@ function configureDefaultServer(callback) {
|
||||
debug('configureDefaultServer: create new cert');
|
||||
|
||||
var cn = 'cloudron-' + (new Date()).toISOString(); // randomize date a bit to keep firefox happy
|
||||
var certCommand = util.format('openssl req -x509 -newkey rsa:2048 -keyout %s -out %s -days 3650 -subj /CN=%s -nodes', keyFilePath, certFilePath, cn);
|
||||
safe.child_process.execSync(certCommand);
|
||||
if (!safe.child_process.execSync(`openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 3650 -subj /CN=${cn} -nodes`)) {
|
||||
debug(`configureDefaultServer: could not generate certificate: ${safe.error.message}`);
|
||||
return callback(safe.error);
|
||||
}
|
||||
}
|
||||
|
||||
writeAdminConfig({ certFilePath, keyFilePath }, 'default.conf', '', function (error) {
|
||||
writeAdminNginxConfig({ certFilePath, keyFilePath }, constants.NGINX_DEFAULT_CONFIG_FILE_NAME, '', function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('configureDefaultServer: done');
|
||||
|
||||
+16
-7
@@ -134,6 +134,7 @@ function installApp(req, res, next) {
|
||||
|
||||
if ('sso' in data && typeof data.sso !== 'boolean') return next(new HttpError(400, 'sso must be a boolean'));
|
||||
if ('enableBackup' in data && typeof data.enableBackup !== 'boolean') return next(new HttpError(400, 'enableBackup must be a boolean'));
|
||||
if ('enableAutomaticUpdate' in data && typeof data.enableAutomaticUpdate !== 'boolean') return next(new HttpError(400, 'enableAutomaticUpdate must be a boolean'));
|
||||
|
||||
if (('debugMode' in data) && typeof data.debugMode !== 'object') return next(new HttpError(400, 'debugMode must be an object'));
|
||||
|
||||
@@ -174,6 +175,10 @@ function configureApp(req, res, next) {
|
||||
|
||||
if ('location' in data && typeof data.location !== 'string') return next(new HttpError(400, 'location must be string'));
|
||||
if ('domain' in data && typeof data.domain !== 'string') return next(new HttpError(400, 'domain must be string'));
|
||||
// domain, location must both be provided since they are unique together
|
||||
if ('location' in data && !('domain' in data)) return next(new HttpError(400, 'domain must be provided'));
|
||||
if (!('location' in data) && 'domain' in data) return next(new HttpError(400, 'location must be provided'));
|
||||
|
||||
if ('portBindings' in data && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
|
||||
if ('accessRestriction' in data && typeof data.accessRestriction !== 'object') return next(new HttpError(400, 'accessRestriction must be an object'));
|
||||
|
||||
@@ -187,6 +192,7 @@ function configureApp(req, res, next) {
|
||||
if (data.xFrameOptions && typeof data.xFrameOptions !== 'string') return next(new HttpError(400, 'xFrameOptions must be a string'));
|
||||
|
||||
if ('enableBackup' in data && typeof data.enableBackup !== 'boolean') return next(new HttpError(400, 'enableBackup must be a boolean'));
|
||||
if ('enableAutomaticUpdate' in data && typeof data.enableAutomaticUpdate !== 'boolean') return next(new HttpError(400, 'enableAutomaticUpdate must be a boolean'));
|
||||
|
||||
if (('debugMode' in data) && typeof data.debugMode !== 'object') return next(new HttpError(400, 'debugMode must be an object'));
|
||||
|
||||
@@ -204,6 +210,8 @@ function configureApp(req, res, next) {
|
||||
if (Object.keys(data.env).some(function (key) { return typeof data.env[key] !== 'string'; })) return next(new HttpError(400, 'env must contain values as strings'));
|
||||
}
|
||||
|
||||
if ('dataDir' in data && typeof data.dataDir !== 'string') return next(new HttpError(400, 'dataDir must be a string'));
|
||||
|
||||
debug('Configuring app id:%s data:%j', req.params.id, data);
|
||||
|
||||
apps.configure(req.params.id, data, req.user, auditSource(req), function (error) {
|
||||
@@ -361,7 +369,7 @@ function getLogStream(req, res, next) {
|
||||
|
||||
debug('Getting logstream of app id:%s', req.params.id);
|
||||
|
||||
var lines = req.query.lines ? parseInt(req.query.lines, 10) : -10; // we ignore last-event-id
|
||||
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10; // we ignore last-event-id
|
||||
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a valid number'));
|
||||
|
||||
function sse(id, data) { return 'id: ' + id + '\ndata: ' + data + '\n\n'; }
|
||||
@@ -370,7 +378,8 @@ function getLogStream(req, res, next) {
|
||||
|
||||
var options = {
|
||||
lines: lines,
|
||||
follow: true
|
||||
follow: true,
|
||||
format: 'json'
|
||||
};
|
||||
|
||||
apps.getLogs(req.params.id, options, function (error, logStream) {
|
||||
@@ -399,7 +408,7 @@ function getLogStream(req, res, next) {
|
||||
function getLogs(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
var lines = req.query.lines ? parseInt(req.query.lines, 10) : 100;
|
||||
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10;
|
||||
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a number'));
|
||||
|
||||
debug('Getting logs of app id:%s', req.params.id);
|
||||
@@ -407,7 +416,7 @@ function getLogs(req, res, next) {
|
||||
var options = {
|
||||
lines: lines,
|
||||
follow: false,
|
||||
format: req.query.format
|
||||
format: req.query.format || 'json'
|
||||
};
|
||||
|
||||
apps.getLogs(req.params.id, options, function (error, logStream) {
|
||||
@@ -513,7 +522,7 @@ function execWebSocket(req, res, next) {
|
||||
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
console.log('Connected to terminal');
|
||||
debug('Connected to terminal');
|
||||
|
||||
req.clearTimeout();
|
||||
|
||||
@@ -521,7 +530,7 @@ function execWebSocket(req, res, next) {
|
||||
duplexStream.on('end', function () { ws.close(); });
|
||||
duplexStream.on('close', function () { ws.close(); });
|
||||
duplexStream.on('error', function (error) {
|
||||
console.error('duplexStream error:', error);
|
||||
debug('duplexStream error:', error);
|
||||
});
|
||||
duplexStream.on('data', function (data) {
|
||||
if (ws.readyState !== WebSocket.OPEN) return;
|
||||
@@ -529,7 +538,7 @@ function execWebSocket(req, res, next) {
|
||||
});
|
||||
|
||||
ws.on('error', function (error) {
|
||||
console.error('websocket error:', error);
|
||||
debug('websocket error:', error);
|
||||
});
|
||||
ws.on('message', function (msg) {
|
||||
duplexStream.write(msg);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
get: get,
|
||||
create: create
|
||||
list: list,
|
||||
startBackup: startBackup
|
||||
};
|
||||
|
||||
var backupdb = require('../backupdb.js'),
|
||||
@@ -16,7 +16,7 @@ function auditSource(req) {
|
||||
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
|
||||
}
|
||||
|
||||
function get(req, res, next) {
|
||||
function list(req, res, next) {
|
||||
var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
|
||||
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number'));
|
||||
|
||||
@@ -31,13 +31,11 @@ function get(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function create(req, res, next) {
|
||||
// note that cloudron.backup only waits for backup initiation and not for backup to complete
|
||||
// backup progress can be checked up ny polling the progress api call
|
||||
backups.backup(auditSource(req), function (error) {
|
||||
function startBackup(req, res, next) {
|
||||
backups.startBackupTask(auditSource(req), function (error, taskId) {
|
||||
if (error && error.reason === BackupsError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
next(new HttpSuccess(202, { taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
getConfig: getConfig,
|
||||
changePlan: changePlan
|
||||
};
|
||||
|
||||
var caas = require('../caas.js'),
|
||||
CaasError = require('../caas.js').CaasError,
|
||||
config = require('../config.js'),
|
||||
debug = require('debug')('box:routes/cloudron'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
_ = require('underscore');
|
||||
|
||||
function getConfig(req, res, next) {
|
||||
if (config.provider() !== 'caas') return next(new HttpError(422, 'Cannot use this API with this provider'));
|
||||
|
||||
caas.getBoxAndUserDetails(function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
// the result is { box: { region, size, plan }, user: { billing, currency } }
|
||||
next(new HttpSuccess(200, {
|
||||
region: result.box.region,
|
||||
size: result.box.size,
|
||||
billing: !!result.user.billing,
|
||||
plan: result.box.plan,
|
||||
currency: result.user.currency
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
function changePlan(req, res, next) {
|
||||
if (config.provider() !== 'caas') return next(new HttpError(422, 'Cannot use this API with this provider'));
|
||||
|
||||
if ('size' in req.body && typeof req.body.size !== 'string') return next(new HttpError(400, 'size must be string'));
|
||||
if ('region' in req.body && typeof req.body.region !== 'string') return next(new HttpError(400, 'region must be string'));
|
||||
|
||||
if ('domain' in req.body) {
|
||||
if (typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be string'));
|
||||
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider must be string'));
|
||||
}
|
||||
|
||||
if ('zoneName' in req.body && typeof req.body.zoneName !== 'string') return next(new HttpError(400, 'zoneName must be string'));
|
||||
|
||||
debug('Migration requested domain:%s size:%s region:%s', req.body.domain, req.body.size, req.body.region);
|
||||
|
||||
var options = _.pick(req.body, 'domain', 'size', 'region');
|
||||
if (Object.keys(options).length === 0) return next(new HttpError(400, 'no migrate option provided'));
|
||||
|
||||
if (options.domain) options.domain = options.domain.toLowerCase();
|
||||
|
||||
caas.changePlan(req.body, function (error) { // pass req.body because 'domain' can have arbitrary options
|
||||
if (error && error.reason === CaasError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === CaasError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
});
|
||||
}
|
||||
+57
-42
@@ -2,45 +2,47 @@
|
||||
|
||||
exports = module.exports = {
|
||||
reboot: reboot,
|
||||
getProgress: getProgress,
|
||||
isRebootRequired: isRebootRequired,
|
||||
getConfig: getConfig,
|
||||
getDisks: getDisks,
|
||||
getUpdateInfo: getUpdateInfo,
|
||||
update: update,
|
||||
feedback: feedback,
|
||||
checkForUpdates: checkForUpdates,
|
||||
getLogs: getLogs,
|
||||
getLogStream: getLogStream
|
||||
getLogStream: getLogStream,
|
||||
setDashboardDomain: setDashboardDomain,
|
||||
prepareDashboardDomain: prepareDashboardDomain,
|
||||
renewCerts: renewCerts
|
||||
};
|
||||
|
||||
var appstore = require('../appstore.js'),
|
||||
AppstoreError = require('../appstore.js').AppstoreError,
|
||||
assert = require('assert'),
|
||||
let assert = require('assert'),
|
||||
async = require('async'),
|
||||
cloudron = require('../cloudron.js'),
|
||||
CloudronError = cloudron.CloudronError,
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
progress = require('../progress.js'),
|
||||
updater = require('../updater.js'),
|
||||
updateChecker = require('../updatechecker.js'),
|
||||
UpdaterError = require('../updater.js').UpdaterError,
|
||||
_ = require('underscore');
|
||||
UpdaterError = require('../updater.js').UpdaterError;
|
||||
|
||||
function auditSource(req) {
|
||||
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
|
||||
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
|
||||
}
|
||||
|
||||
function getProgress(req, res, next) {
|
||||
return next(new HttpSuccess(200, progress.getAll()));
|
||||
}
|
||||
|
||||
function reboot(req, res, next) {
|
||||
// Finish the request, to let the appstore know we triggered the restore it
|
||||
// Finish the request, to let the appstore know we triggered the reboot
|
||||
next(new HttpSuccess(202, {}));
|
||||
|
||||
cloudron.reboot(function () { });
|
||||
cloudron.reboot(function () {});
|
||||
}
|
||||
|
||||
function isRebootRequired(req, res, next) {
|
||||
cloudron.isRebootRequired(function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, { rebootRequired: result }));
|
||||
});
|
||||
}
|
||||
|
||||
function getConfig(req, res, next) {
|
||||
@@ -60,13 +62,12 @@ function getDisks(req, res, next) {
|
||||
|
||||
function update(req, res, next) {
|
||||
// this only initiates the update, progress can be checked via the progress route
|
||||
updater.updateToLatest(auditSource(req), function (error) {
|
||||
updater.updateToLatest(auditSource(req), function (error, taskId) {
|
||||
if (error && error.reason === UpdaterError.ALREADY_UPTODATE) return next(new HttpError(422, error.message));
|
||||
if (error && error.reason === UpdaterError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === UpdaterError.SELF_UPGRADE_NOT_SUPPORTED) return next(new HttpError(412, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
next(new HttpSuccess(202, { taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -75,6 +76,9 @@ function getUpdateInfo(req, res, next) {
|
||||
}
|
||||
|
||||
function checkForUpdates(req, res, next) {
|
||||
// it can take a while sometimes to get all the app updates one by one
|
||||
req.clearTimeout();
|
||||
|
||||
async.series([
|
||||
updateChecker.checkAppUpdates,
|
||||
updateChecker.checkBoxUpdates
|
||||
@@ -83,36 +87,16 @@ function checkForUpdates(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function feedback(req, res, next) {
|
||||
assert.strictEqual(typeof req.user, 'object');
|
||||
|
||||
const VALID_TYPES = [ 'feedback', 'ticket', 'app_missing', 'app_error', 'upgrade_request' ];
|
||||
|
||||
if (typeof req.body.type !== 'string' || !req.body.type) return next(new HttpError(400, 'type must be string'));
|
||||
if (VALID_TYPES.indexOf(req.body.type) === -1) return next(new HttpError(400, 'unknown type'));
|
||||
if (typeof req.body.subject !== 'string' || !req.body.subject) return next(new HttpError(400, 'subject must be string'));
|
||||
if (typeof req.body.description !== 'string' || !req.body.description) return next(new HttpError(400, 'description must be string'));
|
||||
if (req.body.appId && typeof req.body.appId !== 'string') return next(new HttpError(400, 'appId must be string'));
|
||||
|
||||
appstore.sendFeedback(_.extend(req.body, { email: req.user.email, displayName: req.user.displayName }), function (error) {
|
||||
if (error && error.reason === AppstoreError.BILLING_REQUIRED) return next(new HttpError(402, 'Login to App Store to create support tickets. You can also email support@cloudron.io'));
|
||||
if (error) return next(new HttpError(503, 'Error contacting cloudron.io. Please email support@cloudron.io'));
|
||||
|
||||
next(new HttpSuccess(201, {}));
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function getLogs(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.unit, 'string');
|
||||
|
||||
var lines = req.query.lines ? parseInt(req.query.lines, 10) : 100;
|
||||
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10; // we ignore last-event-id
|
||||
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a number'));
|
||||
|
||||
var options = {
|
||||
lines: lines,
|
||||
follow: false,
|
||||
format: req.query.format
|
||||
format: req.query.format || 'json'
|
||||
};
|
||||
|
||||
cloudron.getLogs(req.params.unit, options, function (error, logStream) {
|
||||
@@ -132,7 +116,7 @@ function getLogs(req, res, next) {
|
||||
function getLogStream(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.unit, 'string');
|
||||
|
||||
var lines = req.query.lines ? parseInt(req.query.lines, 10) : -10; // we ignore last-event-id
|
||||
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10; // we ignore last-event-id
|
||||
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a valid number'));
|
||||
|
||||
function sse(id, data) { return 'id: ' + id + '\ndata: ' + data + '\n\n'; }
|
||||
@@ -142,7 +126,7 @@ function getLogStream(req, res, next) {
|
||||
var options = {
|
||||
lines: lines,
|
||||
follow: true,
|
||||
format: req.query.format
|
||||
format: req.query.format || 'json'
|
||||
};
|
||||
|
||||
cloudron.getLogs(req.params.unit, options, function (error, logStream) {
|
||||
@@ -166,3 +150,34 @@ function getLogStream(req, res, next) {
|
||||
logStream.on('error', res.end.bind(res, null));
|
||||
});
|
||||
}
|
||||
|
||||
function setDashboardDomain(req, res, next) {
|
||||
if (!req.body.domain || typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
|
||||
|
||||
cloudron.setDashboardDomain(req.body.domain, function (error) {
|
||||
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(404, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(204, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function prepareDashboardDomain(req, res, next) {
|
||||
if (!req.body.domain || typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
|
||||
|
||||
cloudron.prepareDashboardDomain(req.body.domain, auditSource(req), function (error, taskId) {
|
||||
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(404, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, { taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
function renewCerts(req, res, next) {
|
||||
cloudron.renewCerts({ domain: req.body.domain || null }, auditSource(req), function (error, taskId) {
|
||||
if (error && error.reason === CloudronError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, { taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
+19
-18
@@ -7,8 +7,6 @@ exports = module.exports = {
|
||||
update: update,
|
||||
del: del,
|
||||
|
||||
renewCerts: renewCerts,
|
||||
|
||||
verifyDomainLock: verifyDomainLock
|
||||
};
|
||||
|
||||
@@ -59,7 +57,15 @@ function add(req, res, next) {
|
||||
// some DNS providers like DigitalOcean take a really long time to verify credentials (https://github.com/expressjs/timeout/issues/26)
|
||||
req.clearTimeout();
|
||||
|
||||
domains.add(req.body.domain, req.body.zoneName || '', req.body.provider, req.body.config, req.body.fallbackCertificate || null, req.body.tlsConfig || { provider: 'letsencrypt-prod' }, function (error) {
|
||||
let data = {
|
||||
zoneName: req.body.zoneName || '',
|
||||
provider: req.body.provider,
|
||||
config: req.body.config,
|
||||
fallbackCertificate: req.body.fallbackCertificate || null,
|
||||
tlsConfig: req.body.tlsConfig || { provider: 'letsencrypt-prod' }
|
||||
};
|
||||
|
||||
domains.add(req.body.domain, data, auditSource(req), function (error) {
|
||||
if (error && error.reason === DomainsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === DomainsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === DomainsError.INVALID_PROVIDER) return next(new HttpError(400, error.message));
|
||||
@@ -117,7 +123,15 @@ function update(req, res, next) {
|
||||
// some DNS providers like DigitalOcean take a really long time to verify credentials (https://github.com/expressjs/timeout/issues/26)
|
||||
req.clearTimeout();
|
||||
|
||||
domains.update(req.params.domain, req.body.zoneName || '', req.body.provider, req.body.config, req.body.fallbackCertificate || null, req.body.tlsConfig || { provider: 'letsencrypt-prod' }, function (error) {
|
||||
let data = {
|
||||
zoneName: req.body.zoneName || '',
|
||||
provider: req.body.provider,
|
||||
config: req.body.config,
|
||||
fallbackCertificate: req.body.fallbackCertificate || null,
|
||||
tlsConfig: req.body.tlsConfig || { provider: 'letsencrypt-prod' }
|
||||
};
|
||||
|
||||
domains.update(req.params.domain, data, auditSource(req), function (error) {
|
||||
if (error && error.reason === DomainsError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error && error.reason === DomainsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === DomainsError.INVALID_PROVIDER) return next(new HttpError(400, error.message));
|
||||
@@ -130,7 +144,7 @@ function update(req, res, next) {
|
||||
function del(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.domain, 'string');
|
||||
|
||||
domains.del(req.params.domain, function (error) {
|
||||
domains.del(req.params.domain, auditSource(req), function (error) {
|
||||
if (error && error.reason === DomainsError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error && error.reason === DomainsError.IN_USE) return next(new HttpError(409, 'Domain is still in use. Remove all apps and mailboxes using this domain'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
@@ -138,16 +152,3 @@ function del(req, res, next) {
|
||||
next(new HttpSuccess(204));
|
||||
});
|
||||
}
|
||||
|
||||
function renewCerts(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.domain, 'string');
|
||||
|
||||
domains.renewCerts(req.params.domain, auditSource(req), function (error) {
|
||||
if (error && error.reason === DomainsError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
});
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
}
|
||||
|
||||
+12
-1
@@ -1,14 +1,25 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
get: get
|
||||
get: get,
|
||||
list: list
|
||||
};
|
||||
|
||||
var eventlog = require('../eventlog.js'),
|
||||
EventLogError = eventlog.EventLogError,
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess;
|
||||
|
||||
function get(req, res, next) {
|
||||
eventlog.get(req.params.eventId, function (error, result) {
|
||||
if (error && error.reason === EventLogError.NOT_FOUND) return next(new HttpError(404, 'no such eventlog'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, { event: result }));
|
||||
});
|
||||
}
|
||||
|
||||
function list(req, res, next) {
|
||||
var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
|
||||
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number'));
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ exports = module.exports = {
|
||||
var middleware = require('../middleware/index.js'),
|
||||
url = require('url');
|
||||
|
||||
var graphiteProxy = middleware.proxy(url.parse('http://127.0.0.1:8000'));
|
||||
var graphiteProxy = middleware.proxy(url.parse('http://127.0.0.1:8417'));
|
||||
|
||||
function getGraphs(req, res, next) {
|
||||
var parsedUrl = url.parse(req.url, true /* parseQueryString */);
|
||||
|
||||
+6
-4
@@ -4,7 +4,6 @@ exports = module.exports = {
|
||||
accesscontrol: require('./accesscontrol.js'),
|
||||
apps: require('./apps.js'),
|
||||
backups: require('./backups.js'),
|
||||
caas: require('./caas.js'),
|
||||
clients: require('./clients.js'),
|
||||
cloudron: require('./cloudron.js'),
|
||||
developer: require('./developer.js'),
|
||||
@@ -14,10 +13,13 @@ exports = module.exports = {
|
||||
groups: require('./groups.js'),
|
||||
oauth2: require('./oauth2.js'),
|
||||
mail: require('./mail.js'),
|
||||
notifications: require('./notifications.js'),
|
||||
profile: require('./profile.js'),
|
||||
setup: require('./setup.js'),
|
||||
sysadmin: require('./sysadmin.js'),
|
||||
provision: require('./provision.js'),
|
||||
services: require('./services.js'),
|
||||
settings: require('./settings.js'),
|
||||
ssh: require('./ssh.js'),
|
||||
support: require('./support.js'),
|
||||
sysadmin: require('./sysadmin.js'),
|
||||
tasks: require('./tasks.js'),
|
||||
users: require('./users.js')
|
||||
};
|
||||
|
||||
+18
-9
@@ -17,7 +17,7 @@ exports = module.exports = {
|
||||
|
||||
sendTestMail: sendTestMail,
|
||||
|
||||
getMailboxes: getMailboxes,
|
||||
listMailboxes: listMailboxes,
|
||||
getMailbox: getMailbox,
|
||||
addMailbox: addMailbox,
|
||||
updateMailbox: updateMailbox,
|
||||
@@ -44,6 +44,11 @@ var assert = require('assert'),
|
||||
|
||||
var mailProxy = middleware.proxy(url.parse('http://127.0.0.1:2020'));
|
||||
|
||||
function auditSource(req) {
|
||||
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
|
||||
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
|
||||
}
|
||||
|
||||
function getDomain(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.domain, 'string');
|
||||
|
||||
@@ -86,6 +91,10 @@ function setDnsRecords(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.params.domain, 'string');
|
||||
|
||||
// can take a setup all the DNS entries. this is mostly because some backends try to list DNS entries (DO)
|
||||
// for upsert and this takes a lot of time
|
||||
req.clearTimeout();
|
||||
|
||||
mail.setDnsRecords(req.params.domain, function (error) {
|
||||
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error && error.reason === MailError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
|
||||
@@ -181,7 +190,7 @@ function setMailEnabled(req, res, next) {
|
||||
|
||||
if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled is required'));
|
||||
|
||||
mail.setMailEnabled(req.params.domain, !!req.body.enabled, function (error) {
|
||||
mail.setMailEnabled(req.params.domain, !!req.body.enabled, auditSource(req), function (error) {
|
||||
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error && error.reason === MailError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === MailError.BILLING_REQUIRED) return next(new HttpError(402, error.message));
|
||||
@@ -205,10 +214,10 @@ function sendTestMail(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function getMailboxes(req, res, next) {
|
||||
function listMailboxes(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.domain, 'string');
|
||||
|
||||
mail.getMailboxes(req.params.domain, function (error, result) {
|
||||
mail.listMailboxes(req.params.domain, function (error, result) {
|
||||
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
@@ -234,7 +243,7 @@ function addMailbox(req, res, next) {
|
||||
if (typeof req.body.name !== 'string') return next(new HttpError(400, 'name must be a string'));
|
||||
if (typeof req.body.userId !== 'string') return next(new HttpError(400, 'userId must be a string'));
|
||||
|
||||
mail.addMailbox(req.body.name, req.params.domain, req.body.userId, function (error) {
|
||||
mail.addMailbox(req.body.name, req.params.domain, req.body.userId, auditSource(req), function (error) {
|
||||
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error && error.reason === MailError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === MailError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
@@ -250,7 +259,7 @@ function updateMailbox(req, res, next) {
|
||||
|
||||
if (typeof req.body.userId !== 'string') return next(new HttpError(400, 'userId must be a string'));
|
||||
|
||||
mail.updateMailbox(req.params.name, req.params.domain, req.body.userId, function (error) {
|
||||
mail.updateMailboxOwner(req.params.name, req.params.domain, req.body.userId, function (error) {
|
||||
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error && error.reason === MailError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
@@ -263,7 +272,7 @@ function removeMailbox(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.domain, 'string');
|
||||
assert.strictEqual(typeof req.params.name, 'string');
|
||||
|
||||
mail.removeMailbox(req.params.name, req.params.domain, function (error) {
|
||||
mail.removeMailbox(req.params.name, req.params.domain, auditSource(req), function (error) {
|
||||
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
@@ -349,7 +358,7 @@ function addList(req, res, next) {
|
||||
if (typeof req.body.members[i] !== 'string') return next(new HttpError(400, 'member must be a string'));
|
||||
}
|
||||
|
||||
mail.addList(req.body.name, req.params.domain, req.body.members, function (error) {
|
||||
mail.addList(req.body.name, req.params.domain, req.body.members, auditSource(req), function (error) {
|
||||
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error && error.reason === MailError.ALREADY_EXISTS) return next(new HttpError(409, 'list already exists'));
|
||||
if (error && error.reason === MailError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
@@ -382,7 +391,7 @@ function removeList(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.domain, 'string');
|
||||
assert.strictEqual(typeof req.params.name, 'string');
|
||||
|
||||
mail.removeList(req.params.domain, req.params.name, function (error) {
|
||||
mail.removeList(req.params.name, req.params.domain, auditSource(req), function (error) {
|
||||
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
verifyOwnership: verifyOwnership,
|
||||
get: get,
|
||||
list: list,
|
||||
ack: ack
|
||||
};
|
||||
|
||||
let assert = require('assert'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
notifications = require('../notifications.js'),
|
||||
NotificationsError = notifications.NotificationsError;
|
||||
|
||||
function verifyOwnership(req, res, next) {
|
||||
if (!req.params.notificationId) return next(); // skip for listing
|
||||
|
||||
notifications.get(req.params.notificationId, function (error, result) {
|
||||
if (error && error.reason === NotificationsError.NOT_FOUND) return next(new HttpError(404, 'No such notification'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
if (result.userId !== req.user.id) return next(new HttpError(401, 'Unauthorized'));
|
||||
|
||||
req.notification = result;
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
function get(req, res, next) {
|
||||
assert.strictEqual(typeof req.notification, 'object');
|
||||
|
||||
next(new HttpSuccess(200, { notification: req.notification }));
|
||||
}
|
||||
|
||||
function list(req, res, next) {
|
||||
var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
|
||||
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number'));
|
||||
|
||||
var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
|
||||
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
|
||||
|
||||
var acknowledged = null;
|
||||
if (req.query.acknowledged && !(req.query.acknowledged === 'true' || req.query.acknowledged === 'false')) return next(new HttpError(400, 'acknowledged must be a true or false'));
|
||||
else if (req.query.acknowledged) acknowledged = req.query.acknowledged === 'true' ? true : false;
|
||||
|
||||
notifications.getAllPaged(req.user.id, acknowledged, page, perPage, function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, { notifications: result }));
|
||||
});
|
||||
}
|
||||
|
||||
function ack(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.notificationId, 'string');
|
||||
|
||||
notifications.ack(req.params.notificationId, function (error) {
|
||||
if (error && error.reason === NotificationsError.NOT_FOUND) return next(new HttpError(404, 'No such notification'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(204, {}));
|
||||
});
|
||||
}
|
||||
@@ -3,10 +3,10 @@
|
||||
exports = module.exports = {
|
||||
providerTokenAuth: providerTokenAuth,
|
||||
setupTokenAuth: setupTokenAuth,
|
||||
provision: provision,
|
||||
setup: setup,
|
||||
activate: activate,
|
||||
restore: restore,
|
||||
getStatus: getStatus,
|
||||
getStatus: getStatus
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
@@ -16,10 +16,9 @@ var assert = require('assert'),
|
||||
debug = require('debug')('box:routes/setup'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
setup = require('../setup.js'),
|
||||
SetupError = require('../setup.js').SetupError,
|
||||
superagent = require('superagent'),
|
||||
_ = require('underscore');
|
||||
provision = require('../provision.js'),
|
||||
ProvisionError = require('../provision.js').ProvisionError,
|
||||
superagent = require('superagent');
|
||||
|
||||
function auditSource(req) {
|
||||
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
|
||||
@@ -63,7 +62,7 @@ function setupTokenAuth(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function provision(req, res, next) {
|
||||
function setup(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (!req.body.dnsConfig || typeof req.body.dnsConfig !== 'object') return next(new HttpError(400, 'dnsConfig is required'));
|
||||
@@ -85,24 +84,16 @@ function provision(req, res, next) {
|
||||
// it can take sometime to setup DNS, register cloudron
|
||||
req.clearTimeout();
|
||||
|
||||
setup.provision(dnsConfig, req.body.autoconf || {}, function (error) {
|
||||
if (error && error.reason === SetupError.ALREADY_SETUP) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === SetupError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === SetupError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
provision.setup(dnsConfig, req.body.autoconf || {}, auditSource(req), function (error) {
|
||||
if (error && error.reason === ProvisionError.ALREADY_SETUP) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === ProvisionError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === ProvisionError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200));
|
||||
});
|
||||
}
|
||||
|
||||
function getStatus(req, res, next) {
|
||||
setup.getStatus(function (error, status) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, status));
|
||||
});
|
||||
}
|
||||
|
||||
function activate(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
@@ -119,9 +110,9 @@ function activate(req, res, next) {
|
||||
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
|
||||
debug('activate: username:%s ip:%s', username, ip);
|
||||
|
||||
setup.activate(username, password, email, displayName, ip, auditSource(req), function (error, info) {
|
||||
if (error && error.reason === SetupError.ALREADY_PROVISIONED) return next(new HttpError(409, 'Already setup'));
|
||||
if (error && error.reason === SetupError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
provision.activate(username, password, email, displayName, ip, auditSource(req), function (error, info) {
|
||||
if (error && error.reason === ProvisionError.ALREADY_PROVISIONED) return next(new HttpError(409, 'Already setup'));
|
||||
if (error && error.reason === ProvisionError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
// only in caas case do we have to notify the api server about activation
|
||||
@@ -156,13 +147,21 @@ function restore(req, res, next) {
|
||||
// TODO: validate subfields of these objects
|
||||
if (req.body.autoconf && typeof req.body.autoconf !== 'object') return next(new HttpError(400, 'autoconf must be an object'));
|
||||
|
||||
setup.restore(backupConfig, req.body.backupId, req.body.version, req.body.autoconf || {}, function (error) {
|
||||
if (error && error.reason === SetupError.ALREADY_SETUP) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === SetupError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === SetupError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === SetupError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
|
||||
provision.restore(backupConfig, req.body.backupId, req.body.version, req.body.autoconf || {}, auditSource(req), function (error) {
|
||||
if (error && error.reason === ProvisionError.ALREADY_SETUP) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === ProvisionError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === ProvisionError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === ProvisionError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200));
|
||||
});
|
||||
}
|
||||
|
||||
function getStatus(req, res, next) {
|
||||
provision.getStatus(function (error, status) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, status));
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
getAll: getAll,
|
||||
get: get,
|
||||
configure: configure,
|
||||
getLogs: getLogs,
|
||||
getLogStream: getLogStream,
|
||||
restart: restart
|
||||
};
|
||||
|
||||
var addons = require('../addons.js'),
|
||||
AddonsError = addons.AddonsError,
|
||||
assert = require('assert'),
|
||||
debug = require('debug')('box:routes/addons'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess;
|
||||
|
||||
function getAll(req, res, next) {
|
||||
addons.getServices(function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, { services: result }));
|
||||
});
|
||||
}
|
||||
|
||||
function get(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.service, 'string');
|
||||
|
||||
addons.getService(req.params.service, function (error, result) {
|
||||
if (error && error.reason === AddonsError.NOT_FOUND) return next(new HttpError(404, 'No such service'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, { service: result }));
|
||||
});
|
||||
}
|
||||
|
||||
function configure(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.service, 'string');
|
||||
|
||||
if (typeof req.body.memory !== 'number') return next(new HttpError(400, 'memory must be a number'));
|
||||
|
||||
const data = {
|
||||
memory: req.body.memory,
|
||||
memorySwap: req.body.memory * 2
|
||||
};
|
||||
|
||||
addons.configureService(req.params.service, data, function (error) {
|
||||
if (error && error.reason === AddonsError.NOT_FOUND) return next(new HttpError(404, 'No such service'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function getLogs(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.service, 'string');
|
||||
|
||||
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10; // we ignore last-event-id
|
||||
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a number'));
|
||||
|
||||
debug(`Getting logs of service ${req.params.service}`);
|
||||
|
||||
var options = {
|
||||
lines: lines,
|
||||
follow: false,
|
||||
format: req.query.format || 'json'
|
||||
};
|
||||
|
||||
addons.getServiceLogs(req.params.service, options, function (error, logStream) {
|
||||
if (error && error.reason === AddonsError.NOT_FOUND) return next(new HttpError(404, 'No such service'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/x-logs',
|
||||
'Content-Disposition': 'attachment; filename="log.txt"',
|
||||
'Cache-Control': 'no-cache',
|
||||
'X-Accel-Buffering': 'no' // disable nginx buffering
|
||||
});
|
||||
logStream.pipe(res);
|
||||
});
|
||||
}
|
||||
|
||||
// this route is for streaming logs
|
||||
function getLogStream(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.service, 'string');
|
||||
|
||||
debug(`Getting logstream of service ${req.params.service}`);
|
||||
|
||||
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10; // we ignore last-event-id
|
||||
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a valid number'));
|
||||
|
||||
function sse(id, data) { return 'id: ' + id + '\ndata: ' + data + '\n\n'; }
|
||||
|
||||
if (req.headers.accept !== 'text/event-stream') return next(new HttpError(400, 'This API call requires EventStream'));
|
||||
|
||||
var options = {
|
||||
lines: lines,
|
||||
follow: true,
|
||||
format: 'json'
|
||||
};
|
||||
|
||||
addons.getServiceLogs(req.params.service, options, function (error, logStream) {
|
||||
if (error && error.reason === AddonsError.NOT_FOUND) return next(new HttpError(404, 'No such service'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
'X-Accel-Buffering': 'no', // disable nginx buffering
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
});
|
||||
res.write('retry: 3000\n');
|
||||
res.on('close', logStream.close);
|
||||
logStream.on('data', function (data) {
|
||||
var obj = JSON.parse(data);
|
||||
res.write(sse(obj.monotonicTimestamp, JSON.stringify(obj))); // send timestamp as id
|
||||
});
|
||||
logStream.on('end', res.end.bind(res));
|
||||
logStream.on('error', res.end.bind(res, null));
|
||||
});
|
||||
}
|
||||
|
||||
function restart(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.service, 'string');
|
||||
|
||||
debug(`Restarting service ${req.params.service}`);
|
||||
|
||||
addons.restartService(req.params.service, function (error) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
});
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user