Compare commits
173 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 |
@@ -1505,3 +1505,20 @@
|
||||
* 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
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
};
|
||||
+14
-1
@@ -212,5 +212,18 @@ CREATE TABLE IF NOT EXISTS tasks(
|
||||
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id));
|
||||
|
||||
CHARACTER SET utf8 COLLATE utf8_bin;
|
||||
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
+226
@@ -829,6 +829,35 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@sinonjs/commons": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/@sinonjs/commons/-/commons-1.3.0.tgz",
|
||||
"integrity": "sha1-UKJ1QBa28wqZTO2m2aCow2rdqEk=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"type-detect": "4.0.8"
|
||||
}
|
||||
},
|
||||
"@sinonjs/formatio": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/@sinonjs/formatio/-/formatio-3.1.0.tgz",
|
||||
"integrity": "sha1-asnR6xghmE2ExJlnJuRdFkbYzOU=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sinonjs/samsam": "^2 || ^3"
|
||||
}
|
||||
},
|
||||
"@sinonjs/samsam": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/@sinonjs/samsam/-/samsam-3.0.2.tgz",
|
||||
"integrity": "sha1-ME+zO9VYWgst+KTIAfy0f6hNjkM=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sinonjs/commons": "^1.0.2",
|
||||
"array-from": "^2.1.1",
|
||||
"lodash.get": "^4.4.2"
|
||||
}
|
||||
},
|
||||
"JSONStream": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.2.tgz",
|
||||
@@ -997,6 +1026,12 @@
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
|
||||
},
|
||||
"array-from": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/array-from/-/array-from-2.1.1.tgz",
|
||||
"integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=",
|
||||
"dev": true
|
||||
},
|
||||
"array-uniq": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz",
|
||||
@@ -1208,6 +1243,11 @@
|
||||
"tweetnacl": "^0.14.3"
|
||||
}
|
||||
},
|
||||
"bindings": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.3.1.tgz",
|
||||
"integrity": "sha512-i47mqjF9UbjxJhxGf+pZ6kSxrnI3wBLlnGI2ArWJ4r0VrvDS7ZYXkprq/pLaBWYq4GM0r4zdHY+NNRqEMU7uew=="
|
||||
},
|
||||
"bl": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz",
|
||||
@@ -2460,6 +2500,12 @@
|
||||
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
|
||||
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
|
||||
},
|
||||
"diff": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/diff/-/diff-3.5.0.tgz",
|
||||
"integrity": "sha1-gAwN0eCov7yVg1wgKtIg/jF+WhI=",
|
||||
"dev": true
|
||||
},
|
||||
"dijkstrajs": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.1.tgz",
|
||||
@@ -4293,6 +4339,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/has-flag/-/has-flag-3.0.0.tgz",
|
||||
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
|
||||
"dev": true
|
||||
},
|
||||
"has-unicode": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
|
||||
@@ -4510,6 +4562,21 @@
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
|
||||
},
|
||||
"isemail": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/isemail/-/isemail-3.2.0.tgz",
|
||||
"integrity": "sha512-zKqkK+O+dGqevc93KNsbZ/TqTUFd46MwWjYOoMrjIMZ51eU7DtQG3Wmd9SQQT7i7RVnuTPEiYEWHU3MSbxC1Tg==",
|
||||
"requires": {
|
||||
"punycode": "2.x.x"
|
||||
},
|
||||
"dependencies": {
|
||||
"punycode": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
@@ -4602,6 +4669,23 @@
|
||||
"resolved": "https://registry.npmjs.org/java-packagename-regex/-/java-packagename-regex-1.0.0.tgz",
|
||||
"integrity": "sha1-lR9he9WhlCIO0GcLm4KowOxcYiQ="
|
||||
},
|
||||
"joi": {
|
||||
"version": "13.7.0",
|
||||
"resolved": "https://registry.npmjs.org/joi/-/joi-13.7.0.tgz",
|
||||
"integrity": "sha512-xuY5VkHfeOYK3Hdi91ulocfuFopwgbSORmIwzcwHKESQhC7w1kD5jaVSPnqDxS2I8t3RZ9omCKAxNwXN5zG1/Q==",
|
||||
"requires": {
|
||||
"hoek": "5.x.x",
|
||||
"isemail": "3.x.x",
|
||||
"topo": "3.x.x"
|
||||
},
|
||||
"dependencies": {
|
||||
"hoek": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz",
|
||||
"integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"js-base64": {
|
||||
"version": "2.4.5",
|
||||
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.5.tgz",
|
||||
@@ -4719,6 +4803,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"just-extend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/just-extend/-/just-extend-4.0.2.tgz",
|
||||
"integrity": "sha1-8/R/ffyg+YnFVBCn68iFSwcQivw=",
|
||||
"dev": true
|
||||
},
|
||||
"jwa": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.1.5.tgz",
|
||||
@@ -4887,6 +4977,12 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
|
||||
"integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8="
|
||||
},
|
||||
"lodash.get": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/lodash.get/-/lodash.get-4.4.2.tgz",
|
||||
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.groupby": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz",
|
||||
@@ -4913,6 +5009,12 @@
|
||||
"resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.5.tgz",
|
||||
"integrity": "sha1-euTsJXMC/XkNVXyxDJcQDYV7AFY="
|
||||
},
|
||||
"lolex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/lolex/-/lolex-3.0.0.tgz",
|
||||
"integrity": "sha1-8E7hqKoT9g8avXsOj0IT7HLsGT4=",
|
||||
"dev": true
|
||||
},
|
||||
"longest": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
|
||||
@@ -5532,6 +5634,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"namecheap": {
|
||||
"version": "github:joshuakarjala/node-namecheap#464a9528b7ded3ee2520c2688bc98cbffb08e603",
|
||||
"from": "github:joshuakarjala/node-namecheap#464a952",
|
||||
"requires": {
|
||||
"request": "*",
|
||||
"xml2json": "*"
|
||||
}
|
||||
},
|
||||
"nan": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz",
|
||||
@@ -5549,6 +5659,42 @@
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
|
||||
"integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
|
||||
},
|
||||
"nise": {
|
||||
"version": "1.4.8",
|
||||
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/nise/-/nise-1.4.8.tgz",
|
||||
"integrity": "sha1-zpHDHobPmyxMrEnX/Nf1Z3m/1rA=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sinonjs/formatio": "^3.1.0",
|
||||
"just-extend": "^4.0.2",
|
||||
"lolex": "^2.3.2",
|
||||
"path-to-regexp": "^1.7.0",
|
||||
"text-encoding": "^0.6.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"isarray": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/isarray/-/isarray-0.0.1.tgz",
|
||||
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
|
||||
"dev": true
|
||||
},
|
||||
"lolex": {
|
||||
"version": "2.7.5",
|
||||
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/lolex/-/lolex-2.7.5.tgz",
|
||||
"integrity": "sha1-ETAB1Wv8fgLVbjYpHMXEE9GqBzM=",
|
||||
"dev": true
|
||||
},
|
||||
"path-to-regexp": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/path-to-regexp/-/path-to-regexp-1.7.0.tgz",
|
||||
"integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"isarray": "0.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"nock": {
|
||||
"version": "9.3.2",
|
||||
"resolved": "https://registry.npmjs.org/nock/-/nock-9.3.2.tgz",
|
||||
@@ -5583,6 +5729,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node-expat": {
|
||||
"version": "2.3.17",
|
||||
"resolved": "https://registry.npmjs.org/node-expat/-/node-expat-2.3.17.tgz",
|
||||
"integrity": "sha512-mNTxY/GMiZGayqdKZXyf6lJR7OM1JqyL0EISjE4XF7Ov7+X4zJjmlnfxCi6Gml90IEOyiYBcyJg9MHDsDp6YHw==",
|
||||
"requires": {
|
||||
"bindings": "^1.2.1",
|
||||
"nan": "^2.10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"nan": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.12.1.tgz",
|
||||
"integrity": "sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"node-forge": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.1.tgz",
|
||||
@@ -7997,6 +8159,32 @@
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
|
||||
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
|
||||
},
|
||||
"sinon": {
|
||||
"version": "7.2.2",
|
||||
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/sinon/-/sinon-7.2.2.tgz",
|
||||
"integrity": "sha1-OI7KvUL6k8WSv8cdNacIlNWgygc=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sinonjs/commons": "^1.2.0",
|
||||
"@sinonjs/formatio": "^3.1.0",
|
||||
"@sinonjs/samsam": "^3.0.2",
|
||||
"diff": "^3.5.0",
|
||||
"lolex": "^3.0.0",
|
||||
"nise": "^1.4.7",
|
||||
"supports-color": "^5.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"supports-color": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/supports-color/-/supports-color-5.5.0.tgz",
|
||||
"integrity": "sha1-4uaaRKyHcveKHsCzW2id9lMO/I8=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has-flag": "^3.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sntp": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz",
|
||||
@@ -8614,6 +8802,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"text-encoding": {
|
||||
"version": "0.6.4",
|
||||
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/text-encoding/-/text-encoding-0.6.4.tgz",
|
||||
"integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=",
|
||||
"dev": true
|
||||
},
|
||||
"through2": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz",
|
||||
@@ -8636,6 +8830,21 @@
|
||||
"resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz",
|
||||
"integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg=="
|
||||
},
|
||||
"topo": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/topo/-/topo-3.0.3.tgz",
|
||||
"integrity": "sha512-IgpPtvD4kjrJ7CRA3ov2FhWQADwv+Tdqbsf1ZnPUSAtCJ9e1Z44MmoSGDXGk4IppoZA7jd/QRkNddlLJWlUZsQ==",
|
||||
"requires": {
|
||||
"hoek": "6.x.x"
|
||||
},
|
||||
"dependencies": {
|
||||
"hoek": {
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/hoek/-/hoek-6.1.2.tgz",
|
||||
"integrity": "sha512-6qhh/wahGYZHFSFw12tBbJw5fsAhhwrrG/y3Cs0YMTv2WzMnL0oLPnQJjv1QJvEfylRSOFuP+xCu+tdx0tD16Q=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"tough-cookie": {
|
||||
"version": "2.3.4",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz",
|
||||
@@ -8952,6 +9161,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"xml2json": {
|
||||
"version": "0.11.2",
|
||||
"resolved": "https://registry.npmjs.org/xml2json/-/xml2json-0.11.2.tgz",
|
||||
"integrity": "sha512-ZJpHpPOL0T5lOvAHMnWm59iQOPqNtam5t2TMUllWZ1k5Wm8L5YyvQnkeaVnRKCvDwY5EumqXWyOjjMdQVz272A==",
|
||||
"requires": {
|
||||
"hoek": "^4.2.1",
|
||||
"joi": "^13.1.2",
|
||||
"node-expat": "^2.3.15"
|
||||
},
|
||||
"dependencies": {
|
||||
"hoek": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "http://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz",
|
||||
"integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"xtend": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
|
||||
|
||||
+4
-4
@@ -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 -- --no-timeouts --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",
|
||||
|
||||
@@ -101,6 +101,7 @@ elif [[ \
|
||||
"${provider}" != "azure" && \
|
||||
"${provider}" != "caas" && \
|
||||
"${provider}" != "cloudscale" && \
|
||||
"${provider}" != "contabo" && \
|
||||
"${provider}" != "digitalocean" && \
|
||||
"${provider}" != "ec2" && \
|
||||
"${provider}" != "exoscale" && \
|
||||
@@ -117,7 +118,7 @@ elif [[ \
|
||||
"${provider}" != "vultr" && \
|
||||
"${provider}" != "generic" \
|
||||
]]; then
|
||||
echo "--provider must be one of: azure, cloudscale.ch, digitalocean, ec2, exoscale, galaxygate, gce, hetzner, lightsail, linode, netcup, 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
|
||||
|
||||
@@ -138,6 +139,12 @@ 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}"
|
||||
@@ -243,7 +250,7 @@ echo -e "\n\n${GREEN}Visit https://<IP> and accept the self-signed certificate t
|
||||
if [[ "${rebootServer}" == "true" ]]; then
|
||||
systemctl stop box mysql # sometimes mysql ends up having corrupt privilege tables
|
||||
|
||||
read -p "This server has to rebooted to apply all the settings. Reboot now ? [Y/n] " yn
|
||||
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;;
|
||||
|
||||
+9
-17
@@ -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 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
|
||||
@@ -88,25 +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}"
|
||||
|
||||
# remove old cloudron.conf. Can be removed after 3.4
|
||||
rm -f "${CONFIG_DIR}/cloudron.conf"
|
||||
$json -f /etc/cloudron/cloudron.conf -I -e "delete this.version" # remove the version field
|
||||
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
|
||||
@@ -183,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)"
|
||||
@@ -198,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}"
|
||||
|
||||
+10
-4
@@ -1,8 +1,14 @@
|
||||
# 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/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
|
||||
@@ -25,8 +31,8 @@ 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
|
||||
|
||||
@@ -17,9 +17,12 @@ ExecStart=/bin/sh -c 'echo "Logging to /home/yellowtent/platformdata/logs/box.lo
|
||||
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
|
||||
|
||||
+23
-12
@@ -33,6 +33,7 @@ exports = module.exports = {
|
||||
|
||||
var accesscontrol = require('./accesscontrol.js'),
|
||||
appdb = require('./appdb.js'),
|
||||
apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
clients = require('./clients.js'),
|
||||
@@ -109,7 +110,7 @@ var KNOWN_ADDONS = {
|
||||
clear: NOOP,
|
||||
},
|
||||
localstorage: {
|
||||
setup: setupLocalStorage, // docker creates the directory for us
|
||||
setup: setupLocalStorage,
|
||||
teardown: teardownLocalStorage,
|
||||
backup: NOOP, // no backup because it's already inside app data
|
||||
restore: NOOP,
|
||||
@@ -369,24 +370,25 @@ function getServiceLogs(serviceName, 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');
|
||||
|
||||
if (!KNOWN_SERVICES[serviceName]) return callback(new AddonsError(AddonsError.NOT_FOUND));
|
||||
|
||||
debug(`Getting logs for ${serviceName}`);
|
||||
|
||||
var lines = options.lines || 100,
|
||||
var lines = options.lines,
|
||||
format = options.format || 'json',
|
||||
follow = !!options.follow;
|
||||
follow = options.follow;
|
||||
|
||||
assert.strictEqual(typeof lines, 'number');
|
||||
assert.strictEqual(typeof format, 'string');
|
||||
|
||||
var cmd;
|
||||
var args = [ '--lines=' + lines ];
|
||||
let cmd, args = [];
|
||||
|
||||
// docker and unbound use journald
|
||||
if (serviceName === 'docker' || serviceName === 'unbound') {
|
||||
cmd = 'journalctl';
|
||||
|
||||
args.push('--lines=' + (lines === -1 ? 'all' : lines));
|
||||
args.push(`--unit=${serviceName}`);
|
||||
args.push('--no-pager');
|
||||
args.push('--output=short-iso');
|
||||
@@ -395,6 +397,7 @@ function getServiceLogs(serviceName, options, callback) {
|
||||
} else {
|
||||
cmd = '/usr/bin/tail';
|
||||
|
||||
args.push('--lines=' + (lines === -1 ? '+1' : lines));
|
||||
if (follow) args.push('--follow', '--retry', '--quiet'); // same as -F. to make it work if file doesn't exist, --quiet to not output file headers, which are no logs
|
||||
args.push(path.join(paths.LOG_DIR, serviceName, 'app.log'));
|
||||
}
|
||||
@@ -726,8 +729,13 @@ function setupLocalStorage(app, options, callback) {
|
||||
|
||||
debugApp(app, 'setupLocalStorage');
|
||||
|
||||
// if you change the name, you have to change getMountsSync
|
||||
docker.createVolume(app, `${app.id}-localstorage`, 'data', callback);
|
||||
const volumeDataDir = apps.getDataDir(app, app.dataDir);
|
||||
|
||||
// reomve any existing volume in case it's bound with an old dataDir
|
||||
async.series([
|
||||
docker.removeVolume.bind(null, app, `${app.id}-localstorage`),
|
||||
docker.createVolume.bind(null, app, `${app.id}-localstorage`, volumeDataDir)
|
||||
], callback);
|
||||
}
|
||||
|
||||
function clearLocalStorage(app, options, callback) {
|
||||
@@ -737,7 +745,7 @@ function clearLocalStorage(app, options, callback) {
|
||||
|
||||
debugApp(app, 'clearLocalStorage');
|
||||
|
||||
docker.clearVolume(app, `${app.id}-localstorage`, 'data', callback);
|
||||
docker.clearVolume(app, `${app.id}-localstorage`, { removeDirectory: false }, callback);
|
||||
}
|
||||
|
||||
function teardownLocalStorage(app, options, callback) {
|
||||
@@ -747,7 +755,10 @@ function teardownLocalStorage(app, options, callback) {
|
||||
|
||||
debugApp(app, 'teardownLocalStorage');
|
||||
|
||||
docker.removeVolume(app, `${app.id}-localstorage`, 'data', callback);
|
||||
async.series([
|
||||
docker.clearVolume.bind(null, app, `${app.id}-localstorage`, { removeDirectory: true }),
|
||||
docker.removeVolume.bind(null, app, `${app.id}-localstorage`)
|
||||
], callback);
|
||||
}
|
||||
|
||||
function setupOauth(app, options, callback) {
|
||||
|
||||
+5
-1
@@ -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.mailboxName', 'apps.enableAutomaticUpdate', '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(',');
|
||||
|
||||
@@ -139,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) {
|
||||
|
||||
+16
-8
@@ -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');
|
||||
|
||||
@@ -46,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');
|
||||
@@ -132,19 +134,25 @@ function processDockerEvents(intervalSecs, 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 containerId = ev.id;
|
||||
|
||||
appdb.getByContainerId(containerId, function (error, app) { // this can error for addons
|
||||
var program = error || !app.id ? containerId : `app-${app.id}`;
|
||||
var now = Date.now();
|
||||
if (app) context = context + '\n\n' + JSON.stringify(app, null, 4) + '\n';
|
||||
|
||||
const notifyUser = (!app || !app.debugMode) && (now - gLastOomMailTime > OOM_MAIL_LIMIT);
|
||||
|
||||
debug('OOM Context: %s. notifyUser: %s. lastOomTime: %s (now: %s)', context, notifyUser, gLastOomMailTime, now);
|
||||
debug('OOM %s notifyUser: %s. lastOomTime: %s (now: %s)', program, notifyUser, gLastOomMailTime, now, ev);
|
||||
|
||||
// do not send mails for dev apps
|
||||
if (notifyUser) {
|
||||
mailer.oomEvent(program, context); // app can be null if it's an addon crash
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
+78
-34
@@ -38,6 +38,7 @@ exports = module.exports = {
|
||||
configureInstalledApps: configureInstalledApps,
|
||||
|
||||
getAppConfig: getAppConfig,
|
||||
getDataDir: getDataDir,
|
||||
|
||||
downloadFile: downloadFile,
|
||||
uploadFile: uploadFile,
|
||||
@@ -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', 'enableAutomaticUpdate');
|
||||
'alternateDomains', 'ownerId', 'env', 'enableAutomaticUpdate', 'dataDir');
|
||||
}
|
||||
|
||||
function removeRestrictedFields(app) {
|
||||
@@ -746,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));
|
||||
@@ -801,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) {
|
||||
@@ -837,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'));
|
||||
|
||||
@@ -868,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 ];
|
||||
@@ -952,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);
|
||||
});
|
||||
|
||||
+39
-30
@@ -52,6 +52,7 @@ 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');
|
||||
|
||||
@@ -135,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);
|
||||
@@ -187,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));
|
||||
@@ -469,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.
|
||||
@@ -497,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) {
|
||||
@@ -527,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) {
|
||||
@@ -538,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, (progress) => updateApp(app, { installationProgress: progress.message }, NOOP_CALLBACK))
|
||||
backups.restoreApp.bind(null, app, app.manifest.addons, restoreConfig, (progress) => updateApp(app, { installationProgress: `65, Restore - ${progress.message}` }, NOOP_CALLBACK))
|
||||
], next);
|
||||
}
|
||||
},
|
||||
@@ -583,7 +585,7 @@ function backup(app, callback) {
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '10, Backing up' }),
|
||||
backups.backupApp.bind(null, app, (progress) => updateApp(app, { installationProgress: progress.message }, NOOP_CALLBACK)),
|
||||
backups.backupApp.bind(null, app, (progress) => updateApp(app, { installationProgress: `30, ${progress.message}` }, NOOP_CALLBACK)),
|
||||
|
||||
// done!
|
||||
function (callback) {
|
||||
@@ -605,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' }),
|
||||
@@ -613,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();
|
||||
@@ -621,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' }),
|
||||
@@ -636,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),
|
||||
|
||||
@@ -696,7 +705,7 @@ function update(app, callback) {
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '15, Backing up app' }),
|
||||
backups.backupApp.bind(null, app, (progress) => updateApp(app, { installationProgress: progress.message }, NOOP_CALLBACK))
|
||||
backups.backupApp.bind(null, app, (progress) => updateApp(app, { installationProgress: `15, Backup - ${progress.message}` }, NOOP_CALLBACK))
|
||||
], function (error) {
|
||||
if (error) error.backupError = true;
|
||||
next(error);
|
||||
@@ -714,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();
|
||||
|
||||
@@ -800,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' }),
|
||||
|
||||
+214
-122
@@ -22,13 +22,12 @@ exports = module.exports = {
|
||||
|
||||
upload: upload,
|
||||
|
||||
startCleanupTask: startCleanupTask,
|
||||
cleanup: cleanup,
|
||||
cleanupCacheFilesSync: cleanupCacheFilesSync,
|
||||
|
||||
// for testing
|
||||
_getBackupFilePath: getBackupFilePath,
|
||||
_createTarPackStream: createTarPackStream,
|
||||
_tarExtract: tarExtract,
|
||||
_restoreFsMetadata: restoreFsMetadata,
|
||||
_saveFsMetadata: saveFsMetadata
|
||||
};
|
||||
@@ -44,6 +43,7 @@ var addons = require('./addons.js'),
|
||||
crypto = require('crypto'),
|
||||
database = require('./database.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
DataLayout = require('./datalayout.js'),
|
||||
debug = require('debug')('box:backups'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
fs = require('fs'),
|
||||
@@ -64,7 +64,6 @@ var addons = require('./addons.js'),
|
||||
util = require('util'),
|
||||
zlib = require('zlib');
|
||||
|
||||
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
const BACKUP_UPLOAD_CMD = path.join(__dirname, 'scripts/backupupload.js');
|
||||
|
||||
function debugApp(app) {
|
||||
@@ -228,7 +227,7 @@ function createReadStream(sourceFile, key) {
|
||||
var ps = progressStream({ time: 10000 }); // display a progress every 10 seconds
|
||||
|
||||
stream.on('error', function (error) {
|
||||
debug('createReadStream: tar stream error.', error);
|
||||
debug('createReadStream: read stream error.', error);
|
||||
ps.emit('error', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
||||
});
|
||||
|
||||
@@ -262,15 +261,16 @@ function createWriteStream(destFile, key) {
|
||||
}
|
||||
}
|
||||
|
||||
function createTarPackStream(sourceDir, key) {
|
||||
assert.strictEqual(typeof sourceDir, 'string');
|
||||
function tarPack(dataLayout, key, callback) {
|
||||
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
|
||||
assert(key === null || typeof key === 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var pack = tar.pack('/', {
|
||||
dereference: false, // pack the symlink and not what it points to
|
||||
entries: [ sourceDir ],
|
||||
entries: dataLayout.localPaths(),
|
||||
map: function(header) {
|
||||
header.name = header.name.replace(new RegExp('^' + sourceDir + '(/?)'), '.$1'); // make paths relative
|
||||
header.name = dataLayout.toRemotePath(header.name);
|
||||
return header;
|
||||
},
|
||||
strict: false // do not error for unknown types (skip fifo, char/block devices)
|
||||
@@ -280,35 +280,40 @@ function createTarPackStream(sourceDir, key) {
|
||||
var ps = progressStream({ time: 10000 }); // emit 'pgoress' every 10 seconds
|
||||
|
||||
pack.on('error', function (error) {
|
||||
debug('createTarPackStream: tar stream error.', error);
|
||||
debug('tarPack: tar stream error.', error);
|
||||
ps.emit('error', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
||||
});
|
||||
|
||||
gzip.on('error', function (error) {
|
||||
debug('createTarPackStream: gzip stream error.', error);
|
||||
debug('tarPack: gzip stream error.', error);
|
||||
ps.emit('error', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
||||
});
|
||||
|
||||
if (key !== null) {
|
||||
var encrypt = crypto.createCipher('aes-256-cbc', key);
|
||||
encrypt.on('error', function (error) {
|
||||
debug('createTarPackStream: encrypt stream error.', error);
|
||||
debug('tarPack: encrypt stream error.', error);
|
||||
ps.emit('error', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
||||
});
|
||||
return pack.pipe(gzip).pipe(encrypt).pipe(ps);
|
||||
pack.pipe(gzip).pipe(encrypt).pipe(ps);
|
||||
} else {
|
||||
return pack.pipe(gzip).pipe(ps);
|
||||
pack.pipe(gzip).pipe(ps);
|
||||
}
|
||||
|
||||
callback(null, ps);
|
||||
}
|
||||
|
||||
function sync(backupConfig, backupId, dataDir, progressCallback, callback) {
|
||||
function sync(backupConfig, backupId, dataLayout, progressCallback, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
assert.strictEqual(typeof dataDir, 'string');
|
||||
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
syncer.sync(dataDir, function processTask(task, iteratorCallback) {
|
||||
// the number here has to take into account the s3.upload partSize (which is 10MB). So 20=200MB
|
||||
const concurrency = backupConfig.syncConcurrency || (backupConfig.provider === 's3' ? 20 : 10);
|
||||
|
||||
syncer.sync(dataLayout, function processTask(task, iteratorCallback) {
|
||||
debug('sync: processing task: %j', task);
|
||||
// the empty task.path is special to signify the directory
|
||||
const destPath = task.path && backupConfig.key ? encryptFilePath(task.path, backupConfig.key) : task.path;
|
||||
@@ -329,16 +334,18 @@ function sync(backupConfig, backupId, dataDir, progressCallback, callback) {
|
||||
retryCallback = once(retryCallback); // protect again upload() erroring much later after read stream error
|
||||
|
||||
++retryCount;
|
||||
progressCallback({ message: `${task.operation} ${task.path} try ${retryCount}` });
|
||||
if (task.operation === 'add') {
|
||||
progressCallback({ message: `Adding ${task.path}` + (retryCount > 1 ? ` (Try ${retryCount})` : '') });
|
||||
debug(`Adding ${task.path} position ${task.position} try ${retryCount}`);
|
||||
var stream = createReadStream(path.join(dataDir, task.path), backupConfig.key || null);
|
||||
var stream = createReadStream(dataLayout.toLocalPath('./' + task.path), backupConfig.key || null);
|
||||
stream.on('error', function (error) {
|
||||
debug(`read stream error for ${task.path}: ${error.message}`);
|
||||
retryCallback();
|
||||
}); // ignore error if file disappears
|
||||
stream.on('progress', function(progress) {
|
||||
progressCallback({ message: `Uploading ${task.path}: ${Math.round(progress.transferred/1024/1024)}M@${Math.round(progress.speed/1024/1024)}` });
|
||||
const transferred = Math.round(progress.transferred/1024/1024), speed = Math.round(progress.speed/1024/1024);
|
||||
if (!transferred && !speed) return progressCallback({ message: `Uploading ${task.path}` }); // 0M@0Mbps looks wrong
|
||||
progressCallback({ message: `Uploading ${task.path}: ${transferred}M@${speed}Mbps` }); // 0M@0Mbps looks wrong
|
||||
});
|
||||
api(backupConfig.provider).upload(backupConfig, backupFilePath, stream, function (error) {
|
||||
debug(error ? `Error uploading ${task.path} try ${retryCount}: ${error.message}` : `Uploaded ${task.path}`);
|
||||
@@ -346,42 +353,52 @@ function sync(backupConfig, backupId, dataDir, progressCallback, callback) {
|
||||
});
|
||||
}
|
||||
}, iteratorCallback);
|
||||
}, backupConfig.syncConcurrency || 10 /* concurrency */, function (error) {
|
||||
}, concurrency, function (error) {
|
||||
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function saveFsMetadata(appDataDir, callback) {
|
||||
assert.strictEqual(typeof appDataDir, 'string');
|
||||
// this is not part of 'snapshotting' because we need root access to traverse
|
||||
function saveFsMetadata(dataLayout, metadataFile, callback) {
|
||||
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
|
||||
assert.strictEqual(typeof metadataFile, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var emptyDirs = safe.child_process.execSync('find . -type d -empty', { cwd: `${appDataDir}`, encoding: 'utf8' });
|
||||
if (emptyDirs === null) return callback(safe.error);
|
||||
|
||||
var execFiles = safe.child_process.execSync('find . -type f -executable', { cwd: `${appDataDir}`, encoding: 'utf8' });
|
||||
if (execFiles === null) return callback(safe.error);
|
||||
|
||||
var metadata = {
|
||||
emptyDirs: emptyDirs.length === 0 ? [ ] : emptyDirs.trim().split('\n'),
|
||||
execFiles: execFiles.length === 0 ? [ ] : execFiles.trim().split('\n')
|
||||
// contains paths prefixed with './'
|
||||
let metadata = {
|
||||
emptyDirs: [],
|
||||
execFiles: []
|
||||
};
|
||||
|
||||
if (!safe.fs.writeFileSync(`${appDataDir}/fsmetadata.json`, JSON.stringify(metadata, null, 4))) return callback(safe.error);
|
||||
for (let lp of dataLayout.localPaths()) {
|
||||
var emptyDirs = safe.child_process.execSync(`find ${lp} -type d -empty\n`, { encoding: 'utf8' });
|
||||
if (emptyDirs === null) return callback(safe.error);
|
||||
if (emptyDirs.length) metadata.emptyDirs = metadata.emptyDirs.concat(emptyDirs.trim().split('\n').map((ed) => dataLayout.toRemotePath(ed)));
|
||||
|
||||
var execFiles = safe.child_process.execSync(`find ${lp} -type f -executable\n`, { encoding: 'utf8' });
|
||||
if (execFiles === null) return callback(safe.error);
|
||||
|
||||
if (execFiles.length) metadata.execFiles = metadata.execFiles.concat(execFiles.trim().split('\n').map((ef) => dataLayout.toRemotePath(ef)));
|
||||
}
|
||||
|
||||
if (!safe.fs.writeFileSync(metadataFile, JSON.stringify(metadata, null, 4))) return callback(safe.error);
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
// this function is called via backupupload (since it needs root to traverse app's directory)
|
||||
function upload(backupId, format, dataDir, progressCallback, callback) {
|
||||
function upload(backupId, format, dataLayoutString, progressCallback, callback) {
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
assert.strictEqual(typeof format, 'string');
|
||||
assert.strictEqual(typeof dataDir, 'string');
|
||||
assert.strictEqual(typeof dataLayoutString, 'string');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`upload: id ${backupId} format ${format} dataDir ${dataDir}`);
|
||||
debug(`upload: id ${backupId} format ${format} dataLayout ${dataLayoutString}`);
|
||||
|
||||
const dataLayout = DataLayout.fromString(dataLayoutString);
|
||||
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
@@ -390,87 +407,99 @@ function upload(backupId, format, dataDir, progressCallback, callback) {
|
||||
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
|
||||
retryCallback = once(retryCallback); // protect again upload() erroring much later after tar stream error
|
||||
|
||||
var tarStream = createTarPackStream(dataDir, backupConfig.key || null);
|
||||
tarStream.on('progress', function(progress) {
|
||||
progressCallback({ message: `Uploading ${Math.round(progress.transferred/1024/1024)}M@${Math.round(progress.speed/1024/1024)}Mbps` });
|
||||
});
|
||||
tarStream.on('error', retryCallback); // already returns BackupsError
|
||||
tarPack(dataLayout, backupConfig.key || null, function (error, tarStream) {
|
||||
if (error) return retryCallback(error);
|
||||
|
||||
api(backupConfig.provider).upload(backupConfig, getBackupFilePath(backupConfig, backupId, format), tarStream, retryCallback);
|
||||
tarStream.on('progress', function(progress) {
|
||||
const transferred = Math.round(progress.transferred/1024/1024), speed = Math.round(progress.speed/1024/1024);
|
||||
if (!transferred && !speed) return progressCallback({ message: 'Uploading' }); // 0M@0Mbps looks wrong
|
||||
progressCallback({ message: `Uploading ${transferred}M@${speed}Mbps` });
|
||||
});
|
||||
tarStream.on('error', retryCallback); // already returns BackupsError
|
||||
|
||||
api(backupConfig.provider).upload(backupConfig, getBackupFilePath(backupConfig, backupId, format), tarStream, retryCallback);
|
||||
});
|
||||
}, callback);
|
||||
} else {
|
||||
async.series([
|
||||
saveFsMetadata.bind(null, dataDir),
|
||||
sync.bind(null, backupConfig, backupId, dataDir, progressCallback)
|
||||
saveFsMetadata.bind(null, dataLayout, `${dataLayout.localRoot()}/fsmetadata.json`),
|
||||
sync.bind(null, backupConfig, backupId, dataLayout, progressCallback)
|
||||
], callback);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function tarExtract(inStream, destination, key, callback) {
|
||||
function tarExtract(inStream, dataLayout, key, callback) {
|
||||
assert.strictEqual(typeof inStream, 'object');
|
||||
assert.strictEqual(typeof destination, 'string');
|
||||
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
|
||||
assert(key === null || typeof key === 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
callback = once(callback);
|
||||
|
||||
var gunzip = zlib.createGunzip({});
|
||||
var ps = progressStream({ time: 10000 }); // display a progress every 10 seconds
|
||||
var extract = tar.extract(destination);
|
||||
var extract = tar.extract('/', {
|
||||
map: function (header) {
|
||||
header.name = dataLayout.toLocalPath(header.name);
|
||||
return header;
|
||||
}
|
||||
});
|
||||
|
||||
const emitError = once((error) => ps.emit('error', error));
|
||||
|
||||
inStream.on('error', function (error) {
|
||||
debug('tarExtract: input stream error.', error);
|
||||
callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
||||
emitError(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
||||
});
|
||||
|
||||
gunzip.on('error', function (error) {
|
||||
debug('tarExtract: gunzip stream error.', error);
|
||||
callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
||||
emitError(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
||||
});
|
||||
|
||||
extract.on('error', function (error) {
|
||||
debug('tarExtract: extract stream error.', error);
|
||||
callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
||||
emitError(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
||||
});
|
||||
|
||||
extract.on('finish', function () {
|
||||
debug('tarExtract: done.');
|
||||
callback(null);
|
||||
// we use a separate event because ps is a through2 stream which emits 'finish' event indicating end of inStream and not extract
|
||||
ps.emit('done');
|
||||
});
|
||||
|
||||
if (key !== null) {
|
||||
var decrypt = crypto.createDecipher('aes-256-cbc', key);
|
||||
decrypt.on('error', function (error) {
|
||||
debug('tarExtract: decrypt stream error.', error);
|
||||
callback(new BackupsError(BackupsError.EXTERNAL_ERROR, `Failed to decrypt: ${error.message}`));
|
||||
emitError(new BackupsError(BackupsError.EXTERNAL_ERROR, `Failed to decrypt: ${error.message}`));
|
||||
});
|
||||
inStream.pipe(ps).pipe(decrypt).pipe(gunzip).pipe(extract);
|
||||
} else {
|
||||
inStream.pipe(ps).pipe(gunzip).pipe(extract);
|
||||
}
|
||||
|
||||
return ps;
|
||||
callback(null, ps);
|
||||
}
|
||||
|
||||
function restoreFsMetadata(appDataDir, callback) {
|
||||
assert.strictEqual(typeof appDataDir, 'string');
|
||||
function restoreFsMetadata(dataLayout, metadataFile, callback) {
|
||||
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
|
||||
assert.strictEqual(typeof metadataFile, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`Recreating empty directories in ${appDataDir}`);
|
||||
debug(`Recreating empty directories in ${dataLayout.toString()}`);
|
||||
|
||||
var metadataJson = safe.fs.readFileSync(path.join(appDataDir, 'fsmetadata.json'), 'utf8');
|
||||
var metadataJson = safe.fs.readFileSync(metadataFile, 'utf8');
|
||||
if (metadataJson === null) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Error loading fsmetadata.txt:' + safe.error.message));
|
||||
var metadata = safe.JSON.parse(metadataJson);
|
||||
if (metadata === null) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Error parsing fsmetadata.txt:' + safe.error.message));
|
||||
|
||||
async.eachSeries(metadata.emptyDirs, function createPath(emptyDir, iteratorDone) {
|
||||
mkdirp(path.join(appDataDir, emptyDir), iteratorDone);
|
||||
mkdirp(dataLayout.toLocalPath(emptyDir), iteratorDone);
|
||||
}, function (error) {
|
||||
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, `unable to create path: ${error.message}`));
|
||||
|
||||
async.eachSeries(metadata.execFiles, function createPath(execFile, iteratorDone) {
|
||||
fs.chmod(path.join(appDataDir, execFile), parseInt('0755', 8), iteratorDone);
|
||||
fs.chmod(dataLayout.toLocalPath(execFile), parseInt('0755', 8), iteratorDone);
|
||||
}, function (error) {
|
||||
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, `unable to chmod: ${error.message}`));
|
||||
|
||||
@@ -479,14 +508,14 @@ function restoreFsMetadata(appDataDir, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function downloadDir(backupConfig, backupFilePath, destDir, progressCallback, callback) {
|
||||
function downloadDir(backupConfig, backupFilePath, dataLayout, progressCallback, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof backupFilePath, 'string');
|
||||
assert.strictEqual(typeof destDir, 'string');
|
||||
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`downloadDir: ${backupFilePath} to ${destDir}`);
|
||||
debug(`downloadDir: ${backupFilePath} to ${dataLayout.toString()}`);
|
||||
|
||||
function downloadFile(entry, callback) {
|
||||
let relativePath = path.relative(backupFilePath, entry.fullPath);
|
||||
@@ -494,55 +523,79 @@ function downloadDir(backupConfig, backupFilePath, destDir, progressCallback, ca
|
||||
relativePath = decryptFilePath(relativePath, backupConfig.key);
|
||||
if (!relativePath) return callback(new BackupsError(BackupsError.BAD_STATE, 'Unable to decrypt file'));
|
||||
}
|
||||
const destFilePath = path.join(destDir, relativePath);
|
||||
const destFilePath = dataLayout.toLocalPath('./' + relativePath);
|
||||
|
||||
mkdirp(path.dirname(destFilePath), function (error) {
|
||||
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
||||
|
||||
api(backupConfig.provider).download(backupConfig, entry.fullPath, function (error, sourceStream) {
|
||||
if (error) return callback(error);
|
||||
|
||||
sourceStream.on('error', callback);
|
||||
|
||||
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
|
||||
let destStream = createWriteStream(destFilePath, backupConfig.key || null);
|
||||
destStream.on('error', callback);
|
||||
|
||||
progressCallback({ message: `Downloading ${entry.fullPath} to ${destFilePath}` });
|
||||
// protect against multiple errors. must destroy the write stream so that a previous retry does not write
|
||||
let closeAndRetry = once((error) => {
|
||||
if (error) progressCallback({ message: `Download ${entry.fullPath} errored: ${error.message}` });
|
||||
else progressCallback({ message: `Download ${entry.fullPath} finished` });
|
||||
destStream.destroy();
|
||||
retryCallback(error);
|
||||
});
|
||||
|
||||
sourceStream.pipe(destStream, { end: true }).on('finish', callback);
|
||||
});
|
||||
api(backupConfig.provider).download(backupConfig, entry.fullPath, function (error, sourceStream) {
|
||||
if (error) return closeAndRetry(error);
|
||||
|
||||
sourceStream.on('error', closeAndRetry);
|
||||
destStream.on('error', closeAndRetry);
|
||||
|
||||
progressCallback({ message: `Downloading ${entry.fullPath} to ${destFilePath}` });
|
||||
|
||||
sourceStream.pipe(destStream, { end: true }).on('finish', closeAndRetry);
|
||||
});
|
||||
}, callback);
|
||||
});
|
||||
}
|
||||
|
||||
api(backupConfig.provider).listDir(backupConfig, backupFilePath, 1000, function (entries, done) {
|
||||
async.each(entries, downloadFile, done);
|
||||
// https://www.digitalocean.com/community/questions/rate-limiting-on-spaces?answer=40441
|
||||
const concurrency = backupConfig.downloadConcurrency || (backupConfig.provider === 's3' ? 30 : 10);
|
||||
|
||||
async.eachLimit(entries, concurrency, downloadFile, done);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function download(backupConfig, backupId, format, dataDir, progressCallback, callback) {
|
||||
function download(backupConfig, backupId, format, dataLayout, progressCallback, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
assert.strictEqual(typeof format, 'string');
|
||||
assert.strictEqual(typeof dataDir, 'string');
|
||||
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`download - Downloading ${backupId} of format ${format} to ${dataDir}`);
|
||||
debug(`download - Downloading ${backupId} of format ${format} to ${dataLayout.toString()}`);
|
||||
|
||||
const backupFilePath = getBackupFilePath(backupConfig, backupId, format);
|
||||
|
||||
if (format === 'tgz') {
|
||||
api(backupConfig.provider).download(backupConfig, getBackupFilePath(backupConfig, backupId, format), function (error, sourceStream) {
|
||||
if (error) return callback(error);
|
||||
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
|
||||
api(backupConfig.provider).download(backupConfig, backupFilePath, function (error, sourceStream) {
|
||||
if (error) return retryCallback(error);
|
||||
|
||||
let ps = tarExtract(sourceStream, dataDir, backupConfig.key || null, callback);
|
||||
ps.on('progress', function (progress) {
|
||||
progressCallback({ message: `Downloading ${Math.round(progress.transferred/1024/1024)}M@${Math.round(progress.speed/1024/1024)}Mbps` });
|
||||
tarExtract(sourceStream, dataLayout, backupConfig.key || null, function (error, ps) {
|
||||
if (error) return retryCallback(error);
|
||||
|
||||
ps.on('progress', function (progress) {
|
||||
const transferred = Math.round(progress.transferred/1024/1024), speed = Math.round(progress.speed/1024/1024);
|
||||
if (!transferred && !speed) return progressCallback({ message: 'Downloading' }); // 0M@0Mbps looks wrong
|
||||
progressCallback({ message: `Downloading ${transferred}M@${speed}Mbps` });
|
||||
});
|
||||
ps.on('error', retryCallback);
|
||||
ps.on('done', retryCallback);
|
||||
});
|
||||
});
|
||||
});
|
||||
}, callback);
|
||||
} else {
|
||||
downloadDir(backupConfig, getBackupFilePath(backupConfig, backupId, format), dataDir, progressCallback, function (error) {
|
||||
downloadDir(backupConfig, backupFilePath, dataLayout, progressCallback, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
restoreFsMetadata(dataDir, callback);
|
||||
restoreFsMetadata(dataLayout, `${dataLayout.localRoot()}/fsmetadata.json`, callback);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -553,12 +606,14 @@ function restore(backupConfig, backupId, progressCallback, callback) {
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
download(backupConfig, backupId, backupConfig.format, paths.BOX_DATA_DIR, progressCallback, function (error) {
|
||||
const dataLayout = new DataLayout(paths.BOX_DATA_DIR, []);
|
||||
|
||||
download(backupConfig, backupId, backupConfig.format, dataLayout, progressCallback, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('restore: download completed, importing database');
|
||||
|
||||
database.importFromFile(`${paths.BOX_DATA_DIR}/box.mysqldump`, function (error) {
|
||||
database.importFromFile(`${dataLayout.localRoot()}/box.mysqldump`, function (error) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
debug('restore: database imported');
|
||||
@@ -575,7 +630,9 @@ function restoreApp(app, addonsToRestore, restoreConfig, progressCallback, callb
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var appDataDir = safe.fs.realpathSync(path.join(paths.APPS_DATA_DIR, app.id));
|
||||
const appDataDir = safe.fs.realpathSync(path.join(paths.APPS_DATA_DIR, app.id));
|
||||
if (!appDataDir) return callback(safe.error);
|
||||
const dataLayout = new DataLayout(appDataDir, app.dataDir ? [{ localDir: app.dataDir, remoteDir: 'data' }] : []);
|
||||
|
||||
var startTime = new Date();
|
||||
|
||||
@@ -583,7 +640,7 @@ function restoreApp(app, addonsToRestore, restoreConfig, progressCallback, callb
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
async.series([
|
||||
download.bind(null, backupConfig, restoreConfig.backupId, restoreConfig.backupFormat, appDataDir, progressCallback),
|
||||
download.bind(null, backupConfig, restoreConfig.backupId, restoreConfig.backupFormat, dataLayout, progressCallback),
|
||||
addons.restoreAddons.bind(null, app, addonsToRestore)
|
||||
], function (error) {
|
||||
debug('restoreApp: time: %s', (new Date() - startTime)/1000);
|
||||
@@ -593,16 +650,16 @@ function restoreApp(app, addonsToRestore, restoreConfig, progressCallback, callb
|
||||
});
|
||||
}
|
||||
|
||||
function runBackupUpload(backupId, format, dataDir, progressCallback, callback) {
|
||||
function runBackupUpload(backupId, format, dataLayout, progressCallback, callback) {
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
assert.strictEqual(typeof format, 'string');
|
||||
assert.strictEqual(typeof dataDir, 'string');
|
||||
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
let result = '';
|
||||
|
||||
shell.sudo(`backup-${backupId}`, [ BACKUP_UPLOAD_CMD, backupId, format, dataDir ], { preserveEnv: true, ipc: true }, function (error) {
|
||||
shell.sudo(`backup-${backupId}`, [ BACKUP_UPLOAD_CMD, backupId, format, dataLayout.toString() ], { preserveEnv: true, ipc: true }, function (error) {
|
||||
if (error && (error.code === null /* signal */ || (error.code !== 0 && error.code !== 50))) { // backuptask crashed
|
||||
return callback(new BackupsError(BackupsError.INTERNAL_ERROR, 'Backuptask crashed'));
|
||||
} else if (error && error.code === 50) { // exited with error
|
||||
@@ -662,7 +719,11 @@ function uploadBoxSnapshot(backupConfig, progressCallback, callback) {
|
||||
snapshotBox(progressCallback, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
runBackupUpload('snapshot/box', backupConfig.format, paths.BOX_DATA_DIR, progressCallback, function (error) {
|
||||
const boxDataDir = safe.fs.realpathSync(paths.BOX_DATA_DIR);
|
||||
if (!boxDataDir) return callback(safe.error);
|
||||
|
||||
const dataLayout = new DataLayout(boxDataDir, []);
|
||||
runBackupUpload('snapshot/box', backupConfig.format, dataLayout, progressCallback, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('uploadBoxSnapshot: time: %s secs', (new Date() - startTime)/1000);
|
||||
@@ -771,7 +832,7 @@ function snapshotApp(app, progressCallback, callback) {
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
progressCallback({ message: `Snapshotting app ${app.id}` });
|
||||
progressCallback({ message: `Snapshotting app ${app.fqdn}` });
|
||||
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/config.json'), JSON.stringify(apps.getAppConfig(app)))) {
|
||||
return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Error creating config.json: ' + safe.error.message));
|
||||
@@ -834,9 +895,13 @@ function uploadAppSnapshot(backupConfig, app, progressCallback, callback) {
|
||||
snapshotApp(app, progressCallback, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var backupId = util.format('snapshot/app_%s', app.id);
|
||||
var appDataDir = safe.fs.realpathSync(path.join(paths.APPS_DATA_DIR, app.id));
|
||||
runBackupUpload(backupId, backupConfig.format, appDataDir, progressCallback, function (error) {
|
||||
const backupId = util.format('snapshot/app_%s', app.id);
|
||||
const appDataDir = safe.fs.realpathSync(path.join(paths.APPS_DATA_DIR, app.id));
|
||||
if (!appDataDir) return callback(safe.error);
|
||||
|
||||
const dataLayout = new DataLayout(appDataDir, app.dataDir ? [{ localDir: app.dataDir, remoteDir: 'data' }] : []);
|
||||
|
||||
runBackupUpload(backupId, backupConfig.format, dataLayout, progressCallback, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debugApp(app, 'uploadAppSnapshot: %s done time: %s secs', backupId, (new Date() - startTime)/1000);
|
||||
@@ -924,9 +989,9 @@ function backupBoxAndApps(progressCallback, callback) {
|
||||
|
||||
function startBackupTask(auditSource, callback) {
|
||||
let error = locker.lock(locker.OP_FULL_BACKUP);
|
||||
if (error) return callback(error);
|
||||
if (error) return callback(new BackupsError(BackupsError.BAD_STATE, `Cannot backup now: ${error.message}`));
|
||||
|
||||
let task = tasks.startTask(tasks.TASK_BACKUP, [], auditSource);
|
||||
let task = tasks.startTask(tasks.TASK_BACKUP, []);
|
||||
task.on('error', (error) => callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)));
|
||||
task.on('start', (taskId) => {
|
||||
eventlog.add(eventlog.ACTION_BACKUP_START, auditSource, { taskId });
|
||||
@@ -1006,22 +1071,24 @@ function cleanupAppBackups(backupConfig, referencedAppBackups, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const now = new Date();
|
||||
let removedAppBackups = [];
|
||||
|
||||
// we clean app backups of any state because the ones to keep are determined by the box cleanup code
|
||||
backupdb.getByTypePaged(backupdb.BACKUP_TYPE_APP, 1, 1000, function (error, appBackups) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
async.eachSeries(appBackups, function iterator(backup, iteratorDone) {
|
||||
if (referencedAppBackups.indexOf(backup.id) !== -1) return iteratorDone();
|
||||
if ((now - backup.creationTime) < (backupConfig.retentionSecs * 1000)) return iteratorDone();
|
||||
async.eachSeries(appBackups, function iterator(appBackup, iteratorDone) {
|
||||
if (referencedAppBackups.indexOf(appBackup.id) !== -1) return iteratorDone();
|
||||
if ((now - appBackup.creationTime) < (backupConfig.retentionSecs * 1000)) return iteratorDone();
|
||||
|
||||
debug('cleanupAppBackups: removing %s', backup.id);
|
||||
debug('cleanupAppBackups: removing %s', appBackup.id);
|
||||
|
||||
cleanupBackup(backupConfig, backup, iteratorDone);
|
||||
removedAppBackups.push(appBackup.id);
|
||||
cleanupBackup(backupConfig, appBackup, iteratorDone);
|
||||
}, function () {
|
||||
debug('cleanupAppBackups: done');
|
||||
|
||||
callback();
|
||||
callback(null, removedAppBackups);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1032,12 +1099,12 @@ function cleanupBoxBackups(backupConfig, auditSource, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const now = new Date();
|
||||
var referencedAppBackups = [];
|
||||
let referencedAppBackups = [], removedBoxBackups = [];
|
||||
|
||||
backupdb.getByTypePaged(backupdb.BACKUP_TYPE_BOX, 1, 1000, function (error, boxBackups) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (boxBackups.length === 0) return callback(null, []);
|
||||
if (boxBackups.length === 0) return callback(null, { removedBoxBackups, referencedAppBackups });
|
||||
|
||||
// search for the first valid backup
|
||||
var i;
|
||||
@@ -1054,21 +1121,22 @@ function cleanupBoxBackups(backupConfig, auditSource, callback) {
|
||||
debug('cleanupBoxBackups: no box backup to preserve');
|
||||
}
|
||||
|
||||
async.eachSeries(boxBackups, function iterator(backup, nextBackup) {
|
||||
async.eachSeries(boxBackups, function iterator(boxBackup, iteratorNext) {
|
||||
// TODO: errored backups should probably be cleaned up before retention time, but we will
|
||||
// have to be careful not to remove any backup currently being created
|
||||
if ((now - backup.creationTime) < (backupConfig.retentionSecs * 1000)) {
|
||||
referencedAppBackups = referencedAppBackups.concat(backup.dependsOn);
|
||||
return nextBackup();
|
||||
if ((now - boxBackup.creationTime) < (backupConfig.retentionSecs * 1000)) {
|
||||
referencedAppBackups = referencedAppBackups.concat(boxBackup.dependsOn);
|
||||
return iteratorNext();
|
||||
}
|
||||
|
||||
debug('cleanupBoxBackups: removing %s', backup.id);
|
||||
debug('cleanupBoxBackups: removing %s', boxBackup.id);
|
||||
|
||||
cleanupBackup(backupConfig, backup, nextBackup);
|
||||
removedBoxBackups.push(boxBackup.id);
|
||||
cleanupBackup(backupConfig, boxBackup, iteratorNext);
|
||||
}, function () {
|
||||
debug('cleanupBoxBackups: done');
|
||||
|
||||
return callback(null, referencedAppBackups);
|
||||
callback(null, { removedBoxBackups, referencedAppBackups });
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1122,29 +1190,53 @@ function cleanupSnapshots(backupConfig, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function cleanup(auditSource, callback) {
|
||||
function cleanup(auditSource, progressCallback, callback) {
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert(!callback || typeof callback === 'function'); // callback is null when called from cronjob
|
||||
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (backupConfig.retentionSecs < 0) {
|
||||
debug('cleanup: keeping all backups');
|
||||
return callback();
|
||||
return callback(null, {});
|
||||
}
|
||||
|
||||
cleanupBoxBackups(backupConfig, auditSource, function (error, referencedAppBackups) {
|
||||
progressCallback({ percent: 10, message: 'Cleaning box backups' });
|
||||
|
||||
cleanupBoxBackups(backupConfig, auditSource, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
cleanupAppBackups(backupConfig, referencedAppBackups, function (error) {
|
||||
progressCallback({ percent: 40, message: 'Cleaning app backups' });
|
||||
|
||||
cleanupAppBackups(backupConfig, result.referencedAppBackups, function (error, removedAppBackups) {
|
||||
if (error) return callback(error);
|
||||
|
||||
cleanupSnapshots(backupConfig, callback);
|
||||
progressCallback({ percent: 90, message: 'Cleaning snapshots' });
|
||||
|
||||
cleanupSnapshots(backupConfig, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, { removedBoxBackups: result.removedBoxBackups, removedAppBackups: removedAppBackups });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function startCleanupTask(auditSource, callback) {
|
||||
let task = tasks.startTask(tasks.TASK_CLEAN_BACKUPS, [ auditSource ]);
|
||||
task.on('error', (error) => callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)));
|
||||
task.on('start', (taskId) => {
|
||||
eventlog.add(eventlog.ACTION_BACKUP_CLEANUP_START, auditSource, { taskId });
|
||||
callback(null, taskId);
|
||||
});
|
||||
task.on('finish', (error, result) => { // result is { removedBoxBackups, removedAppBackups }
|
||||
eventlog.add(eventlog.ACTION_BACKUP_CLEANUP_FINISH, auditSource, {
|
||||
errorMessage: error ? error.message : null,
|
||||
removedBoxBackups: result ? result.removedBoxBackups : [],
|
||||
removedAppBackups: result ? result.removedAppBackups : []
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+2
-2
@@ -226,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) {
|
||||
@@ -290,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) {
|
||||
|
||||
+29
-99
@@ -8,26 +8,24 @@ exports = module.exports = {
|
||||
getConfig: getConfig,
|
||||
getDisks: getDisks,
|
||||
getLogs: getLogs,
|
||||
getStatus: getStatus,
|
||||
|
||||
reboot: reboot,
|
||||
isRebootRequired: isRebootRequired,
|
||||
|
||||
onActivated: onActivated,
|
||||
|
||||
prepareDashboardDomain: prepareDashboardDomain,
|
||||
setDashboardDomain: setDashboardDomain,
|
||||
renewCerts: renewCerts,
|
||||
|
||||
checkDiskSpace: checkDiskSpace,
|
||||
|
||||
configureWebadmin: configureWebadmin,
|
||||
getWebadminStatus: getWebadminStatus
|
||||
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'),
|
||||
@@ -44,7 +42,6 @@ var assert = require('assert'),
|
||||
shell = require('./shell.js'),
|
||||
spawn = require('child_process').spawn,
|
||||
split = require('split'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
tasks = require('./tasks.js'),
|
||||
users = require('./users.js'),
|
||||
util = require('util');
|
||||
@@ -53,16 +50,6 @@ var REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh');
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
let gWebadminStatus = {
|
||||
dns: false,
|
||||
tls: false,
|
||||
configuring: false,
|
||||
restore: {
|
||||
active: false,
|
||||
error: null
|
||||
}
|
||||
};
|
||||
|
||||
function CloudronError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
@@ -123,7 +110,7 @@ function runStartupTasks() {
|
||||
reverseProxy.configureDefaultServer(NOOP_CALLBACK);
|
||||
|
||||
// always generate webadmin config since we have no versioning mechanism for the ejs
|
||||
configureWebadmin(NOOP_CALLBACK);
|
||||
if (config.adminDomain()) reverseProxy.writeAdminConfig(config.adminDomain(), NOOP_CALLBACK);
|
||||
|
||||
// check activation state and start the platform
|
||||
users.isActivated(function (error, activated) {
|
||||
@@ -241,15 +228,13 @@ 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);
|
||||
|
||||
@@ -283,75 +268,16 @@ function getLogs(unit, options, callback) {
|
||||
return callback(null, transformStream);
|
||||
}
|
||||
|
||||
function configureWebadmin(callback) {
|
||||
function prepareDashboardDomain(domain, auditSource, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('configureWebadmin: adminDomain:%s status:%j', config.adminDomain(), gWebadminStatus);
|
||||
debug(`prepareDashboardDomain: ${domain}`);
|
||||
|
||||
if (process.env.BOX_ENV === 'test' || !config.adminDomain() || gWebadminStatus.configuring) return callback();
|
||||
|
||||
gWebadminStatus.configuring = true; // re-entracy guard
|
||||
|
||||
function configureReverseProxy(error) {
|
||||
debug('configureReverseProxy: error %j', error || null);
|
||||
|
||||
reverseProxy.configureAdmin({ userId: null, username: 'setup' }, function (error) {
|
||||
debug('configureWebadmin: done error: %j', error || {});
|
||||
gWebadminStatus.configuring = false;
|
||||
|
||||
if (error) return callback(error);
|
||||
|
||||
gWebadminStatus.tls = true;
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
// update the DNS. configure nginx regardless of whether it succeeded so that
|
||||
// box is accessible even if dns creds are invalid
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return configureReverseProxy(error);
|
||||
|
||||
domains.upsertDnsRecords(config.adminLocation(), config.adminDomain(), 'A', [ ip ], function (error) {
|
||||
debug('addWebadminDnsRecord: updated records with error:', error);
|
||||
if (error) return configureReverseProxy(error);
|
||||
|
||||
domains.waitForDnsRecord(config.adminLocation(), config.adminDomain(), 'A', ip, { interval: 30000, times: 50000 }, function (error) {
|
||||
if (error) return configureReverseProxy(error);
|
||||
|
||||
gWebadminStatus.dns = true;
|
||||
|
||||
configureReverseProxy();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getWebadminStatus() {
|
||||
return gWebadminStatus;
|
||||
}
|
||||
|
||||
function getStatus(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
users.isActivated(function (error, activated) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
settings.getCloudronName(function (error, cloudronName) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, {
|
||||
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(),
|
||||
webadminStatus: gWebadminStatus // only valid when !activated
|
||||
});
|
||||
});
|
||||
});
|
||||
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) {
|
||||
@@ -360,20 +286,24 @@ function setDashboardDomain(domain, callback) {
|
||||
|
||||
debug(`setDashboardDomain: ${domain}`);
|
||||
|
||||
domains.get(domain, function (error, result) {
|
||||
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));
|
||||
|
||||
config.setAdminDomain(result.domain);
|
||||
config.setAdminLocation('my');
|
||||
config.setAdminFqdn('my' + (result.config.hyphenatedSubdomains ? '-' : '.') + result.domain);
|
||||
|
||||
clients.addDefaultClients(config.adminOrigin(), function (error) {
|
||||
reverseProxy.writeAdminConfig(domain, function (error) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
const fqdn = domains.fqdn(constants.ADMIN_LOCATION, domainObject);
|
||||
|
||||
configureWebadmin(NOOP_CALLBACK); // ## trigger as task
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+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
|
||||
|
||||
+1
-2
@@ -17,9 +17,8 @@ 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',
|
||||
|
||||
+1
-1
@@ -153,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
|
||||
});
|
||||
|
||||
+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}`));
|
||||
|
||||
+27
-21
@@ -54,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');
|
||||
@@ -373,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) {
|
||||
@@ -517,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',
|
||||
@@ -541,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) {
|
||||
@@ -552,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();
|
||||
});
|
||||
}
|
||||
|
||||
+80
-63
@@ -10,6 +10,7 @@ module.exports = exports = {
|
||||
isLocked: isLocked,
|
||||
|
||||
fqdn: fqdn,
|
||||
getName: getName,
|
||||
|
||||
getDnsRecords: getDnsRecords,
|
||||
upsertDnsRecords: upsertDnsRecords,
|
||||
@@ -26,13 +27,13 @@ module.exports = exports = {
|
||||
|
||||
parentDomain: parentDomain,
|
||||
|
||||
DomainsError: DomainsError,
|
||||
prepareDashboardDomain: prepareDashboardDomain,
|
||||
|
||||
// exported for testing
|
||||
_getName: getName
|
||||
DomainsError: DomainsError
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
@@ -90,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');
|
||||
@@ -102,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));
|
||||
@@ -224,23 +226,19 @@ function add(domain, data, auditSource, callback) {
|
||||
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(config, domain, zoneName, provider, ip, function (error, sanitizedConfig) {
|
||||
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: sanitizedConfig, 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 });
|
||||
|
||||
eventlog.add(eventlog.ACTION_DOMAIN_ADD, auditSource, { domain, zoneName, provider });
|
||||
|
||||
callback();
|
||||
});
|
||||
callback();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -318,25 +316,21 @@ function update(domain, data, auditSource, callback) {
|
||||
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(config, domain, zoneName, provider, ip, function (error, sanitizedConfig) {
|
||||
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: sanitizedConfig, 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));
|
||||
|
||||
eventlog.add(eventlog.ACTION_DOMAIN_UPDATE, auditSource, { domain, zoneName, provider });
|
||||
|
||||
callback();
|
||||
});
|
||||
callback();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -372,39 +366,36 @@ function clear(callback) {
|
||||
}
|
||||
|
||||
// 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);
|
||||
@@ -413,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);
|
||||
@@ -433,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);
|
||||
@@ -453,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');
|
||||
@@ -464,9 +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);
|
||||
api(domainObject.provider).wait(domainObject, location, type, value, options, callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -494,3 +483,31 @@ function makeWildcard(hostname) {
|
||||
parts[0] = '*';
|
||||
return parts.join('.');
|
||||
}
|
||||
|
||||
function prepareDashboardDomain(domain, auditSource, progressCallback, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
get(domain, function (error, domainObject) {
|
||||
if (error) return callback(error);
|
||||
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error.message));
|
||||
|
||||
async.series([
|
||||
(done) => { progressCallback({ percent: 10, message: 'Updating DNS' }); done(); },
|
||||
upsertDnsRecords.bind(null, constants.ADMIN_LOCATION, domain, 'A', [ ip ]),
|
||||
(done) => { progressCallback({ percent: 40, message: 'Waiting for DNS' }); done(); },
|
||||
waitForDnsRecord.bind(null, constants.ADMIN_LOCATION, domain, 'A', ip, { interval: 30000, times: 50000 }),
|
||||
(done) => { progressCallback({ percent: 70, message: 'Getting certificate' }); done(); },
|
||||
reverseProxy.ensureCertificate.bind(null, fqdn(constants.ADMIN_LOCATION, domainObject), domain, auditSource)
|
||||
], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
+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();
|
||||
});
|
||||
|
||||
+29
-1
@@ -18,10 +18,14 @@ 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',
|
||||
@@ -47,12 +51,17 @@ exports = module.exports = {
|
||||
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');
|
||||
|
||||
@@ -93,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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ exports = module.exports = {
|
||||
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:2.0.2@sha256:6dcee0731dfb9b013ed94d56205eee219040ee806c7e251db3b3886eaa4947ff' },
|
||||
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:2.0.2@sha256:95e006390ddce7db637e1672eb6f3c257d3c2652747424f529b1dee3cbe6728c' },
|
||||
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.0.0@sha256:8a88dd334b62b578530a014ca1a2425a54cb9df1e475f5d3a36806e5cfa22121' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.0.1@sha256:deee3739011670d45abd8997a8a0b8d3c4cd577a93f235417614dea58338e0f9' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.1.0@sha256:131db42dcb90111f679ab1f0f37c552f93f797d9b803b2346c7c202daf86ac36' },
|
||||
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.0.2@sha256:454f035d60b768153d4f31210380271b5ba1c09367c9d95c7fa37f9e39d2f59c' }
|
||||
}
|
||||
};
|
||||
|
||||
+1
-1
@@ -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) {
|
||||
|
||||
+11
-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,7 +15,6 @@ 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');
|
||||
@@ -29,50 +28,32 @@ function collectLogs(unitName, callback) {
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+7
-7
@@ -141,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);
|
||||
});
|
||||
@@ -493,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
|
||||
@@ -503,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 +552,10 @@ function restartMail(callback) {
|
||||
|
||||
const tag = infra.images.mail.tag;
|
||||
const memoryLimit = 4 * 256;
|
||||
const cloudronToken = hat(8 * 128);
|
||||
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
|
||||
@@ -586,6 +585,7 @@ function restartMail(callback) {
|
||||
--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} \
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Dear <%= cloudronName %> Admin,
|
||||
|
||||
Unfortunately <%= program %> exited unexpectedly!
|
||||
<%= subject %>
|
||||
|
||||
Please see some excerpt of the logs below:
|
||||
|
||||
|
||||
+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);
|
||||
});
|
||||
}
|
||||
+1
-1
@@ -8,7 +8,6 @@ exports = module.exports = {
|
||||
CLOUDRON_DEFAULT_AVATAR_FILE: path.join(__dirname + '/../assets/avatar.png'),
|
||||
INFRA_VERSION_FILE: path.join(config.baseDir(), 'platformdata/INFRA_VERSION'),
|
||||
|
||||
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'),
|
||||
@@ -23,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'),
|
||||
|
||||
+96
-39
@@ -4,6 +4,7 @@ exports = module.exports = {
|
||||
setup: setup,
|
||||
restore: restore,
|
||||
activate: activate,
|
||||
getStatus: getStatus,
|
||||
|
||||
ProvisionError: ProvisionError
|
||||
};
|
||||
@@ -21,21 +22,32 @@ var assert = require('assert'),
|
||||
DomainsError = domains.DomainsError,
|
||||
eventlog = require('./eventlog.js'),
|
||||
mail = require('./mail.js'),
|
||||
path = require('path'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
settingsdb = require('./settingsdb.js'),
|
||||
settings = require('./settings.js'),
|
||||
shell = require('./shell.js'),
|
||||
superagent = require('superagent'),
|
||||
users = require('./users.js'),
|
||||
UsersError = users.UsersError,
|
||||
tld = require('tldjs'),
|
||||
util = require('util');
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
var RESTART_CMD = path.join(__dirname, 'scripts/restart.sh');
|
||||
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
var 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');
|
||||
@@ -63,6 +75,12 @@ 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');
|
||||
@@ -102,7 +120,7 @@ function unprovision(callback) {
|
||||
|
||||
config.setAdminDomain('');
|
||||
config.setAdminFqdn('');
|
||||
config.setAdminLocation('my');
|
||||
config.setAdminLocation(constants.ADMIN_LOCATION);
|
||||
|
||||
// TODO: also cancel any existing configureWebadmin task
|
||||
async.series([
|
||||
@@ -117,23 +135,27 @@ function setup(dnsConfig, autoconf, auditSource, callback) {
|
||||
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 callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
|
||||
if (activated) return callback(new ProvisionError(ProvisionError.ALREADY_SETUP));
|
||||
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 callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
|
||||
|
||||
let webadminStatus = cloudron.getWebadminStatus();
|
||||
|
||||
if (webadminStatus.configuring || webadminStatus.restore.active) return callback(new ProvisionError(ProvisionError.BAD_STATE, 'Already restoring or configuring'));
|
||||
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);
|
||||
|
||||
const adminFqdn = 'my' + (dnsConfig.config.hyphenatedSubdomains ? '-' : '.') + domain;
|
||||
|
||||
debug(`provision: Setting up Cloudron with domain ${domain} and zone ${zoneName} using admin fqdn ${adminFqdn}`);
|
||||
debug(`provision: Setting up Cloudron with domain ${domain} and zone ${zoneName}`);
|
||||
|
||||
let data = {
|
||||
zoneName: zoneName,
|
||||
@@ -144,16 +166,24 @@ function setup(dnsConfig, autoconf, auditSource, callback) {
|
||||
};
|
||||
|
||||
domains.add(domain, data, auditSource, function (error) {
|
||||
if (error && error.reason === DomainsError.BAD_FIELD) return callback(new ProvisionError(ProvisionError.BAD_FIELD, error.message));
|
||||
if (error && error.reason === DomainsError.ALREADY_EXISTS) return callback(new ProvisionError(ProvisionError.BAD_FIELD, error.message));
|
||||
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, 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([
|
||||
mail.addDomain.bind(null, domain),
|
||||
cloudron.setDashboardDomain.bind(null, domain), // triggers task to setup my. dns/cert/reverseproxy
|
||||
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, { })
|
||||
], callback);
|
||||
], function (error) {
|
||||
gProvisionStatus.setup.active = false;
|
||||
gProvisionStatus.setup.errorMessage = error ? error.message : '';
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -229,40 +259,67 @@ function restore(backupConfig, backupId, version, autoconf, auditSource, callbac
|
||||
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`));
|
||||
|
||||
let webadminStatus = cloudron.getWebadminStatus();
|
||||
if (gProvisionStatus.setup.active || gProvisionStatus.restore.active) return callback(new ProvisionError(ProvisionError.BAD_STATE, 'Already setting up or restoring'));
|
||||
|
||||
if (webadminStatus.configuring || webadminStatus.restore.active) return callback(new ProvisionError(ProvisionError.BAD_STATE, 'Already restoring or configuring'));
|
||||
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 callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
|
||||
if (activated) return callback(new ProvisionError(ProvisionError.ALREADY_PROVISIONED, 'Already 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 callback(new ProvisionError(ProvisionError.BAD_FIELD, error.message));
|
||||
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new ProvisionError(ProvisionError.EXTERNAL_ERROR, error.message));
|
||||
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, 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}`);
|
||||
|
||||
webadminStatus.restore.active = true;
|
||||
webadminStatus.restore.error = null;
|
||||
|
||||
callback(null); // do no block
|
||||
callback(); // now that the fields are validated, continue task in the background
|
||||
|
||||
async.series([
|
||||
backups.restore.bind(null, backupConfig, backupId, (progress) => debug(`restore: ${progress}`)),
|
||||
eventlog.add.bind(null, eventlog.ACTION_RESTORE, auditSource, { backupId }),
|
||||
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()),
|
||||
shell.sudo.bind(null, 'restart', [ RESTART_CMD ], {})
|
||||
eventlog.add.bind(null, eventlog.ACTION_RESTORE, auditSource, { backupId }),
|
||||
], function (error) {
|
||||
debug('restore:', error);
|
||||
if (error) webadminStatus.restore.error = error.message;
|
||||
webadminStatus.restore.active = false;
|
||||
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));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+51
-21
@@ -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,7 +57,7 @@ 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' }),
|
||||
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/appconfig.ejs', { encoding: 'utf8' }),
|
||||
RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh');
|
||||
|
||||
function ReverseProxyError(reason, errorOrMessage) {
|
||||
@@ -313,17 +316,18 @@ function getCertificateByHostname(hostname, domainObject, callback) {
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function getCertificate(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
function getCertificate(fqdn, domain, callback) {
|
||||
assert.strictEqual(typeof fqdn, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
domains.get(app.domain, function (error, domainObject) {
|
||||
domains.get(domain, function (error, domainObject) {
|
||||
if (error) return callback(error);
|
||||
|
||||
getCertificateByHostname(app.fqdn, domainObject, function (error, result) {
|
||||
getCertificateByHostname(fqdn, domainObject, function (error, result) {
|
||||
if (error || result) return callback(error, result);
|
||||
|
||||
return getFallbackCertificate(app.domain, callback);
|
||||
return getFallbackCertificate(domain, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -373,7 +377,7 @@ function ensureCertificate(vhost, domain, auditSource, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
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');
|
||||
@@ -395,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');
|
||||
@@ -442,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');
|
||||
@@ -481,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);
|
||||
});
|
||||
@@ -519,7 +549,7 @@ function renewCerts(options, auditSource, progressCallback, 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) {
|
||||
@@ -549,9 +579,9 @@ function renewCerts(options, auditSource, progressCallback, 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);
|
||||
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) {
|
||||
@@ -575,7 +605,7 @@ function renewAll(auditSource, callback) {
|
||||
|
||||
function removeAppConfigs() {
|
||||
for (let appConfigFile of fs.readdirSync(paths.NGINX_APPCONFIG_DIR)) {
|
||||
if (appConfigFile !== constants.NGINX_DEFAULT_CONFIG_FILE_NAME && appConfigFile !== constants.NGINX_ADMIN_CONFIG_FILE_NAME) {
|
||||
if (appConfigFile !== constants.NGINX_DEFAULT_CONFIG_FILE_NAME && !appConfigFile.startsWith(constants.ADMIN_LOCATION)) {
|
||||
fs.unlinkSync(path.join(paths.NGINX_APPCONFIG_DIR, appConfigFile));
|
||||
}
|
||||
}
|
||||
@@ -597,7 +627,7 @@ function configureDefaultServer(callback) {
|
||||
}
|
||||
}
|
||||
|
||||
writeAdminConfig({ certFilePath, keyFilePath }, constants.NGINX_DEFAULT_CONFIG_FILE_NAME, '', function (error) {
|
||||
writeAdminNginxConfig({ certFilePath, keyFilePath }, constants.NGINX_DEFAULT_CONFIG_FILE_NAME, '', function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('configureDefaultServer: done');
|
||||
|
||||
+7
-4
@@ -210,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) {
|
||||
@@ -367,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'; }
|
||||
@@ -376,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) {
|
||||
@@ -405,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);
|
||||
@@ -413,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) {
|
||||
|
||||
+13
-34
@@ -7,18 +7,15 @@ exports = module.exports = {
|
||||
getDisks: getDisks,
|
||||
getUpdateInfo: getUpdateInfo,
|
||||
update: update,
|
||||
feedback: feedback,
|
||||
checkForUpdates: checkForUpdates,
|
||||
getLogs: getLogs,
|
||||
getLogStream: getLogStream,
|
||||
getStatus: getStatus,
|
||||
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,
|
||||
@@ -26,8 +23,7 @@ var appstore = require('../appstore.js'),
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
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;
|
||||
@@ -91,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) {
|
||||
@@ -140,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'; }
|
||||
@@ -150,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) {
|
||||
@@ -186,11 +162,14 @@ function setDashboardDomain(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function getStatus(req, res, next) {
|
||||
cloudron.getStatus(function (error, status) {
|
||||
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(200, status));
|
||||
next(new HttpSuccess(202, { taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+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'));
|
||||
|
||||
|
||||
+2
-1
@@ -13,12 +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'),
|
||||
provision: require('./provision.js'),
|
||||
services: require('./services.js'),
|
||||
settings: require('./settings.js'),
|
||||
support: require('./support.js'),
|
||||
sysadmin: require('./sysadmin.js'),
|
||||
ssh: require('./ssh.js'),
|
||||
tasks: require('./tasks.js'),
|
||||
users: require('./users.js')
|
||||
};
|
||||
|
||||
@@ -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, {}));
|
||||
});
|
||||
}
|
||||
+10
-1
@@ -5,7 +5,8 @@ exports = module.exports = {
|
||||
setupTokenAuth: setupTokenAuth,
|
||||
setup: setup,
|
||||
activate: activate,
|
||||
restore: restore
|
||||
restore: restore,
|
||||
getStatus: getStatus
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
@@ -156,3 +157,11 @@ function restore(req, res, next) {
|
||||
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));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ function configure(req, res, next) {
|
||||
function getLogs(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.service, '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'));
|
||||
|
||||
debug(`Getting logs of service ${req.params.service}`);
|
||||
@@ -64,7 +64,7 @@ function getLogs(req, res, next) {
|
||||
var options = {
|
||||
lines: lines,
|
||||
follow: false,
|
||||
format: req.query.format
|
||||
format: req.query.format || 'json'
|
||||
};
|
||||
|
||||
addons.getServiceLogs(req.params.service, options, function (error, logStream) {
|
||||
@@ -87,7 +87,7 @@ function getLogStream(req, res, next) {
|
||||
|
||||
debug(`Getting logstream of service ${req.params.service}`);
|
||||
|
||||
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'; }
|
||||
@@ -96,7 +96,8 @@ function getLogStream(req, res, next) {
|
||||
|
||||
var options = {
|
||||
lines: lines,
|
||||
follow: true
|
||||
follow: true,
|
||||
format: 'json'
|
||||
};
|
||||
|
||||
addons.getServiceLogs(req.params.service, options, function (error, logStream) {
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
getAuthorizedKeys: getAuthorizedKeys,
|
||||
getAuthorizedKey: getAuthorizedKey,
|
||||
addAuthorizedKey: addAuthorizedKey,
|
||||
delAuthorizedKey: delAuthorizedKey
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
ssh = require('../ssh.js'),
|
||||
SshError = ssh.SshError;
|
||||
|
||||
function getAuthorizedKeys(req, res, next) {
|
||||
ssh.getAuthorizedKeys(function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
next(new HttpSuccess(200, { keys: result }));
|
||||
});
|
||||
}
|
||||
|
||||
function getAuthorizedKey(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.identifier, 'string');
|
||||
|
||||
ssh.getAuthorizedKey(req.params.identifier, function (error, result) {
|
||||
if (error && error.reason === SshError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
next(new HttpSuccess(200, { identifier: result.identifier, key: result.key }));
|
||||
});
|
||||
}
|
||||
|
||||
function addAuthorizedKey(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.key !== 'string' || !req.body.key) return next(new HttpError(400, 'key must be a non empty'));
|
||||
|
||||
ssh.addAuthorizedKey(req.body.key, function (error) {
|
||||
if (error && error.reason === SshError.INVALID_KEY) return next(new HttpError(400, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(201, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function delAuthorizedKey(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.identifier, 'string');
|
||||
|
||||
ssh.delAuthorizedKey(req.params.identifier, function (error) {
|
||||
if (error && error.reason === SshError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
next(new HttpSuccess(202, {}));
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
feedback: feedback,
|
||||
|
||||
getRemoteSupport: getRemoteSupport,
|
||||
enableRemoteSupport: enableRemoteSupport
|
||||
};
|
||||
|
||||
var appstore = require('../appstore.js'),
|
||||
AppstoreError = require('../appstore.js').AppstoreError,
|
||||
assert = require('assert'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
support = require('../support.js'),
|
||||
_ = require('underscore');
|
||||
|
||||
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 enableRemoteSupport(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.enable !== 'boolean') return next(new HttpError(400, 'enabled is required'));
|
||||
|
||||
support.enableRemoteSupport(req.body.enable, function (error) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function getRemoteSupport(req, res, next) {
|
||||
support.getRemoteSupport(function (error, status) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, status));
|
||||
});
|
||||
}
|
||||
+5
-4
@@ -59,13 +59,13 @@ function list(req, res, next) {
|
||||
function getLogs(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.taskId, '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'
|
||||
};
|
||||
|
||||
tasks.getLogs(req.params.taskId, options, function (error, logStream) {
|
||||
@@ -86,7 +86,7 @@ function getLogs(req, res, next) {
|
||||
function getLogStream(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.taskId, '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'; }
|
||||
@@ -95,7 +95,8 @@ function getLogStream(req, res, next) {
|
||||
|
||||
var options = {
|
||||
lines: lines,
|
||||
follow: true
|
||||
follow: true,
|
||||
format: 'json'
|
||||
};
|
||||
|
||||
tasks.getLogs(req.params.taskId, options, function (error, logStream) {
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
/* global before:false */
|
||||
/* global after:false */
|
||||
|
||||
var accesscontrol = require('../../accesscontrol.js'),
|
||||
async = require('async'),
|
||||
let async = require('async'),
|
||||
config = require('../../config.js'),
|
||||
database = require('../../database.js'),
|
||||
expect = require('expect.js'),
|
||||
@@ -209,141 +208,6 @@ describe('Cloudron', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('feedback', function () {
|
||||
before(function (done) {
|
||||
async.series([
|
||||
setup,
|
||||
|
||||
function (callback) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
|
||||
// stash token for further use
|
||||
token = result.body.token;
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
], done);
|
||||
});
|
||||
|
||||
after(cleanup);
|
||||
|
||||
it('fails without token', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/feedback')
|
||||
.send({ type: 'ticket', subject: 'some subject', description: 'some description' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails without type', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/feedback')
|
||||
.send({ subject: 'some subject', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with empty type', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/feedback')
|
||||
.send({ type: '', subject: 'some subject', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with unknown type', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/feedback')
|
||||
.send({ type: 'foobar', subject: 'some subject', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails without description', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/feedback')
|
||||
.send({ type: 'ticket', subject: 'some subject' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with empty subject', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/feedback')
|
||||
.send({ type: 'ticket', subject: '', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with empty description', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/feedback')
|
||||
.send({ type: 'ticket', subject: 'some subject', description: '' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails without subject', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/feedback')
|
||||
.send({ type: 'ticket', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with ticket type', function (done) {
|
||||
var scope2 = nock(config.apiServerOrigin())
|
||||
.filteringRequestBody(function (/* unusedBody */) { return ''; }) // strip out body
|
||||
.post('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/feedback?accessToken=ACCESS_TOKEN')
|
||||
.reply(201, { });
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/feedback')
|
||||
.send({ type: 'ticket', subject: 'some subject', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(201);
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with app type', function (done) {
|
||||
var scope2 = nock(config.apiServerOrigin())
|
||||
.filteringRequestBody(function (/* unusedBody */) { return ''; }) // strip out body
|
||||
.post('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/feedback?accessToken=ACCESS_TOKEN')
|
||||
.reply(201, { });
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/feedback')
|
||||
.send({ type: 'app_missing', subject: 'some subject', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(201);
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('logs', function () {
|
||||
before(function (done) {
|
||||
async.series([
|
||||
|
||||
@@ -10,6 +10,7 @@ var accesscontrol = require('../../accesscontrol.js'),
|
||||
async = require('async'),
|
||||
config = require('../../config.js'),
|
||||
database = require('../../database.js'),
|
||||
eventlogdb = require('../../eventlogdb.js'),
|
||||
expect = require('expect.js'),
|
||||
superagent = require('superagent'),
|
||||
server = require('../../server.js'),
|
||||
@@ -22,6 +23,17 @@ var token = null;
|
||||
|
||||
var USER_1_ID = null, token_1;
|
||||
|
||||
var EVENT_0 = {
|
||||
id: 'event_0',
|
||||
action: 'foobaraction',
|
||||
source: {
|
||||
ip: '127.0.0.1'
|
||||
},
|
||||
data: {
|
||||
something: 'is there'
|
||||
}
|
||||
};
|
||||
|
||||
function setup(done) {
|
||||
config._reset();
|
||||
config.setFqdn('example-eventlog-test.com');
|
||||
@@ -64,6 +76,10 @@ function setup(done) {
|
||||
|
||||
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
|
||||
tokendb.add(token_1, USER_1_ID, 'test-client-id', Date.now() + 100000, accesscontrol.SCOPE_PROFILE, '', callback);
|
||||
},
|
||||
|
||||
function (callback) {
|
||||
eventlogdb.add(EVENT_0.id, EVENT_0.action, EVENT_0.source, EVENT_0.data, callback);
|
||||
}
|
||||
|
||||
], done);
|
||||
@@ -82,6 +98,53 @@ describe('Eventlog API', function () {
|
||||
after(cleanup);
|
||||
|
||||
describe('get', function () {
|
||||
it('fails due to wrong token', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/cloudron/eventlog/' + EVENT_0.id)
|
||||
.query({ access_token: token.toUpperCase() })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails for non-admin', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/cloudron/eventlog/' + EVENT_0.id)
|
||||
.query({ access_token: token_1 })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(403);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails if not exists', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/cloudron/eventlog/doesnotexist')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(404);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds for admin', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/cloudron/eventlog/' + EVENT_0.id)
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.event).to.be.an('object');
|
||||
expect(result.body.event.creationTime).to.be.a('string');
|
||||
|
||||
delete result.body.event.creationTime;
|
||||
expect(result.body.event).to.eql(EVENT_0);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('list', function () {
|
||||
it('fails due to wrong token', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/cloudron/eventlog')
|
||||
.query({ access_token: token.toUpperCase() })
|
||||
|
||||
@@ -48,7 +48,7 @@ function setup(done) {
|
||||
|
||||
function dnsSetup(callback) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/setup')
|
||||
.send({ dnsConfig: { provider: ADMIN_DOMAIN.provider, domain: ADMIN_DOMAIN.domain, config: ADMIN_DOMAIN.config } })
|
||||
.send({ dnsConfig: { provider: ADMIN_DOMAIN.provider, domain: ADMIN_DOMAIN.domain, config: ADMIN_DOMAIN.config, tlsConfig: { provider: 'fallback' } } })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(200);
|
||||
@@ -57,6 +57,21 @@ function setup(done) {
|
||||
});
|
||||
},
|
||||
|
||||
function waitForSetup(done) {
|
||||
async.retry({ times: 5, interval: 4000 }, function (retryCallback) {
|
||||
superagent.get(SERVER_URL + '/api/v1/cloudron/status')
|
||||
.end(function (error, result) {
|
||||
if (!result || result.statusCode !== 200) return retryCallback(new Error('Bad result'));
|
||||
|
||||
console.dir(result.body);
|
||||
|
||||
if (!result.body.setup.active && result.body.setup.errorMessage === '' && result.body.adminFqdn) return retryCallback();
|
||||
|
||||
retryCallback(new Error('Not done yet: ' + JSON.stringify(result.body)));
|
||||
});
|
||||
}, done);
|
||||
},
|
||||
|
||||
function createAdmin(callback) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
|
||||
@@ -30,7 +30,7 @@ var accesscontrol = require('../../accesscontrol.js'),
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
let AUDIT_SOURCE = { ip: '1.2.3.4' };
|
||||
let AUDIT_SOURCE = { ip: '1.2.3.4', userId: 'someuserid' };
|
||||
|
||||
describe('OAuth2', function () {
|
||||
|
||||
@@ -221,7 +221,7 @@ describe('OAuth2', function () {
|
||||
clientdb.add.bind(null, CLIENT_7.id, CLIENT_7.appId, CLIENT_7.type, CLIENT_7.clientSecret, CLIENT_7.redirectURI, CLIENT_7.scope),
|
||||
clientdb.add.bind(null, CLIENT_9.id, CLIENT_9.appId, CLIENT_9.type, CLIENT_9.clientSecret, CLIENT_9.redirectURI, CLIENT_9.scope),
|
||||
function (callback) {
|
||||
users.create(USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, { }, null /* source */, function (error, userObject) {
|
||||
users.create(USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, { }, AUDIT_SOURCE, function (error, userObject) {
|
||||
expect(error).to.not.be.ok();
|
||||
|
||||
// update the global objects to reflect the new user id
|
||||
|
||||
@@ -34,6 +34,19 @@ function cleanup(done) {
|
||||
], done);
|
||||
}
|
||||
|
||||
function waitForSetup(done) {
|
||||
async.retry({ times: 5, interval: 4000 }, function (retryCallback) {
|
||||
superagent.get(SERVER_URL + '/api/v1/cloudron/status')
|
||||
.end(function (error, result) {
|
||||
if (!result || result.statusCode !== 200) return retryCallback(new Error('Bad result'));
|
||||
|
||||
if (!result.body.setup.active && result.body.setup.errorMessage === '' && result.body.adminFqdn) return retryCallback();
|
||||
|
||||
retryCallback(new Error('Not done yet: ' + JSON.stringify(result.body)));
|
||||
});
|
||||
}, done);
|
||||
}
|
||||
|
||||
describe('REST API', function () {
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
@@ -128,23 +141,23 @@ describe('REST API', function () {
|
||||
|
||||
it('dns setup succeeds', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/setup')
|
||||
.send({ dnsConfig: { provider: 'noop', domain: DOMAIN, adminFqdn: 'my.' + DOMAIN, config: {} } })
|
||||
.send({ dnsConfig: { provider: 'noop', domain: DOMAIN, adminFqdn: 'my.' + DOMAIN, config: {}, tlsConfig: { provider: 'fallback' } } })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(200);
|
||||
|
||||
done();
|
||||
waitForSetup(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('dns setup twice succeeds', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/setup')
|
||||
.send({ dnsConfig: { provider: 'noop', domain: DOMAIN, DOMAIN, config: {} } })
|
||||
.send({ dnsConfig: { provider: 'noop', domain: DOMAIN, DOMAIN, config: {} }, tlsConfig: { provider: 'fallback' } })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(200);
|
||||
|
||||
done();
|
||||
waitForSetup(done);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
/* global it:false */
|
||||
/* global describe:false */
|
||||
/* global before:false */
|
||||
/* global after:false */
|
||||
|
||||
'use strict';
|
||||
|
||||
var ssh = require('../../ssh.js'),
|
||||
async = require('async'),
|
||||
config = require('../../config.js'),
|
||||
database = require('../../database.js'),
|
||||
expect = require('expect.js'),
|
||||
superagent = require('superagent'),
|
||||
server = require('../../server.js');
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
|
||||
|
||||
var INVALID_KEY_TYPE = 'ssh-foobar AAAAB3NzaC1yc2EAAAADAQABAAABAQCibC8G04mZy3o3AVMxjUMQoEQj0HSsl6AMVZDQK9A0e8qVRWft4HaRZdw0dW3iFDsEdny7s1zSAc5Kp5y38kJdSyEHGKxvcR8TaghUa8jpmu0sEVOTn+X4UtoonkNuJ0Jnl2tjPYsq5BtJmAeYUa1bKH5CjomCYi5OSfXRtnuZV6SiYX0A1OnZPXFWa/iFwwOUJQvDLGbFkgtJxqhxpc7yvzwFK5B9MNs7LJxA8+kRibJ9LTN1OKWNxb0oSk/PE6PFo9M2Q/SL9uj2IRXRipGj2XcOtZlqcAK5i+aq3UjjAGekztK2srQPcBkWbnI3Oim2N8l2purCfe0AoCCQHK7N nebulon@nebulon';
|
||||
var INVALID_KEY_VALUE = 'ssh-rsa foobar nebulon@nebulon';
|
||||
var INVALID_KEY_IDENTIFIER = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCibC8G04mZy3o3AVMxjUMQoEQj0HSsl6AMVZDQK9A0e8qVRWft4HaRZdw0dW3iFDsEdny7s1zSAc5Kp5y38kJdSyEHGKxvcR8TaghUa8jpmu0sEVOTn+X4UtoonkNuJ0Jnl2tjPYsq5BtJmAeYUa1bKH5CjomCYi5OSfXRtnuZV6SiYX0A1OnZPXFWa/iFwwOUJQvDLGbFkgtJxqhxpc7yvzwFK5B9MNs7LJxA8+kRibJ9LTN1OKWNxb0oSk/PE6PFo9M2Q/SL9uj2IRXRipGj2XcOtZlqcAK5i+aq3UjjAGekztK2srQPcBkWbnI3Oim2N8l2purCfe0AoCCQHK7N';
|
||||
var VALID_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCibC8G04mZy3o3AVMxjUMQoEQj0HSsl6AMVZDQK9A0e8qVRWft4HaRZdw0dW3iFDsEdny7s1zSAc5Kp5y38kJdSyEHGKxvcR8TaghUa8jpmu0sEVOTn+X4UtoonkNuJ0Jnl2tjPYsq5BtJmAeYUa1bKH5CjomCYi5OSfXRtnuZV6SiYX0A1OnZPXFWa/iFwwOUJQvDLGbFkgtJxqhxpc7yvzwFK5B9MNs7LJxA8+kRibJ9LTN1OKWNxb0oSk/PE6PFo9M2Q/SL9uj2IRXRipGj2XcOtZlqcAK5i+aq3UjjAGekztK2srQPcBkWbnI3Oim2N8l2purCfe0AoCCQHK7N nebulon@nebulon';
|
||||
var VALID_KEY_1 = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCibC8G04mZy3o3AVMxjUMQoEQj0HSsl6AMVZDQK9A0e8qVRWft4HaRZdw0dW3iFDsEdny7s1zSAc5Kp5y38kJdSyEHGKxvcR8TaghUa8jpmu0sEVOTn+X4UtoonkNuJ0Jnl2tjPYsq5BtJmAeYUa1bKH5CjomCYi5OSfXRtnuZV6SiYX0A1OnZPXFWa/iFwwOUJQvDLGbFkgtJxqhxpc7yvzwFK5B9MNs7LJxA8+kRibJ9LTN1OKWNxb0oSk/PE6PFo9M2Q/SL9uj2IRXRipGj2XcOtZlqcAK5i+aq3UjjAGekztK2srQPcBkWbnI3Oim2N8l2purCfe0AoCCQHK7N muchmore';
|
||||
|
||||
var token = null;
|
||||
|
||||
function setup(done) {
|
||||
config._reset();
|
||||
config.setFqdn('example-ssh-test.com');
|
||||
|
||||
async.series([
|
||||
server.start.bind(server),
|
||||
|
||||
ssh._clear,
|
||||
database._clear,
|
||||
|
||||
function createAdmin(callback) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(201);
|
||||
|
||||
// stash token for further use
|
||||
token = result.body.token;
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
], done);
|
||||
}
|
||||
|
||||
function cleanup(done) {
|
||||
database._clear(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
|
||||
server.stop(done);
|
||||
});
|
||||
}
|
||||
|
||||
describe('SSH API', function () {
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
|
||||
describe('add authorized_keys', function () {
|
||||
it('fails due to missing key', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to empty key', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
|
||||
.query({ access_token: token })
|
||||
.send({ key: '' })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to invalid key', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
|
||||
.query({ access_token: token })
|
||||
.send({ key: 'foobar' })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to invalid key type', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
|
||||
.query({ access_token: token })
|
||||
.send({ key: INVALID_KEY_TYPE })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to invalid key value', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
|
||||
.query({ access_token: token })
|
||||
.send({ key: INVALID_KEY_VALUE })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to invalid key identifier', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
|
||||
.query({ access_token: token })
|
||||
.send({ key: INVALID_KEY_IDENTIFIER })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
|
||||
.query({ access_token: token })
|
||||
.send({ key: VALID_KEY })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(201);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('get authorized_keys', function () {
|
||||
it('fails for non existing key', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys/foobar')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(404);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys/' + VALID_KEY.split(' ')[2])
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body).to.be.an('object');
|
||||
expect(res.body.identifier).to.be.a('string');
|
||||
expect(res.body.identifier).to.equal(VALID_KEY.split(' ')[2]);
|
||||
expect(res.body.key).to.equal(VALID_KEY);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('list authorized_keys', function () {
|
||||
it('succeeds', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body).to.be.an('object');
|
||||
expect(res.body.keys).to.be.an('array');
|
||||
expect(res.body.keys.length).to.equal(1);
|
||||
expect(res.body.keys[0]).to.be.an('object');
|
||||
expect(res.body.keys[0].identifier).to.be.a('string');
|
||||
expect(res.body.keys[0].identifier).to.equal(VALID_KEY.split(' ')[2]);
|
||||
expect(res.body.keys[0].key).to.equal(VALID_KEY);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with two keys', function (done) {
|
||||
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
|
||||
.query({ access_token: token })
|
||||
.send({ key: VALID_KEY_1 })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(201);
|
||||
|
||||
superagent.get(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body).to.be.an('object');
|
||||
expect(res.body.keys).to.be.an('array');
|
||||
expect(res.body.keys.length).to.equal(2);
|
||||
expect(res.body.keys[0]).to.be.an('object');
|
||||
expect(res.body.keys[0].identifier).to.be.a('string');
|
||||
expect(res.body.keys[0].identifier).to.equal(VALID_KEY_1.split(' ')[2]);
|
||||
expect(res.body.keys[0].key).to.equal(VALID_KEY_1);
|
||||
expect(res.body.keys[1]).to.be.an('object');
|
||||
expect(res.body.keys[1].identifier).to.be.a('string');
|
||||
expect(res.body.keys[1].identifier).to.equal(VALID_KEY.split(' ')[2]);
|
||||
expect(res.body.keys[1].key).to.equal(VALID_KEY);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete authorized_keys', function () {
|
||||
it('fails for non existing key', function (done) {
|
||||
superagent.del(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys/foobar')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(404);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
superagent.del(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys/' + VALID_KEY.split(' ')[2])
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(202);
|
||||
|
||||
superagent.get(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys/' + VALID_KEY.split(' ')[2])
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(404);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,263 @@
|
||||
/* global it:false */
|
||||
/* global describe:false */
|
||||
/* global before:false */
|
||||
/* global after:false */
|
||||
|
||||
'use strict';
|
||||
|
||||
var async = require('async'),
|
||||
config = require('../../config.js'),
|
||||
database = require('../../database.js'),
|
||||
expect = require('expect.js'),
|
||||
nock = require('nock'),
|
||||
path = require('path'),
|
||||
safe = require('safetydance'),
|
||||
settings = require('../../settings.js'),
|
||||
settingsdb = require('../../settingsdb.js'),
|
||||
superagent = require('superagent'),
|
||||
server = require('../../server.js');
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
|
||||
var AUTHORIZED_KEYS_FILE = path.join(config.baseDir(), 'authorized_keys');
|
||||
var token = null;
|
||||
|
||||
function setup(done) {
|
||||
nock.cleanAll();
|
||||
config._reset();
|
||||
config.setFqdn('example-ssh-test.com');
|
||||
safe.fs.unlinkSync(AUTHORIZED_KEYS_FILE);
|
||||
|
||||
async.series([
|
||||
server.start.bind(server),
|
||||
|
||||
database._clear,
|
||||
|
||||
function createAdmin(callback) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(201);
|
||||
|
||||
// stash token for further use
|
||||
token = result.body.token;
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
|
||||
settingsdb.set.bind(null, settings.APPSTORE_CONFIG_KEY, JSON.stringify({ userId: 'USER_ID', cloudronId: 'CLOUDRON_ID', token: 'ACCESS_TOKEN' }))
|
||||
], done);
|
||||
}
|
||||
|
||||
function cleanup(done) {
|
||||
database._clear(function (error) {
|
||||
expect(error).to.not.be.ok();
|
||||
|
||||
config._reset();
|
||||
|
||||
server.stop(done);
|
||||
});
|
||||
}
|
||||
|
||||
describe('Support API', function () {
|
||||
describe('remote support', function () {
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
|
||||
it('get remote support', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/support/remote_support')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.enabled).to.be(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('enable remote support', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/support/remote_support')
|
||||
.query({ access_token: token })
|
||||
.send({ enable: true })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(202);
|
||||
|
||||
let data = safe.fs.readFileSync(AUTHORIZED_KEYS_FILE, 'utf8');
|
||||
let count = (data.match(/support@cloudron.io/g) || []).length;
|
||||
expect(count).to.be(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('returns true when remote support enabled', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/support/remote_support')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.enabled).to.be(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('enable remote support (again)', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/support/remote_support')
|
||||
.query({ access_token: token })
|
||||
.send({ enable: true })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(202);
|
||||
|
||||
let data = safe.fs.readFileSync(AUTHORIZED_KEYS_FILE, 'utf8');
|
||||
let count = (data.match(/support@cloudron.io/g) || []).length;
|
||||
expect(count).to.be(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('disable remote support', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/support/remote_support')
|
||||
.query({ access_token: token })
|
||||
.send({ enable: false })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(202);
|
||||
|
||||
let data = safe.fs.readFileSync(AUTHORIZED_KEYS_FILE, 'utf8');
|
||||
let count = (data.match(/support@cloudron.io/g) || []).length;
|
||||
expect(count).to.be(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('disable remote support (again)', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/support/remote_support')
|
||||
.query({ access_token: token })
|
||||
.send({ enable: false })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(202);
|
||||
|
||||
let data = safe.fs.readFileSync(AUTHORIZED_KEYS_FILE, 'utf8');
|
||||
let count = (data.match(/support@cloudron.io/g) || []).length;
|
||||
expect(count).to.be(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('feedback', function () {
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
|
||||
it('fails without token', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/support/feedback')
|
||||
.send({ type: 'ticket', subject: 'some subject', description: 'some description' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails without type', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/support/feedback')
|
||||
.send({ subject: 'some subject', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with empty type', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/support/feedback')
|
||||
.send({ type: '', subject: 'some subject', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with unknown type', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/support/feedback')
|
||||
.send({ type: 'foobar', subject: 'some subject', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails without description', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/support/feedback')
|
||||
.send({ type: 'ticket', subject: 'some subject' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with empty subject', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/support/feedback')
|
||||
.send({ type: 'ticket', subject: '', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with empty description', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/support/feedback')
|
||||
.send({ type: 'ticket', subject: 'some subject', description: '' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails without subject', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/support/feedback')
|
||||
.send({ type: 'ticket', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with ticket type', function (done) {
|
||||
var scope2 = nock(config.apiServerOrigin())
|
||||
.filteringRequestBody(function (/* unusedBody */) { return ''; }) // strip out body
|
||||
.post('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/feedback?accessToken=ACCESS_TOKEN')
|
||||
.reply(201, { });
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/support/feedback')
|
||||
.send({ type: 'ticket', subject: 'some subject', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(201);
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with app type', function (done) {
|
||||
var scope2 = nock(config.apiServerOrigin())
|
||||
.filteringRequestBody(function (/* unusedBody */) { return ''; }) // strip out body
|
||||
.post('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/feedback?accessToken=ACCESS_TOKEN')
|
||||
.reply(201, { });
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/support/feedback')
|
||||
.send({ type: 'app_missing', subject: 'some subject', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(201);
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -135,4 +135,27 @@ describe('Tasks API', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can list tasks', function (done) {
|
||||
let taskId = null;
|
||||
let task = tasks.startTask(tasks._TASK_IDENTITY, [ 'ping' ]);
|
||||
task.on('error', done);
|
||||
task.on('start', (tid) => { taskId = tid; });
|
||||
|
||||
task.on('finish', function () {
|
||||
superagent.get(SERVER_URL + '/api/v1/tasks?type=' + tasks._TASK_IDENTITY)
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.tasks.length >= 1).to.be(true);
|
||||
expect(res.body.tasks[0].id).to.be(taskId);
|
||||
expect(res.body.tasks[0].percent).to.be(100);
|
||||
expect(res.body.tasks[0].args).to.be(undefined);
|
||||
expect(res.body.tasks[0].active).to.be(false); // finished
|
||||
expect(res.body.tasks[0].result).to.be('ping');
|
||||
expect(res.body.tasks[0].errorMessage).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -455,8 +455,7 @@ describe('Users API', function () {
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(201);
|
||||
|
||||
// two mails for user creation
|
||||
checkMails(2, done);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+9
-1
@@ -85,7 +85,15 @@ function update(req, res, next) {
|
||||
}
|
||||
|
||||
function list(req, res, next) {
|
||||
users.list(function (error, results) {
|
||||
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'));
|
||||
|
||||
if (req.query.search && typeof req.query.search !== 'string') return next(new HttpError(400, 'search must be a string'));
|
||||
|
||||
users.getAllPaged(req.query.search || null, page, perPage, function (error, results) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
results = results.map(users.removeRestrictedFields);
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
echo "This script should be run as root." > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
echo "No arguments supplied"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$1" == "--check" ]]; then
|
||||
echo "OK"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# verify argument count
|
||||
if [[ $# -lt 3 ]]; then
|
||||
echo "Usage: authorized_keys.sh <user> <source> <destination>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -f "$2" ]]; then
|
||||
# on some vanilla ubuntu installs, the .ssh directory does not exist
|
||||
mkdir -p "$(dirname $3)"
|
||||
|
||||
cp "$2" "$3"
|
||||
chown "$1":"$1" "$3"
|
||||
fi
|
||||
@@ -21,11 +21,11 @@ function initialize(callback) {
|
||||
}
|
||||
|
||||
// Main process starts here
|
||||
var backupId = process.argv[2];
|
||||
var format = process.argv[3];
|
||||
var dataDir = process.argv[4];
|
||||
const backupId = process.argv[2];
|
||||
const format = process.argv[3];
|
||||
const dataLayoutString = process.argv[4];
|
||||
|
||||
debug(`Backing up ${dataDir} to ${backupId}`);
|
||||
debug(`Backing up ${dataLayoutString} to ${backupId}`);
|
||||
|
||||
process.on('SIGTERM', function () {
|
||||
process.exit(0);
|
||||
@@ -40,7 +40,7 @@ process.on('disconnect', function () {
|
||||
initialize(function (error) {
|
||||
if (error) throw error;
|
||||
|
||||
backups.upload(backupId, format, dataDir, (progress) => process.send(progress), function resultHandler(error) {
|
||||
backups.upload(backupId, format, dataLayoutString, (progress) => process.send(progress), function resultHandler(error) {
|
||||
if (error) debug('upload completed with error', error);
|
||||
|
||||
debug('upload completed');
|
||||
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
echo "This script should be run as root." > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
echo "No arguments supplied"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$1" == "--check" ]]; then
|
||||
echo "OK"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cmd="$1"
|
||||
volume_dir="$2"
|
||||
|
||||
if [[ "${BOX_ENV}" == "test" ]]; then
|
||||
# be careful not to nuke some random directory when testing
|
||||
[[ "${volume_dir}" != *"./cloudron_test/"* ]] && exit 1
|
||||
fi
|
||||
|
||||
if [[ -d "${volume_dir}" ]]; then
|
||||
# this removes hidden files
|
||||
find "${volume_dir}" -maxdepth 1 -mindepth 1 -exec rm -rf '{}' \;
|
||||
fi
|
||||
|
||||
if [[ "${cmd}" == "clear" ]]; then
|
||||
mkdir -p "${volume_dir}"
|
||||
# set it up so that we can restore here as normal user
|
||||
chown $SUDO_USER:$SUDO_USER "${volume_dir}"
|
||||
else
|
||||
# this make not succeed if volume is a mount point
|
||||
rmdir "${volume_dir}" || true
|
||||
fi
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
echo "This script should be run as root." > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
echo "No arguments supplied"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$1" == "--check" ]]; then
|
||||
echo "OK"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
volume_dir="$1"
|
||||
|
||||
mkdir -p "${volume_dir}"
|
||||
|
||||
Executable
+35
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
echo "This script should be run as root." > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
echo "No arguments supplied"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$1" == "--check" ]]; then
|
||||
echo "OK"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
source_dir="$1"
|
||||
target_dir="$2"
|
||||
|
||||
if [[ "${BOX_ENV}" == "test" ]]; then
|
||||
# be careful not to nuke some random directory when testing
|
||||
[[ "${source_dir}" != *"./cloudron_test/"* ]] && exit 1
|
||||
[[ "${target_dir}" != *"./cloudron_test/"* ]] && exit 1
|
||||
fi
|
||||
|
||||
# copy and remove - this way if the copy fails, the original is intact
|
||||
# the find logic is so that move to a subdir works (and we also move hidden files)
|
||||
find "${source_dir}" -maxdepth 1 -mindepth 1 -not -wholename "${target_dir}" -exec cp -ar '{}' "${target_dir}" \;
|
||||
find "${source_dir}" -maxdepth 1 -mindepth 1 -not -wholename "${target_dir}" -exec rm -rf '{}' \;
|
||||
# this will fail if target is a subdir or if source is a mountpoint
|
||||
rmdir "${source_dir}" || true
|
||||
|
||||
Executable
+45
@@ -0,0 +1,45 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
echo "This script should be run as root." > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
echo "No arguments supplied"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$1" == "--check" ]]; then
|
||||
echo "OK"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
CLOUDRON_SUPPORT_PUBLIC_KEY='ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQVilclYAIu+ioDp/sgzzFz6YU0hPcRYY7ze/LiF/lC7uQqK062O54BFXTvQ3ehtFZCx3bNckjlT2e6gB8Qq07OM66De4/S/g+HJW4TReY2ppSPMVNag0TNGxDzVH8pPHOysAm33LqT2b6L/wEXwC6zWFXhOhHjcMqXvi8Ejaj20H1HVVcf/j8qs5Thkp9nAaFTgQTPu8pgwD8wDeYX1hc9d0PYGesTADvo6HF4hLEoEnefLw7PaStEbzk2fD3j7/g5r5HcgQQXBe74xYZ/1gWOX2pFNuRYOBSEIrNfJEjFJsqk3NR1+ZoMGK7j+AZBR4k0xbrmncQLcQzl6MMDzkp support@cloudron.io'
|
||||
|
||||
cmd="$1"
|
||||
keys_file="$2"
|
||||
user="${3:-1000}"
|
||||
|
||||
if [[ "$1" == "is-enabled" ]]; then
|
||||
if grep -q "${CLOUDRON_SUPPORT_PUBLIC_KEY}" "${keys_file}"; then
|
||||
echo "true"
|
||||
else
|
||||
echo "false"
|
||||
fi
|
||||
elif [[ "$1" == "enable" ]]; then
|
||||
mkdir -p $(dirname "${keys_file}") # .ssh does not exist sometimes
|
||||
touch "${keys_file}" # required for concat to work
|
||||
if ! grep -q "${CLOUDRON_SUPPORT_PUBLIC_KEY}" "${keys_file}"; then
|
||||
echo -e "\n${CLOUDRON_SUPPORT_PUBLIC_KEY}" >> "${keys_file}"
|
||||
chmod 600 "${keys_file}"
|
||||
chown "${user}" "${keys_file}"
|
||||
fi
|
||||
elif [[ "$1" == "disable" ]]; then
|
||||
if [[ -f "${keys_file}" ]]; then
|
||||
sed -e "/ support@cloudron.io$/d" -i "${keys_file}"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
echo "This script should be run as root." > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
echo "No arguments supplied"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$1" == "--check" ]]; then
|
||||
echo "OK"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# this script is called from redis addon as well!
|
||||
|
||||
appid="$1"
|
||||
subdir="$2"
|
||||
|
||||
if [[ "${BOX_ENV}" == "cloudron" ]]; then
|
||||
readonly volume_dir="${HOME}/appsdata/${appid}/${subdir}"
|
||||
else
|
||||
readonly volume_dir="${HOME}/.cloudron_test/appsdata/${appid}/${subdir}"
|
||||
fi
|
||||
|
||||
rm -rf "${volume_dir}"
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
'use strict';
|
||||
|
||||
require('supererror')({ splatchError: true });
|
||||
|
||||
var tar = require('tar-fs');
|
||||
|
||||
var sourceDir = process.argv[2];
|
||||
|
||||
if (sourceDir === '--check') return console.log('OK');
|
||||
|
||||
process.stderr.write('Packing ' + sourceDir + '\n');
|
||||
|
||||
tar.pack('/', {
|
||||
dereference: false, // pack the symlink and not what it points to
|
||||
entries: [ sourceDir ],
|
||||
map: function(header) {
|
||||
header.name = header.name.replace(new RegExp('^' + sourceDir + '(/?)'), '.$1'); // make paths relative
|
||||
return header;
|
||||
},
|
||||
strict: false // do not error for unknown types (skip fifo, char/block devices)
|
||||
}).pipe(process.stdout);
|
||||
@@ -43,7 +43,7 @@ else
|
||||
echo "=> starting service (ubuntu 16.04) ${UPDATER_SERVICE}. see logs using journalctl -u ${UPDATER_SERVICE}"
|
||||
fi
|
||||
|
||||
if ! systemd-run --unit "${UPDATER_SERVICE}" $update_service_options ${installer_path}; then
|
||||
if ! systemd-run --property=OOMScoreAdjust=-1000 --unit "${UPDATER_SERVICE}" $update_service_options ${installer_path}; then
|
||||
echo "Failed to install cloudron. See log for details"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
+13
-7
@@ -96,6 +96,7 @@ function initializeExpressSync() {
|
||||
var appsManageScope = [ routes.accesscontrol.scope(accesscontrol.SCOPE_APPS_MANAGE), routes.apps.verifyOwnership ];
|
||||
var settingsScope = routes.accesscontrol.scope(accesscontrol.SCOPE_SETTINGS);
|
||||
var mailScope = routes.accesscontrol.scope(accesscontrol.SCOPE_MAIL);
|
||||
var notificationsScope = [ routes.accesscontrol.scope(accesscontrol.SCOPE_PROFILE), routes.notifications.verifyOwnership ];
|
||||
var clientsScope = routes.accesscontrol.scope(accesscontrol.SCOPE_CLIENTS);
|
||||
var domainsReadScope = routes.accesscontrol.scope(accesscontrol.SCOPE_DOMAINS_READ);
|
||||
var domainsManageScope = routes.accesscontrol.scope(accesscontrol.SCOPE_DOMAINS_MANAGE);
|
||||
@@ -111,7 +112,7 @@ function initializeExpressSync() {
|
||||
router.post('/api/v1/cloudron/setup', routes.provision.providerTokenAuth, routes.provision.setup); // only available until no-domain
|
||||
router.post('/api/v1/cloudron/restore', routes.provision.restore); // only available until activated
|
||||
router.post('/api/v1/cloudron/activate', routes.provision.setupTokenAuth, routes.provision.activate);
|
||||
router.get ('/api/v1/cloudron/status', routes.cloudron.getStatus);
|
||||
router.get ('/api/v1/cloudron/status', routes.provision.getStatus);
|
||||
|
||||
router.get ('/api/v1/cloudron/avatar', routes.settings.getCloudronAvatar); // this is a public alias for /api/v1/settings/cloudron_avatar
|
||||
|
||||
@@ -121,6 +122,7 @@ function initializeExpressSync() {
|
||||
// cloudron routes
|
||||
router.get ('/api/v1/cloudron/update', cloudronScope, routes.cloudron.getUpdateInfo);
|
||||
router.post('/api/v1/cloudron/update', cloudronScope, routes.cloudron.update);
|
||||
router.post('/api/v1/cloudron/prepare_dashboard_domain', cloudronScope, routes.cloudron.prepareDashboardDomain);
|
||||
router.post('/api/v1/cloudron/set_dashboard_domain', cloudronScope, routes.cloudron.setDashboardDomain);
|
||||
router.post('/api/v1/cloudron/renew_certs', cloudronScope, routes.cloudron.renewCerts);
|
||||
router.post('/api/v1/cloudron/check_for_updates', cloudronScope, routes.cloudron.checkForUpdates);
|
||||
@@ -130,11 +132,8 @@ function initializeExpressSync() {
|
||||
router.get ('/api/v1/cloudron/disks', cloudronScope, routes.cloudron.getDisks);
|
||||
router.get ('/api/v1/cloudron/logs/:unit', cloudronScope, routes.cloudron.getLogs);
|
||||
router.get ('/api/v1/cloudron/logstream/:unit', cloudronScope, routes.cloudron.getLogStream);
|
||||
router.get ('/api/v1/cloudron/ssh/authorized_keys', cloudronScope, isUnmanaged, routes.ssh.getAuthorizedKeys);
|
||||
router.put ('/api/v1/cloudron/ssh/authorized_keys', cloudronScope, isUnmanaged, routes.ssh.addAuthorizedKey);
|
||||
router.get ('/api/v1/cloudron/ssh/authorized_keys/:identifier', cloudronScope, isUnmanaged, routes.ssh.getAuthorizedKey);
|
||||
router.del ('/api/v1/cloudron/ssh/authorized_keys/:identifier', cloudronScope, isUnmanaged, routes.ssh.delAuthorizedKey);
|
||||
router.get ('/api/v1/cloudron/eventlog', cloudronScope, routes.eventlog.get);
|
||||
router.get ('/api/v1/cloudron/eventlog', cloudronScope, routes.eventlog.list);
|
||||
router.get ('/api/v1/cloudron/eventlog/:eventId', cloudronScope, routes.eventlog.get);
|
||||
|
||||
// tasks
|
||||
router.get ('/api/v1/tasks', settingsScope, routes.tasks.list);
|
||||
@@ -143,6 +142,11 @@ function initializeExpressSync() {
|
||||
router.get ('/api/v1/tasks/:taskId/logstream', cloudronScope, routes.tasks.getLogStream);
|
||||
router.post('/api/v1/tasks/:taskId/stop', settingsScope, routes.tasks.stopTask);
|
||||
|
||||
// notifications
|
||||
router.get ('/api/v1/notifications', notificationsScope, routes.notifications.list);
|
||||
router.get ('/api/v1/notifications/:notificationId', notificationsScope, routes.notifications.get);
|
||||
router.post('/api/v1/notifications/:notificationId', notificationsScope, routes.notifications.ack);
|
||||
|
||||
// backups
|
||||
router.get ('/api/v1/backups', settingsScope, routes.backups.list);
|
||||
router.post('/api/v1/backups', settingsScope, routes.backups.startBackup);
|
||||
@@ -280,7 +284,9 @@ function initializeExpressSync() {
|
||||
router.del ('/api/v1/mail/:domain/lists/:name', mailScope, routes.mail.removeList);
|
||||
|
||||
// feedback
|
||||
router.post('/api/v1/feedback', cloudronScope, isUnmanaged, routes.cloudron.feedback);
|
||||
router.post('/api/v1/support/feedback', cloudronScope, isUnmanaged, routes.support.feedback);
|
||||
router.get ('/api/v1/support/remote_support', cloudronScope, isUnmanaged, routes.support.getRemoteSupport);
|
||||
router.post('/api/v1/support/remote_support', cloudronScope, isUnmanaged, routes.support.enableRemoteSupport);
|
||||
|
||||
// domain routes
|
||||
router.post('/api/v1/domains', domainsManageScope, routes.domains.add);
|
||||
|
||||
-171
@@ -1,171 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
SshError: SshError,
|
||||
|
||||
getAuthorizedKeys: getAuthorizedKeys,
|
||||
getAuthorizedKey: getAuthorizedKey,
|
||||
addAuthorizedKey: addAuthorizedKey,
|
||||
delAuthorizedKey: delAuthorizedKey,
|
||||
|
||||
_clear: clear
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:ssh'),
|
||||
path = require('path'),
|
||||
safe = require('safetydance'),
|
||||
shell = require('./shell.js'),
|
||||
util = require('util');
|
||||
|
||||
var AUTHORIZED_KEYS_FILEPATH = config.TEST ? path.join(config.baseDir(), 'authorized_keys') : ((config.provider() === 'ec2' || config.provider() === 'lightsail' || config.provider() === 'ami') ? '/home/ubuntu/.ssh/authorized_keys' : '/root/.ssh/authorized_keys');
|
||||
var AUTHORIZED_KEYS_TMP_FILEPATH = '/tmp/.authorized_keys';
|
||||
var AUTHORIZED_KEYS_CMD = path.join(__dirname, 'scripts/authorized_keys.sh');
|
||||
var VALID_KEY_TYPES = ['ssh-rsa']; // TODO add all supported ones
|
||||
var VALID_MIN_KEY_LENGTH = 370; // TODO verify this length requirement
|
||||
|
||||
function SshError(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(SshError, Error);
|
||||
SshError.NOT_FOUND = 'Not found';
|
||||
SshError.INVALID_KEY = 'Invalid key';
|
||||
SshError.INTERNAL_ERROR = 'Internal Error';
|
||||
|
||||
function clear(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
safe.fs.unlinkSync(AUTHORIZED_KEYS_FILEPATH);
|
||||
callback();
|
||||
}
|
||||
|
||||
function saveKeys(keys, callback) {
|
||||
assert(Array.isArray(keys));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!safe.fs.writeFileSync(AUTHORIZED_KEYS_TMP_FILEPATH, keys.map(function (k) { return k.key; }).join('\n'))) {
|
||||
debug('Error writing to temporary file', safe.error);
|
||||
return callback(safe.error);
|
||||
}
|
||||
|
||||
if (!safe.fs.chmodSync(AUTHORIZED_KEYS_TMP_FILEPATH, '600')) { // 600 = rw-------
|
||||
debug('Failed to adjust permissions of %s %s', AUTHORIZED_KEYS_TMP_FILEPATH, safe.error);
|
||||
return callback(safe.error);
|
||||
}
|
||||
|
||||
var user = config.TEST ? process.env.USER : ((config.provider() === 'ec2' || config.provider() === 'lightsail' || config.provider() === 'ami') ? 'ubuntu' : 'root');
|
||||
shell.sudo('authorized_keys', [ AUTHORIZED_KEYS_CMD, user, AUTHORIZED_KEYS_TMP_FILEPATH, AUTHORIZED_KEYS_FILEPATH ], {}, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function getKeys(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
shell.sudo('authorized_keys', [ AUTHORIZED_KEYS_CMD, process.env.USER, AUTHORIZED_KEYS_FILEPATH, AUTHORIZED_KEYS_TMP_FILEPATH ], {}, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var content = safe.fs.readFileSync(AUTHORIZED_KEYS_TMP_FILEPATH, 'utf8');
|
||||
if (!content) return callback(null, []);
|
||||
|
||||
var keys = content.split('\n')
|
||||
.filter(function (k) { return !!k.trim(); })
|
||||
.map(function (k) { return { identifier: k.split(' ')[2], key: k }; })
|
||||
.filter(function (k) { return k.identifier && k.key; });
|
||||
|
||||
safe.fs.unlinkSync(AUTHORIZED_KEYS_TMP_FILEPATH);
|
||||
|
||||
return callback(null, keys);
|
||||
});
|
||||
}
|
||||
|
||||
function getAuthorizedKeys(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getKeys(function (error, keys) {
|
||||
if (error) return callback(new SshError(SshError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null, keys.sort(function (a, b) { return a.identifier.localeCompare(b.identifier); }));
|
||||
});
|
||||
}
|
||||
|
||||
function getAuthorizedKey(identifier, callback) {
|
||||
assert.strictEqual(typeof identifier, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getKeys(function (error, keys) {
|
||||
if (error) return callback(new SshError(SshError.INTERNAL_ERROR, error));
|
||||
|
||||
if (keys.length === 0) return callback(new SshError(SshError.NOT_FOUND));
|
||||
|
||||
var key = keys.find(function (k) { return k.identifier === identifier; });
|
||||
if (!key) return callback(new SshError(SshError.NOT_FOUND));
|
||||
|
||||
callback(null, key);
|
||||
});
|
||||
}
|
||||
|
||||
function addAuthorizedKey(key, callback) {
|
||||
assert.strictEqual(typeof key, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var tmp = key.split(' ');
|
||||
if (tmp.length !== 3) return callback(new SshError(SshError.INVALID_KEY));
|
||||
if (!VALID_KEY_TYPES.some(function (t) { return tmp[0] === t; })) return callback(new SshError(SshError.INVALID_KEY, 'Invalid key type'));
|
||||
if (tmp[1].length < VALID_MIN_KEY_LENGTH) return callback(new SshError(SshError.INVALID_KEY));
|
||||
|
||||
var identifier = tmp[2];
|
||||
|
||||
getKeys(function (error, keys) {
|
||||
if (error) return callback(new SshError(SshError.INTERNAL_ERROR, error));
|
||||
|
||||
var index = keys.findIndex(function (k) { return k.identifier === identifier; });
|
||||
if (index !== -1) keys[index] = { identifier: identifier, key: key };
|
||||
else keys.push({ identifier: identifier, key: key });
|
||||
|
||||
saveKeys(keys, function (error) {
|
||||
if (error) return callback(new SshError(SshError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function delAuthorizedKey(identifier, callback) {
|
||||
assert.strictEqual(typeof identifier, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getKeys(function (error, keys) {
|
||||
if (error) return callback(new SshError(SshError.INTERNAL_ERROR, error));
|
||||
|
||||
let index = keys.findIndex(function (k) { return k.identifier === identifier; });
|
||||
if (index === -1) return callback(new SshError(SshError.NOT_FOUND));
|
||||
|
||||
// now remove the key
|
||||
keys.splice(index, 1);
|
||||
|
||||
saveKeys(keys, function (error) {
|
||||
if (error) return callback(new SshError(SshError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
+2
-2
@@ -353,12 +353,12 @@ function copy(apiConfig, oldFilePath, newFilePath) {
|
||||
});
|
||||
}
|
||||
|
||||
var total = 0, concurrency = 4;
|
||||
var total = 0;
|
||||
const concurrency = apiConfig.copyConcurrency || (apiConfig.provider === 's3' ? 500 : 10);
|
||||
|
||||
listDir(apiConfig, oldFilePath, 1000, function listDirIterator(entries, done) {
|
||||
total += entries.length;
|
||||
|
||||
if (retryCount === 0) concurrency = Math.min(concurrency + 1, 10); else concurrency = Math.max(concurrency - 1, 5);
|
||||
events.emit('progress', `Copying ${total-entries.length}-${total}. ${retryCount} errors so far. concurrency set to ${concurrency}`);
|
||||
retryCount = 0;
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
getRemoteSupport: getRemoteSupport,
|
||||
enableRemoteSupport: enableRemoteSupport,
|
||||
|
||||
SupportError: SupportError
|
||||
};
|
||||
|
||||
let assert = require('assert'),
|
||||
config = require('./config.js'),
|
||||
shell = require('./shell.js'),
|
||||
once = require('once'),
|
||||
path = require('path'),
|
||||
util = require('util');
|
||||
|
||||
var AUTHORIZED_KEYS_FILEPATH = config.TEST ? path.join(config.baseDir(), 'authorized_keys') : ((config.provider() === 'ec2' || config.provider() === 'lightsail' || config.provider() === 'ami') ? '/home/ubuntu/.ssh/authorized_keys' : '/root/.ssh/authorized_keys'),
|
||||
AUTHORIZED_KEYS_USER = config.TEST ? process.getuid() : ((config.provider() === 'ec2' || config.provider() === 'lightsail' || config.provider() === 'ami') ? 'ubuntu' : 'root'),
|
||||
AUTHORIZED_KEYS_CMD = path.join(__dirname, 'scripts/remotesupport.sh');
|
||||
|
||||
function SupportError(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(SupportError, Error);
|
||||
SupportError.NOT_FOUND = 'Not found';
|
||||
SupportError.INVALID_KEY = 'Invalid key';
|
||||
SupportError.INTERNAL_ERROR = 'Internal Error';
|
||||
|
||||
function getRemoteSupport(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
callback = once(callback); // exit may or may not be called after an 'error'
|
||||
|
||||
let result = '';
|
||||
let cp = shell.sudo('support', [ AUTHORIZED_KEYS_CMD, 'is-enabled', AUTHORIZED_KEYS_FILEPATH ], {}, function (error) {
|
||||
if (error) callback(new SupportError(SupportError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, { enabled: result.trim() === 'true' });
|
||||
});
|
||||
cp.stdout.on('data', (data) => result = result + data.toString('utf8'));
|
||||
}
|
||||
|
||||
function enableRemoteSupport(enable, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
shell.sudo('support', [ AUTHORIZED_KEYS_CMD, enable ? 'enable' : 'disable', AUTHORIZED_KEYS_FILEPATH, AUTHORIZED_KEYS_USER ], {}, function (error) {
|
||||
if (error) callback(new SupportError(SupportError.INTERNAL_ERROR, error));
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
+43
-19
@@ -2,6 +2,7 @@
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
DataLayout = require('./datalayout.js'),
|
||||
debug = require('debug')('box:syncer'),
|
||||
fs = require('fs'),
|
||||
path = require('path'),
|
||||
@@ -21,13 +22,36 @@ function readCache(cacheFile) {
|
||||
return result;
|
||||
}
|
||||
|
||||
function readTree(dir) {
|
||||
assert.strictEqual(typeof dir, 'string');
|
||||
function readTree(dirPath) {
|
||||
assert.strictEqual(typeof dirPath, 'string');
|
||||
|
||||
var list = safe.fs.readdirSync(dir).sort();
|
||||
if (!list) return [ ];
|
||||
const names = safe.fs.readdirSync(dirPath).sort();
|
||||
if (!names) return [ ];
|
||||
|
||||
return list.map(function (e) { return { stat: safe.fs.lstatSync(path.join(dir, e)), name: e }; });
|
||||
return names.map((name) => {
|
||||
let absolutePath = path.join(dirPath, name);
|
||||
return {
|
||||
stat: safe.fs.lstatSync(absolutePath),
|
||||
absolutePath: absolutePath,
|
||||
name: name
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function readDataLayoutTree(dataLayout) {
|
||||
assert.strictEqual(typeof dataLayout, 'object');
|
||||
|
||||
let rootEntries = readTree(dataLayout.localRoot());
|
||||
|
||||
for (let l of dataLayout.directoryMap()) {
|
||||
rootEntries.push({
|
||||
stat: safe.fs.lstatSync(l.localDir),
|
||||
absolutePath: l.localDir,
|
||||
name: l.remoteDir,
|
||||
});
|
||||
}
|
||||
|
||||
return rootEntries.sort((e1, e2) => { return e1.name < e2.name ? -1 : (e1.name > e2.name ? +1 : 0); });
|
||||
}
|
||||
|
||||
function ISDIR(x) {
|
||||
@@ -38,16 +62,16 @@ function ISFILE(x) {
|
||||
return (x & fs.constants.S_IFREG) === fs.constants.S_IFREG;
|
||||
}
|
||||
|
||||
function sync(dir, taskProcessor, concurrency, callback) {
|
||||
assert.strictEqual(typeof dir, 'string');
|
||||
function sync(dataLayout, taskProcessor, concurrency, callback) {
|
||||
assert(dataLayout instanceof DataLayout, 'Expecting dataLayout to be a DataLayout');
|
||||
assert.strictEqual(typeof taskProcessor, 'function');
|
||||
assert.strictEqual(typeof concurrency, 'number');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var curCacheIndex = 0, addQueue = [ ], delQueue = [ ];
|
||||
|
||||
var cacheFile = path.join(paths.BACKUP_INFO_DIR, path.basename(dir) + '.sync.cache'),
|
||||
newCacheFile = path.join(paths.BACKUP_INFO_DIR, path.basename(dir) + '.sync.cache.new');
|
||||
var cacheFile = path.join(paths.BACKUP_INFO_DIR, dataLayout.getBasename() + '.sync.cache'),
|
||||
newCacheFile = path.join(paths.BACKUP_INFO_DIR, dataLayout.getBasename() + '.sync.cache.new');
|
||||
|
||||
var cache = [ ];
|
||||
|
||||
@@ -80,12 +104,10 @@ function sync(dir, taskProcessor, concurrency, callback) {
|
||||
}
|
||||
}
|
||||
|
||||
function traverse(relpath) {
|
||||
var entries = readTree(path.join(dir, relpath));
|
||||
|
||||
for (var i = 0; i < entries.length; i++) {
|
||||
var entryPath = path.join(relpath, entries[i].name);
|
||||
var entryStat = entries[i].stat;
|
||||
function traverse(entries, relpath) {
|
||||
for (const entry of entries) {
|
||||
let entryPath = path.join(relpath, entry.name);
|
||||
let entryStat = entry.stat;
|
||||
|
||||
if (!entryStat) continue; // some stat error. prented it doesn't exist
|
||||
if (!entryStat.isDirectory() && !entryStat.isFile()) continue; // ignore non-files and dirs
|
||||
@@ -102,13 +124,15 @@ function sync(dir, taskProcessor, concurrency, callback) {
|
||||
|
||||
if (cachePath === null || cachePath > entryPath) { // new files appeared
|
||||
if (entryStat.isDirectory()) {
|
||||
traverse(entryPath);
|
||||
traverse(readTree(entry.absolutePath), entryPath);
|
||||
} else {
|
||||
addQueue.push({ operation: 'add', path: entryPath, reason: 'new', position: addQueue.length });
|
||||
}
|
||||
} else if (ISDIR(cacheStat.mode) && entryStat.isDirectory()) { // dir names match
|
||||
++curCacheIndex;
|
||||
traverse(entryPath);
|
||||
// if we just pass path, have to keep looking into data layout!!! so pass an object
|
||||
// the object needs to have the path where we are traversing...
|
||||
traverse(readTree(entry.absolutePath), entryPath);
|
||||
} else if (ISFILE(cacheStat.mode) && entryStat.isFile()) { // file names match
|
||||
if (entryStat.mtime.getTime() !== cacheStat.mtime || entryStat.size != cacheStat.size || entryStat.inode !== cacheStat.inode) { // file changed
|
||||
addQueue.push({ operation: 'add', path: entryPath, reason: 'changed', position: addQueue.length });
|
||||
@@ -117,7 +141,7 @@ function sync(dir, taskProcessor, concurrency, callback) {
|
||||
} else if (entryStat.isDirectory()) { // was a file, now a directory
|
||||
delQueue.push({ operation: 'remove', path: cachePath, reason: 'wasfile' });
|
||||
++curCacheIndex;
|
||||
traverse(entryPath);
|
||||
traverse(readTree(entry.absolutePath), entryPath);
|
||||
} else { // was a dir, now a file
|
||||
delQueue.push({ operation: 'removedir', path: cachePath, reason: 'wasdir' });
|
||||
while (curCacheIndex !== cache.length && cache[curCacheIndex].path.startsWith(cachePath)) ++curCacheIndex;
|
||||
@@ -126,7 +150,7 @@ function sync(dir, taskProcessor, concurrency, callback) {
|
||||
}
|
||||
}
|
||||
|
||||
traverse('');
|
||||
traverse(readDataLayoutTree(dataLayout), '');
|
||||
advanceCache(''); // remove rest of the cache entries
|
||||
|
||||
safe.fs.closeSync(newCacheFd);
|
||||
|
||||
+9
-8
@@ -34,19 +34,20 @@ function SysInfoError(reason, errorOrMessage) {
|
||||
}
|
||||
util.inherits(SysInfoError, Error);
|
||||
SysInfoError.INTERNAL_ERROR = 'Internal Error';
|
||||
SysInfoError.EXTERNAL_ERROR = 'External Error';
|
||||
|
||||
function getApi(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
switch (config.provider()) {
|
||||
case '': return callback(null, caas); // current fallback for caas
|
||||
case 'caas': return callback(null, caas);
|
||||
case 'digitalocean': return callback(null, generic);
|
||||
case 'ec2': return callback(null, ec2);
|
||||
case 'lightsail': return callback(null, ec2);
|
||||
case 'ami': return callback(null, ec2);
|
||||
case 'scaleway': return callback(null, scaleway);
|
||||
default: return callback(null, generic);
|
||||
case '': return callback(null, caas); // current fallback for caas
|
||||
case 'caas': return callback(null, caas);
|
||||
case 'digitalocean': return callback(null, generic);
|
||||
case 'ec2': return callback(null, ec2);
|
||||
case 'lightsail': return callback(null, ec2);
|
||||
case 'ami': return callback(null, ec2);
|
||||
case 'scaleway': return callback(null, scaleway);
|
||||
default: return callback(null, generic);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -17,14 +17,14 @@ function getPublicIp(callback) {
|
||||
superagent.get('http://169.254.169.254/metadata/v1.json').timeout(30 * 1000).end(function (error, result) {
|
||||
if (error || result.statusCode !== 200) {
|
||||
console.error('Error getting metadata', error);
|
||||
return callback(new SysInfoError(SysInfoError.INTERNAL_ERROR, 'Could not detect public IP from metadata'));
|
||||
return callback(new SysInfoError(SysInfoError.EXTERNAL_ERROR, 'Could not detect public IP from metadata'));
|
||||
}
|
||||
|
||||
// Note that we do not use a floating IP for 3 reasons:
|
||||
// The PTR record is not set to floating IP, the outbound interface is not changeable to floating IP
|
||||
// and there are reports that port 25 on floating IP is blocked.
|
||||
var ip = safe.query(result.body, 'interfaces.public[0].ipv4.ip_address');
|
||||
if (!ip) return callback(new SysInfoError(SysInfoError.INTERNAL_ERROR, 'Could not detect public IP from interface'));
|
||||
if (!ip) return callback(new SysInfoError(SysInfoError.EXTERNAL_ERROR, 'Could not detect public IP from interface'));
|
||||
|
||||
callback(null, ip);
|
||||
});
|
||||
|
||||
@@ -19,11 +19,11 @@ function getPublicIp(callback) {
|
||||
superagent.get(config.apiServerOrigin() + '/api/v1/helper/public_ip').timeout(30 * 1000).end(function (error, result) {
|
||||
if (error || result.statusCode !== 200) {
|
||||
console.error('Error getting IP', error);
|
||||
return callback(new SysInfoError(SysInfoError.INTERNAL_ERROR, 'Unable to contact api server'));
|
||||
return callback(new SysInfoError(SysInfoError.EXTERNAL_ERROR, 'Unable to detect IP. API server unreachable'));
|
||||
}
|
||||
if (!result.body && !result.body.ip) {
|
||||
console.error('Unexpected answer. No "ip" found in response body.', result.body);
|
||||
return callback(new SysInfoError(SysInfoError.INTERNAL_ERROR, 'No IP found in body'));
|
||||
return callback(new SysInfoError(SysInfoError.EXTERNAL_ERROR, 'Unable to detect IP. No IP found in response'));
|
||||
}
|
||||
|
||||
callback(null, result.body.ip);
|
||||
|
||||
@@ -5,13 +5,15 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
superagent = require('superagent');
|
||||
superagent = require('superagent'),
|
||||
SysInfoError = require('../sysinfo.js').SysInfoError,
|
||||
util = require('util');
|
||||
|
||||
function getPublicIp(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
superagent.get('http://169.254.42.42/conf').timeout(30 * 1000).end(function (error, result) {
|
||||
if (error) return callback(new SysInfoError(SysInfoError.INTERNAL_ERROR, error.status ? 'Request failed: ' + error.status : 'Network failure'));
|
||||
if (error) return callback(new SysInfoError(SysInfoError.EXTERNAL_ERROR, error.status ? 'Request failed: ' + error.status : 'Network failure'));
|
||||
if (result.statusCode !== 200) return callback(new SysInfoError(SysInfoError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
|
||||
|
||||
var kv = result.text.split('\n').filter(function (line) { return line.startsWith('PUBLIC_IP_ADDRESS='); });
|
||||
|
||||
+18
-10
@@ -13,16 +13,19 @@ let assert = require('assert'),
|
||||
DatabaseError = require('./databaseerror'),
|
||||
safe = require('safetydance');
|
||||
|
||||
const TASKS_FIELDS = [ 'id', 'type', 'argsJson', 'percent', 'message', 'errorMessage', 'creationTime', 'result', 'ts' ];
|
||||
const TASKS_FIELDS = [ 'id', 'type', 'argsJson', 'percent', 'message', 'errorMessage', 'creationTime', 'resultJson', 'ts' ];
|
||||
|
||||
function postProcess(result) {
|
||||
assert.strictEqual(typeof result, 'object');
|
||||
function postProcess(task) {
|
||||
assert.strictEqual(typeof task, 'object');
|
||||
|
||||
assert(result.argsJson === null || typeof result.argsJson === 'string');
|
||||
result.args = safe.JSON.parse(result.argsJson) || [];
|
||||
delete result.argsJson;
|
||||
assert(task.argsJson === null || typeof task.argsJson === 'string');
|
||||
task.args = safe.JSON.parse(task.argsJson) || [];
|
||||
delete task.argsJson;
|
||||
|
||||
result.id = String(result.id);
|
||||
task.id = String(task.id);
|
||||
|
||||
task.result = JSON.parse(task.resultJson);
|
||||
delete task.resultJson;
|
||||
}
|
||||
|
||||
function add(task, callback) {
|
||||
@@ -47,8 +50,13 @@ function update(id, data, callback) {
|
||||
let args = [ ];
|
||||
let fields = [ ];
|
||||
for (let k in data) {
|
||||
fields.push(k + ' = ?');
|
||||
args.push(data[k]);
|
||||
if (k === 'result') {
|
||||
fields.push('resultJson = ?');
|
||||
args.push(JSON.stringify(data[k]));
|
||||
} else {
|
||||
fields.push(k + ' = ?');
|
||||
args.push(data[k]);
|
||||
}
|
||||
}
|
||||
args.push(id);
|
||||
|
||||
@@ -100,7 +108,7 @@ function listByTypePaged(type, page, perPage, callback) {
|
||||
data.push(type);
|
||||
}
|
||||
|
||||
query += ' ORDER BY creationTime DESC LIMIT ?,?';
|
||||
query += ' ORDER BY creationTime DESC, id DESC LIMIT ?,?'; // put latest task first
|
||||
|
||||
data.push((page-1)*perPage);
|
||||
data.push(perPage);
|
||||
|
||||
+3
-3
@@ -23,7 +23,7 @@ var appdb = require('./appdb.js'),
|
||||
mkdirp = require('mkdirp'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
sendFailureLogs = require('./logcollector.js').sendFailureLogs,
|
||||
eventlog = require('./eventlog.js'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
@@ -150,10 +150,10 @@ function startAppTask(appId, callback) {
|
||||
debug('Task for %s pid %s completed with status %s', appId, pid, code);
|
||||
if (code === null /* signal */ || (code !== 0 && code !== 50)) { // apptask crashed
|
||||
debug('Apptask crashed with code %s and signal %s', code, signal);
|
||||
sendFailureLogs('apptask', { unit: 'box' });
|
||||
appdb.update(appId, { installationState: appdb.ISTATE_ERROR, installationProgress: 'Apptask crashed with code ' + code + ' and signal ' + signal }, NOOP_CALLBACK);
|
||||
eventlog.add(eventlog.ACTION_APP_TASK_CRASH, { appId: appId }, { crashLogFile: logFilePath }, NOOP_CALLBACK);
|
||||
} else if (code === 50) {
|
||||
sendFailureLogs('apptask', { unit: 'box' });
|
||||
eventlog.add(eventlog.ACTION_APP_TASK_CRASH, { appId: appId }, { crashLogFile: logFilePath }, NOOP_CALLBACK);
|
||||
}
|
||||
delete gActiveTasks[appId];
|
||||
locker.unlock(locker.OP_APPTASK); // unlock event will trigger next task
|
||||
|
||||
+10
-7
@@ -18,6 +18,8 @@ exports = module.exports = {
|
||||
TASK_BACKUP: 'backup',
|
||||
TASK_UPDATE: 'update',
|
||||
TASK_RENEW_CERTS: 'renewcerts',
|
||||
TASK_PREPARE_DASHBOARD_DOMAIN: 'prepareDashboardDomain',
|
||||
TASK_CLEAN_BACKUPS: 'cleanBackups',
|
||||
|
||||
// testing
|
||||
_TASK_IDENTITY: '_identity',
|
||||
@@ -105,7 +107,7 @@ function startTask(type, args) {
|
||||
if (error) return events.emit('error', new TaskError(TaskError.INTERNAL_ERROR, error));
|
||||
|
||||
const logFile = `${paths.TASKS_LOG_DIR}/${taskId}.log`;
|
||||
let fd = safe.fs.openSync(logFile, 'a'); // will autoclose
|
||||
let fd = safe.fs.openSync(logFile, 'w'); // will autoclose
|
||||
if (!fd) {
|
||||
debug(`startTask: unable to get log filedescriptor ${safe.error.message}`);
|
||||
return events.emit('error', new TaskError(TaskError.INTERNAL_ERROR, error.message));
|
||||
@@ -119,7 +121,7 @@ function startTask(type, args) {
|
||||
|
||||
get(taskId, function (error, task) {
|
||||
if (!error && task.percent !== 100) { // task crashed or was killed by us (code 50)
|
||||
error = code === 0 ? new Error(`${taskId} task stopped`) : new Error(`${taskId} task crashed with code ${code} and signal ${signal}`);
|
||||
error = code === 0 ? new Error(`task ${taskId} stopped`) : new Error(`task ${taskId} crashed with code ${code} and signal ${signal}`);
|
||||
update(taskId, { percent: 100, errorMessage: error.message }, NOOP_CALLBACK);
|
||||
} else if (!error && task.errorMessage) {
|
||||
error = new Error(task.errorMessage);
|
||||
@@ -174,14 +176,15 @@ function getLogs(taskId, 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 ${taskId}`);
|
||||
|
||||
var lines = options.lines || 100,
|
||||
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');
|
||||
follow = options.follow;
|
||||
|
||||
let cmd = '/usr/bin/tail';
|
||||
var args = [ '--lines=' + lines ];
|
||||
|
||||
@@ -6,6 +6,7 @@ var assert = require('assert'),
|
||||
backups = require('./backups.js'),
|
||||
database = require('./database.js'),
|
||||
debug = require('debug')('box:taskworker'),
|
||||
domains = require('./domains.js'),
|
||||
reverseProxy = require('./reverseproxy.js'),
|
||||
tasks = require('./tasks.js'),
|
||||
updater = require('./updater.js');
|
||||
@@ -16,6 +17,8 @@ const TASKS = { // indexed by task type
|
||||
backup: backups.backupBoxAndApps,
|
||||
update: updater.update,
|
||||
renewcerts: reverseProxy.renewCerts,
|
||||
prepareDashboardDomain: domains.prepareDashboardDomain,
|
||||
cleanBackups: backups.cleanup,
|
||||
|
||||
_identity: (arg, progressCallback, callback) => callback(null, arg),
|
||||
_error: (arg, progressCallback, callback) => callback(new Error(`Failed for arg: ${arg}`)),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user