Compare commits

..

7 Commits

Author SHA1 Message Date
Johannes Zellner 51a69cce41 Ensure notifications are sorted by time descending
(cherry picked from commit 63310c44c0)
2019-05-14 09:49:26 -07:00
Johannes Zellner 8686832bd1 4.0.3 changes 2019-05-14 16:57:45 +02:00
Girish Ramakrishnan c93c06ba88 gcs: fix crash
(cherry picked from commit 05d3f8a667)
2019-05-12 18:14:17 -07:00
Girish Ramakrishnan 36d64b3566 4.0.2 changes
(cherry picked from commit 3fa45ea728)
2019-05-12 14:04:26 -07:00
Girish Ramakrishnan e0c531564c Add option to skip backup before update
(cherry picked from commit a7d2098f09)
2019-05-12 13:59:07 -07:00
Girish Ramakrishnan 1e0ec75f0a gcdns: fix crash
(cherry picked from commit e1ecb49d59)
2019-05-12 12:57:06 -07:00
Girish Ramakrishnan 36ac02d29f Fix crash because params was undefined
(cherry picked from commit 800e25a7a7)
2019-05-10 13:08:26 -07:00
57 changed files with 1457 additions and 1013 deletions
-25
View File
@@ -1600,28 +1600,3 @@
[4.0.3]
* Fix dashboard issue for non-admins
[4.1.0]
* Remove password requirement for uninstalling apps and users
* Hosting provider edition
* Enforce limits in mail container
* Fix crash when using unauthenticated relay
* Fix domain and tag filtering
* Customizable app icons
* Remove obsolete X-Frame-Options from nginx configs
* Give SFTP access based on access restriction
[4.1.1]
* Add UI hint about SFTP access restriction
[4.1.2]
* Accept incoming mail from a private relay
* Fix issue where unused addon images were not pruned
* Add UI for redirect from multiple domains
* Allow apps to be relocated to custom data directory
* Make all cloudron env vars have CLOUDRON_ prefix
* Update manifest version to 2
* Fix issue where DKIM keys were inaccessible
* Fix DKIM selector conflict when adding same domain across multiple cloudrons
* Fix name.com DNS backend issue for naked domains
* Add DigitalOcean Frankfurt (fra1) region for backup storage
+6 -1
View File
@@ -41,7 +41,12 @@ Try our demo at https://my.demo.cloudron.io (username: cloudron password: cloudr
## Installing
[Install script](https://cloudron.io/documentation/installation/) - [Pricing](https://cloudron.io/pricing.html)
You can install the Cloudron platform on your own server or get a managed server
from cloudron.io. In either case, the Cloudron platform will keep your server and
apps up-to-date and secure.
* [Selfhosting](https://cloudron.io/documentation/installation/) - [Pricing](https://cloudron.io/pricing.html)
* [Managed Hosting](https://cloudron.io/managed.html)
**Note:** This repo is a small part of what gets installed on your server - there is
the dashboard, database addons, graph container, base image etc. Cloudron also relies
+1 -4
View File
@@ -46,15 +46,12 @@ apt-get -y install \
openssh-server \
pwgen \
resolvconf \
sudo \
swaks \
tzdata \
unattended-upgrades \
unbound \
xfsprogs
# on some providers like scaleway the sudo file is changed and we want to keep the old one
apt-get -o Dpkg::Options::="--force-confold" install -y sudo
# this ensures that unattended upgades are enabled, if it was disabled during ubuntu install time (see #346)
# debconf-set-selection of unattended-upgrades/enable_auto_updates + dpkg-reconfigure does not work
cp /usr/share/unattended-upgrades/20auto-upgrades /etc/apt/apt.conf.d/20auto-upgrades
@@ -1,15 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE mail ADD COLUMN dkimSelector VARCHAR(128) NOT NULL DEFAULT "cloudron"', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE mail DROP COLUMN dkimSelector', function (error) {
if (error) console.error(error);
callback(error);
});
};
-2
View File
@@ -174,8 +174,6 @@ CREATE TABLE IF NOT EXISTS mail(
catchAllJson TEXT,
relayJson TEXT,
dkimSelector VARCHAR(128) NOT NULL DEFAULT "cloudron",
FOREIGN KEY(domain) REFERENCES domains(domain),
PRIMARY KEY(domain))
+263 -62
View File
@@ -129,6 +129,42 @@
"execa": "^1.0.0"
}
},
"@sinonjs/commons": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.4.0.tgz",
"integrity": "sha512-9jHK3YF/8HtJ9wCAbG+j8cD0i0+ATS9A7gXFqS36TblLPNy6rEEc+SB0imo91eCboGaBYGV/MT1/br/J+EE7Tw==",
"dev": true,
"requires": {
"type-detect": "4.0.8"
}
},
"@sinonjs/formatio": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.1.tgz",
"integrity": "sha512-tsHvOB24rvyvV2+zKMmPkZ7dXX6LSLKZ7aOtXY6Edklp0uRcgGpOsQTTGTcWViFyx4uhWc6GV8QdnALbIbIdeQ==",
"dev": true,
"requires": {
"@sinonjs/commons": "^1",
"@sinonjs/samsam": "^3.1.0"
}
},
"@sinonjs/samsam": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.1.tgz",
"integrity": "sha512-wRSfmyd81swH0hA1bxJZJ57xr22kC07a1N4zuIL47yTS04bDk6AoCkczcqHEjcRPmJ+FruGJ9WBQiJwMtIElFw==",
"dev": true,
"requires": {
"@sinonjs/commons": "^1.0.2",
"array-from": "^2.1.1",
"lodash": "^4.17.11"
}
},
"@sinonjs/text-encoding": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz",
"integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==",
"dev": true
},
"@types/caseless": {
"version": "0.12.2",
"resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz",
@@ -224,7 +260,7 @@
},
"amdefine": {
"version": "1.0.1",
"resolved": false,
"resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
"integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
"dev": true
},
@@ -283,9 +319,15 @@
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
},
"array-from": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz",
"integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=",
"dev": true
},
"arrify": {
"version": "1.0.1",
"resolved": false,
"resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
"integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0="
},
"asn1": {
@@ -298,7 +340,7 @@
},
"assert-plus": {
"version": "1.0.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
},
"assertion-error": {
@@ -359,7 +401,7 @@
},
"backoff": {
"version": "2.5.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz",
"integrity": "sha1-9hbtqdPktmuMp/ynn2lXIsX44m8=",
"requires": {
"precond": "0.2"
@@ -401,6 +443,14 @@
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz",
"integrity": "sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ=="
},
"bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"requires": {
"file-uri-to-path": "1.0.0"
}
},
"bl": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz",
@@ -497,7 +547,7 @@
},
"buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": false,
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk="
},
"buffer-fill": {
@@ -512,7 +562,7 @@
},
"bunyan": {
"version": "1.8.12",
"resolved": false,
"resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.12.tgz",
"integrity": "sha1-8VDw9nSKvdcq6uhPBEA74u8RN5c=",
"requires": {
"dtrace-provider": "~0.8",
@@ -620,9 +670,9 @@
}
},
"cloudron-manifestformat": {
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-2.15.0.tgz",
"integrity": "sha512-hSL+n/ttjrjZby/tSa5YSTRUAcxfzAi9CFUSPyu3dx8OMxzHsDyTvtKHjwBtIZ0Fjz7B3THfR3kfvIgP0lULSg==",
"version": "2.14.2",
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-2.14.2.tgz",
"integrity": "sha512-+VQwlP/2NY0VIjPTkANhlg8DrS62IxkAVS7B7KG6DrzRp+hRCejbDMQjUB8GvyPSVQGerqUoEu5R/tc4/UBiAA==",
"requires": {
"cron": "^1.0.9",
"java-packagename-regex": "^1.0.0",
@@ -651,7 +701,7 @@
},
"code-point-at": {
"version": "1.1.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
},
"color-convert": {
@@ -697,7 +747,7 @@
},
"concat-map": {
"version": "0.0.1",
"resolved": false,
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"concat-stream": {
@@ -884,7 +934,7 @@
},
"core-util-is": {
"version": "1.0.2",
"resolved": false,
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"cron": {
@@ -973,7 +1023,7 @@
},
"dashdash": {
"version": "1.14.1",
"resolved": false,
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
"integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
"requires": {
"assert-plus": "^1.0.0"
@@ -1048,7 +1098,7 @@
},
"decamelize": {
"version": "1.2.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
},
"deep-eql": {
@@ -1352,7 +1402,7 @@
},
"ent": {
"version": "2.2.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz",
"integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0="
},
"error-ex": {
@@ -1449,7 +1499,7 @@
},
"expect.js": {
"version": "0.3.1",
"resolved": false,
"resolved": "https://registry.npmjs.org/expect.js/-/expect.js-0.3.1.tgz",
"integrity": "sha1-sKWaDS7/VDdUTr8M6qYBWEHQm1s=",
"dev": true
},
@@ -1574,7 +1624,7 @@
},
"extsprintf": {
"version": "1.3.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
"integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU="
},
"eyes": {
@@ -1605,6 +1655,11 @@
"pend": "~1.2.0"
}
},
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="
},
"final-fs": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/final-fs/-/final-fs-1.6.1.tgz",
@@ -1722,7 +1777,7 @@
"dependencies": {
"mkdirp": {
"version": "0.3.5",
"resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz",
"integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=",
"dev": true
},
@@ -1734,7 +1789,7 @@
},
"rimraf": {
"version": "2.2.8",
"resolved": "http://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz",
"integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=",
"dev": true
}
@@ -2081,6 +2136,11 @@
}
}
},
"hoek": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz",
"integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA=="
},
"hosted-git-info": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz",
@@ -2193,7 +2253,7 @@
},
"inflight": {
"version": "1.0.6",
"resolved": false,
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"requires": {
"once": "^1.3.0",
@@ -2202,7 +2262,7 @@
},
"inherits": {
"version": "2.0.3",
"resolved": false,
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"ini": {
@@ -2222,7 +2282,7 @@
},
"is-arrayish": {
"version": "0.2.1",
"resolved": false,
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
"dev": true
},
@@ -2268,7 +2328,7 @@
},
"is-stream": {
"version": "1.1.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
"integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
},
"is-stream-ended": {
@@ -2298,12 +2358,20 @@
},
"isarray": {
"version": "1.0.0",
"resolved": false,
"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"
}
},
"isexe": {
"version": "2.0.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
},
"isstream": {
@@ -2321,6 +2389,23 @@
"resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz",
"integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc="
},
"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.5.1",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz",
@@ -2380,7 +2465,7 @@
},
"jsonfile": {
"version": "1.0.1",
"resolved": "http://registry.npmjs.org/jsonfile/-/jsonfile-1.0.1.tgz",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-1.0.1.tgz",
"integrity": "sha1-6l7+QLg2kLmGZ2FKc5L8YOhCwN0=",
"dev": true
},
@@ -2400,6 +2485,12 @@
"verror": "1.10.0"
}
},
"just-extend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz",
"integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==",
"dev": true
},
"jwa": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
@@ -2434,7 +2525,7 @@
},
"ldap-filter": {
"version": "0.2.2",
"resolved": false,
"resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.2.2.tgz",
"integrity": "sha1-8rhCvguG2jNSeYUFsx68rlkNd9A=",
"requires": {
"assert-plus": "0.1.5"
@@ -2442,7 +2533,7 @@
"dependencies": {
"assert-plus": {
"version": "0.1.5",
"resolved": false,
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz",
"integrity": "sha1-7nQAlBMALYTOxyGcasgRgS5yMWA="
}
}
@@ -2548,6 +2639,12 @@
"chalk": "^2.0.1"
}
},
"lolex": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lolex/-/lolex-4.0.1.tgz",
"integrity": "sha512-UHuOBZ5jjsKuzbB/gRNNW8Vg8f00Emgskdq2kvZxgBJCS0aqquAuXai/SkWORlKeZEiNQWZjFZOqIUcH9LqKCw==",
"dev": true
},
"loud-rejection": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz",
@@ -2672,19 +2769,19 @@
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "0.0.8",
"resolved": false,
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
},
"mkdirp": {
"version": "0.5.1",
"resolved": false,
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"requires": {
"minimist": "0.0.8"
@@ -2911,7 +3008,7 @@
},
"mv": {
"version": "2.1.1",
"resolved": false,
"resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz",
"integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=",
"optional": true,
"requires": {
@@ -2922,7 +3019,7 @@
"dependencies": {
"glob": {
"version": "6.0.4",
"resolved": false,
"resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
"integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=",
"optional": true,
"requires": {
@@ -2935,13 +3032,13 @@
},
"ncp": {
"version": "2.0.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz",
"integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=",
"optional": true
},
"rimraf": {
"version": "2.4.5",
"resolved": false,
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz",
"integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=",
"optional": true,
"requires": {
@@ -2961,6 +3058,14 @@
"sqlstring": "2.3.1"
}
},
"namecheap": {
"version": "github:joshuakarjala/node-namecheap#464a9528b7ded3ee2520c2688bc98cbffb08e603",
"from": "github:joshuakarjala/node-namecheap#464a952",
"requires": {
"request": "*",
"xml2json": "*"
}
},
"nan": {
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.12.1.tgz",
@@ -2981,6 +3086,42 @@
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="
},
"nise": {
"version": "1.4.10",
"resolved": "https://registry.npmjs.org/nise/-/nise-1.4.10.tgz",
"integrity": "sha512-sa0RRbj53dovjc7wombHmVli9ZihXbXCQ2uH3TNm03DyvOSIQbxg+pbqDKrk2oxMK1rtLGVlKxcB9rrc6X5YjA==",
"dev": true,
"requires": {
"@sinonjs/formatio": "^3.1.0",
"@sinonjs/text-encoding": "^0.7.1",
"just-extend": "^4.0.2",
"lolex": "^2.3.2",
"path-to-regexp": "^1.7.0"
},
"dependencies": {
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
"dev": true
},
"lolex": {
"version": "2.7.5",
"resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz",
"integrity": "sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q==",
"dev": true
},
"path-to-regexp": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz",
"integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=",
"dev": true,
"requires": {
"isarray": "0.0.1"
}
}
}
},
"nock": {
"version": "10.0.6",
"resolved": "https://registry.npmjs.org/nock/-/nock-10.0.6.tgz",
@@ -3030,6 +3171,15 @@
}
}
},
"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"
}
},
"node-fetch": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.3.0.tgz",
@@ -3203,7 +3353,7 @@
},
"nopt": {
"version": "3.0.6",
"resolved": false,
"resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
"integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=",
"dev": true,
"requires": {
@@ -3252,7 +3402,7 @@
},
"number-is-nan": {
"version": "1.0.1",
"resolved": false,
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
},
"oauth-sign": {
@@ -3403,7 +3553,7 @@
},
"p-finally": {
"version": "1.0.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4="
},
"p-is-promise": {
@@ -3442,7 +3592,7 @@
},
"parse-json": {
"version": "2.2.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
"integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=",
"dev": true,
"requires": {
@@ -3512,7 +3662,7 @@
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": false,
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
},
"path-key": {
@@ -3607,7 +3757,7 @@
},
"precond": {
"version": "0.2.3",
"resolved": false,
"resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz",
"integrity": "sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw="
},
"process-nextick-args": {
@@ -3659,7 +3809,7 @@
},
"pseudomap": {
"version": "1.0.2",
"resolved": false,
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
"integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM="
},
"psl": {
@@ -3888,12 +4038,12 @@
},
"require-directory": {
"version": "2.1.1",
"resolved": false,
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I="
},
"require-main-filename": {
"version": "1.0.1",
"resolved": false,
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
"integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE="
},
"resolve": {
@@ -4177,7 +4327,7 @@
},
"set-blocking": {
"version": "2.0.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
},
"setprototypeof": {
@@ -4187,7 +4337,7 @@
},
"shebang-command": {
"version": "1.2.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
"integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
"requires": {
"shebang-regex": "^1.0.0"
@@ -4195,7 +4345,7 @@
},
"shebang-regex": {
"version": "1.0.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
"integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM="
},
"showdown": {
@@ -4364,9 +4514,35 @@
},
"signal-exit": {
"version": "3.0.2",
"resolved": false,
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
},
"sinon": {
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/sinon/-/sinon-7.3.2.tgz",
"integrity": "sha512-thErC1z64BeyGiPvF8aoSg0LEnptSaWE7YhdWWbWXgelOyThent7uKOnnEh9zBxDbKixtr5dEko+ws1sZMuFMA==",
"dev": true,
"requires": {
"@sinonjs/commons": "^1.4.0",
"@sinonjs/formatio": "^3.2.1",
"@sinonjs/samsam": "^3.3.1",
"diff": "^3.5.0",
"lolex": "^4.0.1",
"nise": "^1.4.10",
"supports-color": "^5.5.0"
},
"dependencies": {
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
}
}
}
},
"smtp-connection": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/smtp-connection/-/smtp-connection-2.12.0.tgz",
@@ -4449,7 +4625,7 @@
},
"sprintf-js": {
"version": "1.0.3",
"resolved": false,
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
},
"sqlstring": {
@@ -4527,7 +4703,7 @@
},
"stream-shift": {
"version": "1.0.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz",
"integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI="
},
"streamsearch": {
@@ -4576,7 +4752,7 @@
},
"strip-eof": {
"version": "1.0.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
"integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8="
},
"strip-indent": {
@@ -4595,7 +4771,7 @@
},
"stubs": {
"version": "3.0.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz",
"integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls="
},
"superagent": {
@@ -4784,6 +4960,21 @@
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
},
"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.4.3",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
@@ -4880,7 +5071,7 @@
},
"typedarray": {
"version": "0.0.6",
"resolved": false,
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
},
"uid-safe": {
@@ -4957,7 +5148,7 @@
},
"util-deprecate": {
"version": "1.0.2",
"resolved": false,
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"utile": {
@@ -5017,7 +5208,7 @@
},
"vasync": {
"version": "1.6.4",
"resolved": false,
"resolved": "https://registry.npmjs.org/vasync/-/vasync-1.6.4.tgz",
"integrity": "sha1-3+k2Fq0OeugBszKp2Iv8XNyOHR8=",
"requires": {
"verror": "1.6.0"
@@ -5025,12 +5216,12 @@
"dependencies": {
"extsprintf": {
"version": "1.2.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.2.0.tgz",
"integrity": "sha1-WtlGwi9bMrp/jNdCZxHG6KP8JSk="
},
"verror": {
"version": "1.6.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/verror/-/verror-1.6.0.tgz",
"integrity": "sha1-fROyex+swuLakEBetepuW90lLqU=",
"requires": {
"extsprintf": "1.2.0"
@@ -5040,7 +5231,7 @@
},
"verror": {
"version": "1.10.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
"integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
"requires": {
"assert-plus": "^1.0.0",
@@ -5063,7 +5254,7 @@
},
"which-module": {
"version": "2.0.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho="
},
"wide-align": {
@@ -5118,7 +5309,7 @@
},
"wrap-ansi": {
"version": "2.1.0",
"resolved": false,
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
"integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
"requires": {
"string-width": "^1.0.1",
@@ -5160,7 +5351,7 @@
},
"wrappy": {
"version": "1.0.2",
"resolved": false,
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"write-file-atomic": {
@@ -5195,6 +5386,16 @@
"xmlbuilder": "~9.0.1"
}
},
"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"
}
},
"xmlbuilder": {
"version": "9.0.7",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz",
@@ -5208,7 +5409,7 @@
},
"xtend": {
"version": "4.0.1",
"resolved": false,
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
"integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68="
},
"y18n": {
+5 -5
View File
@@ -20,7 +20,7 @@
"async": "^2.6.2",
"aws-sdk": "^2.441.0",
"body-parser": "^1.18.3",
"cloudron-manifestformat": "^2.15.0",
"cloudron-manifestformat": "^2.14.2",
"connect": "^3.6.6",
"connect-ensure-login": "^0.1.1",
"connect-lastmile": "^1.0.2",
@@ -40,13 +40,13 @@
"js-yaml": "^3.13.1",
"json": "^9.0.6",
"ldapjs": "^1.0.2",
"lodash": "^4.17.11",
"lodash.chunk": "^4.2.0",
"mime": "^2.4.2",
"moment-timezone": "^0.5.25",
"morgan": "^1.9.1",
"multiparty": "^4.2.1",
"mysql": "^2.17.1",
"namecheap": "github:joshuakarjala/node-namecheap#464a952",
"nodemailer": "^6.1.1",
"nodemailer-smtp-transport": "^2.7.4",
"oauth2orize": "^1.11.0",
@@ -78,8 +78,7 @@
"uuid": "^3.3.2",
"valid-url": "^1.0.9",
"validator": "^10.11.0",
"ws": "^6.2.1",
"xml2js": "^0.4.19"
"ws": "^6.2.1"
},
"devDependencies": {
"expect.js": "*",
@@ -89,7 +88,8 @@
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
"nock": "^10.0.6",
"node-sass": "^4.11.0",
"recursive-readdir": "^2.2.2"
"recursive-readdir": "^2.2.2",
"sinon": "^7.3.2"
},
"scripts": {
"test": "./runTests",
+106
View File
@@ -0,0 +1,106 @@
#!/bin/bash
set -eu -o pipefail
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400 --http1.1"
ip=""
dns_config=""
tls_cert_file=""
tls_key_file=""
license_file=""
backup_config=""
args=$(getopt -o "" -l "ip:,backup-config:,license:,dns-config:,tls-cert:,tls-key:" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--ip) ip="$2"; shift 2;;
--dns-config) dns_config="$2"; shift 2;;
--tls-cert) tls_cert_file="$2"; shift 2;;
--tls-key) tls_key_file="$2"; shift 2;;
--license) license_file="$2"; shift 2;;
--backup-config) backup_config="$2"; shift 2;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
done
# validate arguments in the absence of data
if [[ -z "${ip}" ]]; then
echo "--ip is required"
exit 1
fi
if [[ -z "${dns_config}" ]]; then
echo "--dns-config is required"
exit 1
fi
if [[ ! -f "${license_file}" ]]; then
echo "--license must be a valid license file"
exit 1
fi
function get_status() {
key="$1"
if status=$($curl -q -f -k "https://${ip}/api/v1/cloudron/status" 2>/dev/null); then
currentValue=$(echo "${status}" | python3 -c 'import sys, json; print(json.dumps(json.load(sys.stdin)[sys.argv[1]]))' "${key}")
echo "${currentValue}"
return 0
fi
return 1
}
function wait_for_status() {
key="$1"
expectedValue="$2"
echo "wait_for_status: $key to be $expectedValue"
while true; do
if currentValue=$(get_status "${key}"); then
echo "wait_for_status: $key is current: $currentValue expecting: $expectedValue"
if [[ "${currentValue}" == $expectedValue ]]; then
break
fi
fi
sleep 3
done
}
echo "=> Waiting for cloudron to be ready"
wait_for_status "version" '*'
domain=$(echo "${dns_config}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["domain"])')
echo "Provisioning Cloudron ${domain}"
if [[ -n "${tls_cert_file}" && -n "${tls_key_file}" ]]; then
tls_cert=$(cat "${tls_cert_file}" | awk '{printf "%s\\n", $0}')
tls_key=$(cat "${tls_key_file}" | awk '{printf "%s\\n", $0}')
fallback_cert=$(printf '{ "cert": "%s", "key": "%s", "provider": "fallback", "restricted": true }' "${tls_cert}" "${tls_key}")
else
fallback_cert=None
fi
tls_config='{ "provider": "fallback" }'
dns_config=$(echo "${dns_config}" | python3 -c "import json,sys;obj=json.load(sys.stdin);obj.update(tlsConfig=${tls_config});obj.update(fallbackCertficate=${fallback_cert});print(json.dumps(obj))")
license=$(cat "${license_file}")
if [[ -z "${backup_config:-}" ]]; then
backup_config='{ "provider": "filesystem", "backupFolder": "/var/backups", "format": "tgz" }'
fi
setupData=$(printf '{ "dnsConfig": %s, "autoconf": { "appstoreConfig": %s, "backupConfig": %s } }' "${dns_config}" "${license}" "${backup_config}")
if ! setupResult=$($curl -kq -X POST -H "Content-Type: application/json" -d "${setupData}" https://${ip}/api/v1/cloudron/setup); then
echo "Failed to setup with ${setupData} ${setupResult}"
exit 1
fi
wait_for_status "webadminStatus" '*"tls": true*'
echo "Cloudron is ready at https://my-${domain}"
+25 -37
View File
@@ -1,7 +1,5 @@
#!/bin/bash
set -eu -o pipefail
# This script collects diagnostic information to help debug server related issues
# It also enables SSH access for the cloudron support team
@@ -19,7 +17,7 @@ This script collects diagnostic information to help debug server related issues
# We require root
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root. Run with sudo"
echo "This script should be run as root." > /dev/stderr
exit 1
fi
@@ -60,6 +58,21 @@ echo -n "Generating Cloudron Support stats..."
# clear file
rm -rf $OUT
ssh_port=$(cat /etc/ssh/sshd_config | grep "Port " | sed -e "s/.*Port //")
if [[ $SUDO_USER == "" ]]; then
ssh_user="root"
ssh_folder="/root/.ssh/"
authorized_key_file="${ssh_folder}/authorized_keys"
else
ssh_user="$SUDO_USER"
ssh_folder="/home/$SUDO_USER/.ssh/"
authorized_key_file="${ssh_folder}/authorized_keys"
fi
echo -e $LINE"SSH"$LINE >> $OUT
echo "Username: ${ssh_user}" >> $OUT
echo "Port: ${ssh_port}" >> $OUT
echo -e $LINE"cloudron.conf"$LINE >> $OUT
cat /etc/cloudron/cloudron.conf &>> $OUT
@@ -86,50 +99,25 @@ systemctl status --lines=100 cloudron.target box mysql unbound cloudron-syslog n
echo -e $LINE"Box logs"$LINE >> $OUT
tail -n 100 /home/yellowtent/platformdata/logs/box.log &>> $OUT
echo -e $LINE"Firewall chains"$LINE >> $OUT
ip addr &>> $OUT
echo -e $LINE"Firewall chains"$LINE >> $OUT
iptables -L &>> $OUT
echo "Done"
if [[ "${enableSSH}" == "true" ]]; then
ssh_port=$(cat /etc/ssh/sshd_config | grep "Port " | sed -e "s/.*Port //")
permit_root_login=$(grep -q ^PermitRootLogin.*yes /etc/ssh/sshd_config && echo "yes" || echo "no")
# support.js uses similar logic
if $(grep -q "ec2\|lightsail\|ami" /etc/cloudron/cloudron.conf); then
ssh_user="ubuntu"
keys_file="/home/ubuntu/.ssh/authorized_keys"
else
ssh_user="root"
keys_file="/root/.ssh/authorized_keys"
fi
echo -e $LINE"SSH"$LINE >> $OUT
echo "Username: ${ssh_user}" >> $OUT
echo "Port: ${ssh_port}" >> $OUT
echo "PermitRootLogin: ${permit_root_login}" >> $OUT
echo "Key file: ${keys_file}" >> $OUT
echo -n "Enabling ssh access for the Cloudron support team..."
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 "${ssh_user}" "${keys_file}"
fi
echo "Done"
fi
echo -n "Uploading information..."
# for some reason not using $(cat $OUT) will not contain newlines!?
paste_key=$(curl -X POST ${PASTEBIN}/documents --silent -d "$(cat $OUT)" | python3 -c "import sys, json; print(json.load(sys.stdin)['key'])")
echo "Done"
if [[ "${enableSSH}" == "true" ]]; then
echo -n "Enabling ssh access for the Cloudron support team..."
mkdir -p "${ssh_folder}"
echo -e "\n${CLOUDRON_SUPPORT_PUBLIC_KEY}" >> ${authorized_key_file}
chown -R ${ssh_user} "${ssh_folder}"
chmod 600 "${authorized_key_file}"
echo "Done"
fi
echo ""
echo "Please email the following link to support@cloudron.io"
echo ""
+3
View File
@@ -110,6 +110,9 @@ systemctl restart cloudron-syslog
$json -f /etc/cloudron/cloudron.conf -I -e "delete this.edition" # can be removed after 4.0
echo "==> Clearing custom.yml"
rm -f /etc/cloudron/custom.yml
echo "==> Configuring sudoers"
rm -f /etc/sudoers.d/${USER}
cp "${script_dir}/start/sudoers" /etc/sudoers.d/${USER}
+7 -31
View File
@@ -1,40 +1,16 @@
# add customizations here
# after making changes run "sudo systemctl restart box"
# appstore:
# blacklist:
# - io.wekan.cloudronapp
# - io.cloudron.openvpn
# whitelist:
# org.wordpress.cloudronapp: {}
# chat.rocket.cloudronapp: {}
# com.nextcloud.cloudronapp: {}
#
# backups:
# configurable: true
#
# domains:
# features:
# configureBackup: true
# dynamicDns: true
# changeDashboardDomain: true
#
# subscription:
# configurable: true
#
# subscription: true
# remoteSupport: true
#
# support:
# email: support@cloudron.io
# remoteSupport: true
#
# ticketFormBody: |
# Use this form to open support tickets. You can also write directly to [support@cloudron.io](mailto:support@cloudron.io).
# * [Knowledge Base & App Docs](https://cloudron.io/documentation/apps/?support_view)
# * [Custom App Packaging & API](https://cloudron.io/developer/packaging/?support_view)
# * [Forum](https://forum.cloudron.io/)
#
# submitTickets: true
#
#
# alerts:
# email: support@cloudron.io
# notifyCloudronAdmins: false
#
# footer:
# body: '&copy; 2019 [Cloudron](https://cloudron.io) [Forum <i class="fa fa-comments"></i>](https://forum.cloudron.io)'
+52 -74
View File
@@ -795,12 +795,10 @@ function setupOauth(app, options, callback) {
clients.add(appId, clients.TYPE_OAUTH, redirectURI, scope, function (error, result) {
if (error) return callback(error);
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
var env = [
{ name: `${envPrefix}OAUTH_CLIENT_ID`, value: result.id },
{ name: `${envPrefix}OAUTH_CLIENT_SECRET`, value: result.clientSecret },
{ name: `${envPrefix}OAUTH_ORIGIN`, value: config.adminOrigin() }
{ name: 'OAUTH_CLIENT_ID', value: result.id },
{ name: 'OAUTH_CLIENT_SECRET', value: result.clientSecret },
{ name: 'OAUTH_ORIGIN', value: config.adminOrigin() }
];
debugApp(app, 'Setting oauth addon config to %j', env);
@@ -834,19 +832,17 @@ function setupEmail(app, options, callback) {
const mailInDomains = mailDomains.filter(function (d) { return d.enabled; }).map(function (d) { return d.domain; }).join(',');
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
// note that "external" access info can be derived from MAIL_DOMAIN (since it's part of user documentation)
var env = [
{ name: `${envPrefix}MAIL_SMTP_SERVER`, value: 'mail' },
{ name: `${envPrefix}MAIL_SMTP_PORT`, value: '2525' },
{ name: `${envPrefix}MAIL_IMAP_SERVER`, value: 'mail' },
{ name: `${envPrefix}MAIL_IMAP_PORT`, value: '9993' },
{ name: `${envPrefix}MAIL_SIEVE_SERVER`, value: 'mail' },
{ name: `${envPrefix}MAIL_SIEVE_PORT`, value: '4190' },
{ name: `${envPrefix}MAIL_DOMAIN`, value: app.domain },
{ name: `${envPrefix}MAIL_DOMAINS`, value: mailInDomains },
{ name: `${envPrefix}LDAP_MAILBOXES_BASE_DN`, value: 'ou=mailboxes,dc=cloudron' }
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
{ name: 'MAIL_SMTP_PORT', value: '2525' },
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
{ name: 'MAIL_IMAP_PORT', value: '9993' },
{ name: 'MAIL_SIEVE_SERVER', value: 'mail' },
{ name: 'MAIL_SIEVE_PORT', value: '4190' },
{ name: 'MAIL_DOMAIN', value: app.domain },
{ name: 'MAIL_DOMAINS', value: mailInDomains },
{ name: 'LDAP_MAILBOXES_BASE_DN', value: 'ou=mailboxes,dc=cloudron' }
];
debugApp(app, 'Setting up Email');
@@ -872,16 +868,14 @@ function setupLdap(app, options, callback) {
if (!app.sso) return callback(null);
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
var env = [
{ name: `${envPrefix}LDAP_SERVER`, value: '172.18.0.1' },
{ name: `${envPrefix}LDAP_PORT`, value: '' + config.get('ldapPort') },
{ name: `${envPrefix}LDAP_URL`, value: 'ldap://172.18.0.1:' + config.get('ldapPort') },
{ name: `${envPrefix}LDAP_USERS_BASE_DN`, value: 'ou=users,dc=cloudron' },
{ name: `${envPrefix}LDAP_GROUPS_BASE_DN`, value: 'ou=groups,dc=cloudron' },
{ name: `${envPrefix}LDAP_BIND_DN`, value: 'cn='+ app.id + ',ou=apps,dc=cloudron' },
{ name: `${envPrefix}LDAP_BIND_PASSWORD`, value: hat(4 * 128) } // this is ignored
{ name: 'LDAP_SERVER', value: '172.18.0.1' },
{ name: 'LDAP_PORT', value: '' + config.get('ldapPort') },
{ name: 'LDAP_URL', value: 'ldap://172.18.0.1:' + config.get('ldapPort') },
{ name: 'LDAP_USERS_BASE_DN', value: 'ou=users,dc=cloudron' },
{ name: 'LDAP_GROUPS_BASE_DN', value: 'ou=groups,dc=cloudron' },
{ name: 'LDAP_BIND_DN', value: 'cn='+ app.id + ',ou=apps,dc=cloudron' },
{ name: 'LDAP_BIND_PASSWORD', value: hat(4 * 128) } // this is ignored
];
debugApp(app, 'Setting up LDAP');
@@ -911,16 +905,14 @@ function setupSendMail(app, options, callback) {
var password = error ? hat(4 * 48) : existingPassword; // see box#565 for password length
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
var env = [
{ name: `${envPrefix}MAIL_SMTP_SERVER`, value: 'mail' },
{ name: `${envPrefix}MAIL_SMTP_PORT`, value: '2525' },
{ name: `${envPrefix}MAIL_SMTPS_PORT`, value: '2465' },
{ name: `${envPrefix}MAIL_SMTP_USERNAME`, value: app.mailboxName + '@' + app.domain },
{ name: `${envPrefix}MAIL_SMTP_PASSWORD`, value: password },
{ name: `${envPrefix}MAIL_FROM`, value: app.mailboxName + '@' + app.domain },
{ name: `${envPrefix}MAIL_DOMAIN`, value: app.domain }
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
{ name: 'MAIL_SMTP_PORT', value: '2525' },
{ name: 'MAIL_SMTPS_PORT', value: '2465' },
{ name: 'MAIL_SMTP_USERNAME', value: app.mailboxName + '@' + app.domain },
{ name: 'MAIL_SMTP_PASSWORD', value: password },
{ name: 'MAIL_FROM', value: app.mailboxName + '@' + app.domain },
{ name: 'MAIL_DOMAIN', value: app.domain }
];
debugApp(app, 'Setting sendmail addon config to %j', env);
appdb.setAddonConfig(app.id, 'sendmail', env, callback);
@@ -949,15 +941,13 @@ function setupRecvMail(app, options, callback) {
var password = error ? hat(4 * 48) : existingPassword; // see box#565 for password length
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
var env = [
{ name: `${envPrefix}MAIL_IMAP_SERVER`, value: 'mail' },
{ name: `${envPrefix}MAIL_IMAP_PORT`, value: '9993' },
{ name: `${envPrefix}MAIL_IMAP_USERNAME`, value: app.mailboxName + '@' + app.domain },
{ name: `${envPrefix}MAIL_IMAP_PASSWORD`, value: password },
{ name: `${envPrefix}MAIL_TO`, value: app.mailboxName + '@' + app.domain },
{ name: `${envPrefix}MAIL_DOMAIN`, value: app.domain }
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
{ name: 'MAIL_IMAP_PORT', value: '9993' },
{ name: 'MAIL_IMAP_USERNAME', value: app.mailboxName + '@' + app.domain },
{ name: 'MAIL_IMAP_PASSWORD', value: password },
{ name: 'MAIL_TO', value: app.mailboxName + '@' + app.domain },
{ name: 'MAIL_DOMAIN', value: app.domain }
];
debugApp(app, 'Setting sendmail addon config to %j', env);
@@ -1002,7 +992,6 @@ function startMysql(existingInfra, callback) {
if (error) return callback(error);
const cmd = `docker run --restart=always -d --name="mysql" \
--hostname mysql \
--net cloudron \
--net-alias mysql \
--log-driver syslog \
@@ -1059,13 +1048,11 @@ function setupMySql(app, options, callback) {
if (error) return callback(new Error('Error setting up mysql: ' + error));
if (response.statusCode !== 201) return callback(new Error(`Error setting up mysql. Status code: ${response.statusCode} message: ${response.body.message}`));
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
var env = [
{ name: `${envPrefix}MYSQL_USERNAME`, value: data.username },
{ name: `${envPrefix}MYSQL_PASSWORD`, value: data.password },
{ name: `${envPrefix}MYSQL_HOST`, value: 'mysql' },
{ name: `${envPrefix}MYSQL_PORT`, value: '3306' }
{ name: 'MYSQL_USERNAME', value: data.username },
{ name: 'MYSQL_PASSWORD', value: data.password },
{ name: 'MYSQL_HOST', value: 'mysql' },
{ name: 'MYSQL_PORT', value: '3306' }
];
if (options.multipleDatabases) {
@@ -1217,7 +1204,6 @@ function startPostgresql(existingInfra, callback) {
if (error) return callback(error);
const cmd = `docker run --restart=always -d --name="postgresql" \
--hostname postgresql \
--net cloudron \
--net-alias postgresql \
--log-driver syslog \
@@ -1272,15 +1258,13 @@ function setupPostgreSql(app, options, callback) {
if (error) return callback(new Error('Error setting up postgresql: ' + error));
if (response.statusCode !== 201) return callback(new Error(`Error setting up postgresql. Status code: ${response.statusCode} message: ${response.body.message}`));
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
var env = [
{ name: `${envPrefix}POSTGRESQL_URL`, value: `postgres://${data.username}:${data.password}@postgresql/${data.database}` },
{ name: `${envPrefix}POSTGRESQL_USERNAME`, value: data.username },
{ name: `${envPrefix}POSTGRESQL_PASSWORD`, value: data.password },
{ name: `${envPrefix}POSTGRESQL_HOST`, value: 'postgresql' },
{ name: `${envPrefix}POSTGRESQL_PORT`, value: '5432' },
{ name: `${envPrefix}POSTGRESQL_DATABASE`, value: data.database }
{ name: 'POSTGRESQL_URL', value: `postgres://${data.username}:${data.password}@postgresql/${data.database}` },
{ name: 'POSTGRESQL_USERNAME', value: data.username },
{ name: 'POSTGRESQL_PASSWORD', value: data.password },
{ name: 'POSTGRESQL_HOST', value: 'postgresql' },
{ name: 'POSTGRESQL_PORT', value: '5432' },
{ name: 'POSTGRESQL_DATABASE', value: data.database }
];
debugApp(app, 'Setting postgresql addon config to %j', env);
@@ -1394,7 +1378,6 @@ function startMongodb(existingInfra, callback) {
if (error) return callback(error);
const cmd = `docker run --restart=always -d --name="mongodb" \
--hostname mongodb \
--net cloudron \
--net-alias mongodb \
--log-driver syslog \
@@ -1447,15 +1430,13 @@ function setupMongoDb(app, options, callback) {
if (error) return callback(new Error('Error setting up mongodb: ' + error));
if (response.statusCode !== 201) return callback(new Error(`Error setting up mongodb. Status code: ${response.statusCode}`));
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
var env = [
{ name: `${envPrefix}MONGODB_URL`, value : `mongodb://${data.username}:${data.password}@mongodb/${data.database}` },
{ name: `${envPrefix}MONGODB_USERNAME`, value : data.username },
{ name: `${envPrefix}MONGODB_PASSWORD`, value: data.password },
{ name: `${envPrefix}MONGODB_HOST`, value : 'mongodb' },
{ name: `${envPrefix}MONGODB_PORT`, value : '27017' },
{ name: `${envPrefix}MONGODB_DATABASE`, value : data.database }
{ name: 'MONGODB_URL', value : `mongodb://${data.username}:${data.password}@mongodb/${data.database}` },
{ name: 'MONGODB_USERNAME', value : data.username },
{ name: 'MONGODB_PASSWORD', value: data.password },
{ name: 'MONGODB_HOST', value : 'mongodb' },
{ name: 'MONGODB_PORT', value : '27017' },
{ name: 'MONGODB_DATABASE', value : data.database }
];
debugApp(app, 'Setting mongodb addon config to %j', env);
@@ -1591,7 +1572,6 @@ function setupRedis(app, options, callback) {
const label = app.fqdn;
// note that we do not add appId label because this interferes with the stop/start app logic
const cmd = `docker run --restart=always -d --name=${redisName} \
--hostname ${redisName} \
--label=location=${label} \
--net cloudron \
--net-alias ${redisName} \
@@ -1609,13 +1589,11 @@ function setupRedis(app, options, callback) {
--label isCloudronManaged=true \
--read-only -v /tmp -v /run ${tag}`;
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
var env = [
{ name: `${envPrefix}REDIS_URL`, value: 'redis://redisuser:' + redisPassword + '@redis-' + app.id },
{ name: `${envPrefix}REDIS_PASSWORD`, value: redisPassword },
{ name: `${envPrefix}REDIS_HOST`, value: redisName },
{ name: `${envPrefix}REDIS_PORT`, value: '6379' }
{ name: 'REDIS_URL', value: 'redis://redisuser:' + redisPassword + '@redis-' + app.id },
{ name: 'REDIS_PASSWORD', value: redisPassword },
{ name: 'REDIS_HOST', value: redisName },
{ name: 'REDIS_PORT', value: '6379' }
];
async.series([
+4
View File
@@ -72,6 +72,10 @@ server {
ssl_dhparam /home/yellowtent/boxdata/dhparams.pem;
add_header Strict-Transport-Security "max-age=15768000";
# https://developer.mozilla.org/en-US/docs/Web/HTTP/X-Frame-Options
add_header X-Frame-Options "<%= xFrameOptions %>";
proxy_hide_header X-Frame-Options;
# https://github.com/twitter/secureheaders
# https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#tab=Compatibility_Matrix
# https://wiki.mozilla.org/Security/Guidelines/Web_Security
+8 -4
View File
@@ -69,7 +69,7 @@ var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationSta
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'subdomains.subdomain AS location', 'subdomains.domain',
'apps.accessRestrictionJson', 'apps.restoreConfigJson', 'apps.oldConfigJson', 'apps.updateConfigJson', 'apps.memoryLimit',
'apps.label', 'apps.tagsJson',
'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
'apps.creationTime', 'apps.updateTime', 'apps.ownerId', 'apps.mailboxName', 'apps.enableAutomaticUpdate',
'apps.dataDir', 'apps.ts', 'apps.healthTime' ].join(',');
@@ -121,6 +121,9 @@ function postProcess(result) {
if (result.accessRestriction && !result.accessRestriction.users) result.accessRestriction.users = [];
delete result.accessRestrictionJson;
// TODO remove later once all apps have this attribute
result.xFrameOptions = result.xFrameOptions || 'SAMEORIGIN';
result.sso = !!result.sso; // make it bool
result.enableBackup = !!result.enableBackup; // make it bool
result.enableAutomaticUpdate = !!result.enableAutomaticUpdate; // make it bool
@@ -276,6 +279,7 @@ function add(id, appStoreId, manifest, location, domain, ownerId, portBindings,
const accessRestriction = data.accessRestriction || null;
const accessRestrictionJson = JSON.stringify(accessRestriction);
const memoryLimit = data.memoryLimit || 0;
const xFrameOptions = data.xFrameOptions || '';
const installationState = data.installationState || exports.ISTATE_PENDING_INSTALL;
const restoreConfigJson = data.restoreConfig ? JSON.stringify(data.restoreConfig) : null; // used when cloning
const sso = 'sso' in data ? data.sso : null;
@@ -289,10 +293,10 @@ function add(id, appStoreId, manifest, location, domain, ownerId, portBindings,
var queries = [];
queries.push({
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, '
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, xFrameOptions,'
+ 'restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId, mailboxName, label, tagsJson) '
+ ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, restoreConfigJson,
+ ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson,
sso, debugModeJson, robotsTxt, ownerId, mailboxName, label, tagsJson ]
});
+43 -45
View File
@@ -90,6 +90,7 @@ var appdb = require('./appdb.js'),
taskmanager = require('./taskmanager.js'),
TransformStream = require('stream').Transform,
updateChecker = require('./updatechecker.js'),
url = require('url'),
util = require('util'),
uuid = require('uuid'),
validator = require('validator'),
@@ -240,6 +241,20 @@ function validateMemoryLimit(manifest, memoryLimit) {
return null;
}
// https://tools.ietf.org/html/rfc7034
function validateXFrameOptions(xFrameOptions) {
assert.strictEqual(typeof xFrameOptions, 'string');
if (xFrameOptions === 'DENY') return null;
if (xFrameOptions === 'SAMEORIGIN') return null;
var parts = xFrameOptions.split(' ');
if (parts.length !== 2 || parts[0] !== 'ALLOW-FROM') return new AppsError(AppsError.BAD_FIELD, 'xFrameOptions must be "DENY", "SAMEORIGIN" or "ALLOW-FROM uri"' );
var uri = url.parse(parts[1]);
return (uri.protocol === 'http:' || uri.protocol === 'https:') ? null : new AppsError(AppsError.BAD_FIELD, 'xFrameOptions ALLOW-FROM uri must be a valid http[s] uri' );
}
function validateDebugMode(debugMode) {
assert.strictEqual(typeof debugMode, 'object');
@@ -321,12 +336,11 @@ function validateDataDir(dataDir) {
return null;
}
function getDuplicateErrorDetails(error, location, domainObject, portBindings, alternateDomains) {
function getDuplicateErrorDetails(error, location, domainObject, portBindings) {
assert.strictEqual(error.reason, DatabaseError.ALREADY_EXISTS);
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof portBindings, 'object');
assert(Array.isArray(alternateDomains));
var match = error.message.match(/ER_DUP_ENTRY: Duplicate entry '(.*)' for key '(.*)'/);
if (!match) {
@@ -334,17 +348,10 @@ function getDuplicateErrorDetails(error, location, domainObject, portBindings, a
return new AppsError(AppsError.INTERNAL_ERROR, error);
}
// check if the location or alternateDomains conflicts
// check if the location conflicts
if (match[2] === 'subdomain') {
// mysql reports a unique conflict with a dash: eg. domain:example.com subdomain:test => test-example.com
if (match[1] === `${location}-${domainObject.domain}`) return new AppsError(AppsError.ALREADY_EXISTS, `Domain '${domains.fqdn(location, domainObject)}' is in use`);
// check alternateDomains
let tmp = alternateDomains.filter(function (d) {
return match[1] === `${d.subdomain}-${d.domain}`;
});
if (tmp.length > 0) return new AppsError(AppsError.ALREADY_EXISTS, `Alternate domain '${tmp[0].subdomain}.${tmp[0].domain}' is in use`);
const fqdn = domains.fqdn(location, domainObject);
return new AppsError(AppsError.ALREADY_EXISTS, `subdomain '${fqdn}' is in use`);
}
// check if any of the port bindings conflict
@@ -365,7 +372,7 @@ function getAppConfig(app) {
accessRestriction: app.accessRestriction,
portBindings: app.portBindings,
memoryLimit: app.memoryLimit,
xFrameOptions: app.xFrameOptions || 'SAMEORIGIN',
robotsTxt: app.robotsTxt,
sso: app.sso,
alternateDomains: app.alternateDomains || [],
@@ -382,7 +389,7 @@ function removeInternalFields(app) {
return _.pick(app,
'id', 'appStoreId', 'installationState', 'installationProgress', 'runState', 'health',
'location', 'domain', 'fqdn', 'mailboxName',
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit',
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'xFrameOptions',
'sso', 'debugMode', 'robotsTxt', 'enableBackup', 'creationTime', 'updateTime', 'ts', 'tags',
'label', 'alternateDomains', 'ownerId', 'env', 'enableAutomaticUpdate', 'dataDir');
}
@@ -395,15 +402,8 @@ function removeRestrictedFields(app) {
}
function getIconUrlSync(app) {
const iconUrl = '/api/v1/apps/' + app.id + '/icon';
const userIconPath = `${paths.APP_ICONS_DIR}/${app.id}.user.png`;
if (safe.fs.existsSync(userIconPath)) return iconUrl;
const appstoreIconPath = `${paths.APP_ICONS_DIR}/${app.id}.png`;
if (safe.fs.existsSync(appstoreIconPath)) return iconUrl;
return null;
var iconPath = paths.APP_ICONS_DIR + '/' + app.id + '.png';
return fs.existsSync(iconPath) ? '/api/v1/apps/' + app.id + '/icon' : null;
}
function postProcess(app, domainObjectMap) {
@@ -578,6 +578,7 @@ function install(data, user, auditSource, callback) {
cert = data.cert || null,
key = data.key || null,
memoryLimit = data.memoryLimit || 0,
xFrameOptions = data.xFrameOptions || 'SAMEORIGIN',
sso = 'sso' in data ? data.sso : null,
debugMode = data.debugMode || null,
robotsTxt = data.robotsTxt || null,
@@ -612,6 +613,9 @@ function install(data, user, auditSource, callback) {
error = validateMemoryLimit(manifest, memoryLimit);
if (error) return callback(error);
error = validateXFrameOptions(xFrameOptions);
if (error) return callback(error);
error = validateDebugMode(debugMode);
if (error) return callback(error);
@@ -668,6 +672,7 @@ function install(data, user, auditSource, callback) {
var data = {
accessRestriction: accessRestriction,
memoryLimit: memoryLimit,
xFrameOptions: xFrameOptions,
sso: sso,
debugMode: debugMode,
mailboxName: mailboxName,
@@ -680,7 +685,7 @@ function install(data, user, auditSource, callback) {
};
appdb.add(appId, appStoreId, manifest, location, domain, ownerId, translatePortBindings(portBindings, manifest), data, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error, location, domainObject, portBindings, data.alternateDomains));
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error, location, domainObject, portBindings));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, error.message));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
@@ -749,6 +754,12 @@ function configure(appId, data, user, auditSource, callback) {
if (error) return callback(error);
}
if ('xFrameOptions' in data) {
values.xFrameOptions = data.xFrameOptions;
error = validateXFrameOptions(values.xFrameOptions);
if (error) return callback(error);
}
if ('debugMode' in data) {
values.debugMode = data.debugMode;
error = validateDebugMode(values.debugMode);
@@ -802,18 +813,6 @@ function configure(appId, data, user, auditSource, callback) {
values.tags = data.tags;
}
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 + '.user.png'), Buffer.from(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 + '.user.png'));
}
}
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));
@@ -837,10 +836,10 @@ function configure(appId, data, user, auditSource, callback) {
values.oldConfig = getAppConfig(app);
debug(`configure: id:${appId}`);
debug('Will configure app with id:%s values:%j', appId, values);
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_CONFIGURE, values, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error, location, domainObject, portBindings, data.alternateDomains));
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error, location, domainObject, portBindings));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
@@ -865,7 +864,7 @@ function update(appId, data, auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug(`update: id:${appId}`);
debug('Will update app with id:%s', appId);
get(appId, function (error, app) {
if (error) return callback(error);
@@ -902,11 +901,11 @@ function update(appId, data, auditSource, callback) {
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 + '.user.png'), Buffer.from(data.icon, 'base64'))) {
if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.png'), Buffer.from(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 + '.user.png'));
safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, appId + '.png'));
}
}
@@ -1120,6 +1119,7 @@ function clone(appId, data, user, auditSource, callback) {
installationState: appdb.ISTATE_PENDING_CLONE,
memoryLimit: app.memoryLimit,
accessRestriction: app.accessRestriction,
xFrameOptions: app.xFrameOptions,
restoreConfig: { backupId: backupId, backupFormat: backupInfo.format },
sso: !!app.sso,
mailboxName: mailboxName,
@@ -1129,7 +1129,7 @@ function clone(appId, data, user, auditSource, callback) {
};
appdb.add(newAppId, app.appStoreId, manifest, location, domain, ownerId, translatePortBindings(portBindings, manifest), data, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error, location, domainObject, portBindings, []));
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error, location, domainObject, portBindings));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
purchaseApp({ appId: newAppId, appstoreId: app.appStoreId, manifestId: manifest.id }, function (error) {
@@ -1218,8 +1218,6 @@ function stop(appId, callback) {
function checkManifestConstraints(manifest) {
assert(manifest && typeof manifest === 'object');
if (manifest.manifestVersion > 2) return new AppsError(AppsError.BAD_FIELD, 'Manifest version must be <= 2');
if (!manifest.dockerImage) return new AppsError(AppsError.BAD_FIELD, 'Missing dockerImage'); // dockerImage is optional in manifest
if (semver.valid(manifest.maxBoxVersion) && semver.gt(config.version(), manifest.maxBoxVersion)) {
@@ -1464,7 +1462,7 @@ function downloadFile(appId, filePath, callback) {
transform: function (chunk, ignoredEncoding, callback) {
this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk;
for (;;) {
while (true) {
if (this._buffer.length < 8) break; // header is 8 bytes
var type = this._buffer.readUInt8(0);
+3 -4
View File
@@ -387,15 +387,14 @@ function registerCloudron(data, callback) {
});
}
function registerWithLicense(license, domain, callback) {
function registerWithLicense(license, callback) {
assert.strictEqual(typeof license, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
getCloudronToken(function (error, token) {
if (token) return callback(new AppstoreError(AppstoreError.ALREADY_REGISTERED));
registerCloudron({ license, domain }, callback);
registerCloudron({ license }, callback);
});
}
@@ -447,7 +446,7 @@ function createTicket(info, callback) {
let url = config.apiServerOrigin() + '/api/v1/ticket';
info.supportEmail = custom.spec().support.email; // destination address for tickets
info.supportEmail = custom.supportEmail(); // destination address for tickets
superagent.post(url).query({ accessToken: token }).send(info).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
+5 -15
View File
@@ -420,15 +420,10 @@ function removeIcon(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (!safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, app.id + '.png'))) {
if (safe.error.code !== 'ENOENT') debugApp(app, 'cannot remove icon : %s', safe.error);
}
if (!safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, app.id + '.user.png'))) {
if (safe.error.code !== 'ENOENT') debugApp(app, 'cannot remove user icon : %s', safe.error);
}
callback(null);
fs.unlink(path.join(paths.APP_ICONS_DIR, app.id + '.png'), function (error) {
if (error && error.code !== 'ENOENT') debugApp(app, 'cannot remove icon : %s', error);
callback(null);
});
}
function cleanupLogs(app, callback) {
@@ -702,8 +697,6 @@ function update(app, callback) {
// FIXME: this does not handle option changes (like multipleDatabases)
var unusedAddons = _.omit(app.manifest.addons, Object.keys(app.updateConfig.manifest.addons));
const FORCED_UPDATE = (app.installationState === appdb.ISTATE_PENDING_FORCE_UPDATE);
async.series([
// this protects against the theoretical possibility of an app being marked for update from
// a previous version of box code
@@ -711,7 +704,7 @@ function update(app, callback) {
verifyManifest.bind(null, app.updateConfig.manifest),
function (next) {
if (FORCED_UPDATE) return next(null);
if (app.installationState === appdb.ISTATE_PENDING_FORCE_UPDATE) return next(null);
async.series([
updateApp.bind(null, app, { installationProgress: '15, Backing up app' }),
@@ -798,9 +791,6 @@ function update(app, callback) {
debugApp(app, 'Error updating app: %s', error);
updateApp(app, { installationState: appdb.ISTATE_ERROR, installationProgress: error.message, updateTime: new Date() }, callback.bind(null, error));
} else {
// do not spam the notifcation view
if (FORCED_UPDATE) return callback();
eventlog.add(eventlog.ACTION_APP_UPDATE_FINISH, auditsource.APP_TASK, { app: app, success: true }, callback);
}
});
+2 -1
View File
@@ -174,7 +174,8 @@ function getConfig(callback) {
memory: os.totalmem(),
provider: config.provider(),
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
uiSpec: custom.uiSpec()
features: custom.features(),
supportEmail: custom.supportEmail()
});
});
}
+2
View File
@@ -1,6 +1,7 @@
'use strict';
exports = module.exports = {
API_LOCATION: 'api', // this is unused but reserved for future use (#403)
SMTP_LOCATION: 'smtp',
IMAP_LOCATION: 'imap',
@@ -17,6 +18,7 @@ exports = module.exports = {
],
ADMIN_LOCATION: 'my',
DKIM_SELECTOR: 'cloudron',
NGINX_DEFAULT_CONFIG_FILE_NAME: 'default.conf',
+27 -50
View File
@@ -1,67 +1,44 @@
'use strict';
let config = require('./config.js'),
debug = require('debug')('box:features'),
lodash = require('lodash'),
let debug = require('debug')('box:features'),
paths = require('./paths.js'),
safe = require('safetydance'),
yaml = require('js-yaml');
exports = module.exports = {
uiSpec: uiSpec,
spec: spec
features: features,
supportEmail: supportEmail,
alertsEmail: alertsEmail,
sendAlertsToCloudronAdmins: sendAlertsToCloudronAdmins
};
const DEFAULT_SPEC = {
appstore: {
blacklist: [],
whitelist: null // null imples, not set. this is an object and not an array
},
backups: {
configurable: true
},
domains: {
dynamicDns: true,
changeDashboardDomain: true
},
subscription: {
configurable: true
},
support: {
email: 'support@cloudron.io',
remoteSupport: true,
ticketFormBody:
'Use this form to open support tickets. You can also write directly to [support@cloudron.io](mailto:support@cloudron.io).\n\n'
+ `* [Knowledge Base & App Docs](${config.webServerOrigin()}/documentation/apps/?support_view)\n`
+ `* [Custom App Packaging & API](${config.webServerOrigin()}/developer/packaging/?support_view)\n`
+ '* [Forum](https://forum.cloudron.io/)\n\n',
submitTickets: true
},
alerts: {
email: '',
notifyCloudronAdmins: false
},
footer: {
body: '&copy; 2019 [Cloudron](https://cloudron.io) [Forum <i class="fa fa-comments"></i>](https://forum.cloudron.io)'
}
};
const gSpec = (function () {
const gCustom = (function () {
try {
if (!safe.fs.existsSync(paths.CUSTOM_FILE)) return DEFAULT_SPEC;
const c = yaml.safeLoad(safe.fs.readFileSync(paths.CUSTOM_FILE, 'utf8'));
return lodash.merge({}, DEFAULT_SPEC, c);
if (!safe.fs.existsSync(paths.CUSTOM_FILE)) return {};
return yaml.safeLoad(safe.fs.readFileSync(paths.CUSTOM_FILE, 'utf8'));
} catch (e) {
debug(`Error loading features file from ${paths.CUSTOM_FILE} : ${e.message}`);
return DEFAULT_SPEC;
return {};
}
})();
// flags sent to the UI. this is separate because we have values that are secret to the backend
function uiSpec() {
return gSpec;
function features() {
return {
dynamicDns: safe.query(gCustom, 'features.dynamicDns', true),
remoteSupport: safe.query(gCustom, 'features.remoteSupport', true),
subscription: safe.query(gCustom, 'features.subscription', true),
configureBackup: safe.query(gCustom, 'features.configureBackup', true)
};
}
function spec() {
return gSpec;
}
function supportEmail() {
return safe.query(gCustom, 'support.email', 'support@cloudron.io');
}
function alertsEmail() {
return safe.query(gCustom, 'alerts.email', '');
}
function sendAlertsToCloudronAdmins() {
return safe.query(gCustom, 'alerts.notifyCloudronAdmins', true);
}
+41 -66
View File
@@ -15,14 +15,14 @@ var assert = require('assert'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
safe = require('safetydance'),
superagent = require('superagent'),
Namecheap = require('namecheap'),
sysinfo = require('../sysinfo.js'),
util = require('util'),
waitForDns = require('./waitfordns.js'),
xml2js = require('xml2js');
waitForDns = require('./waitfordns.js');
const ENDPOINT = 'https://api.namecheap.com/xml.response';
function formatError(response) {
return util.format('NameCheap DNS error [%s] %j', response.code, response.message);
}
function removePrivateFields(domainObject) {
domainObject.config.token = domains.SECRET_PLACEHOLDER;
@@ -33,19 +33,37 @@ function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
function getQuery(dnsConfig, callback) {
// Only send required fields - https://www.namecheap.com/support/api/methods/domains-dns/set-hosts.aspx
function mapHosts(hosts) {
return hosts.map(function (host) {
let tmp = {};
tmp.TTL = '300';
tmp.RecordType = host.RecordType || host.Type;
tmp.HostName = host.HostName || host.Name;
tmp.Address = host.Address;
if (tmp.RecordType === 'MX') {
tmp.EmailType = 'MX';
if (host.MXPref) tmp.MXPref = host.MXPref;
}
return tmp;
});
}
function getApi(dnsConfig, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
callback(null, {
ApiUser: dnsConfig.username,
ApiKey: dnsConfig.token,
UserName: dnsConfig.username,
ClientIp: ip
});
// Note that for all NameCheap calls to go through properly, the public IP returned by the getPublicIp method below must be whitelisted on NameCheap's API dashboard
let namecheap = new Namecheap(dnsConfig.username, dnsConfig.token, ip);
namecheap.setUsername(dnsConfig.username);
callback(null, namecheap);
});
}
@@ -56,31 +74,15 @@ function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
getQuery(dnsConfig, function (error, query) {
getApi(dnsConfig, function (error, namecheap) {
if (error) return callback(error);
query.Command = 'namecheap.domains.dns.getHosts';
query.SLD = zoneName.split('.')[0];
query.TLD = zoneName.split('.')[1];
namecheap.domains.dns.getHosts(zoneName, function (error, result) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(error)));
superagent.get(ENDPOINT).query(query).end(function (error, result) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error));
debug('entire getInternal response: %j', result);
var parser = new xml2js.Parser();
parser.parseString(result.text, function (error, result) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error));
var tmp = result.ApiResponse;
if (tmp['$'].Status !== 'OK') return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, safe.query(tmp, 'Errors[0].Error[0]._', 'Invalid response')));
if (!tmp.CommandResponse[0]) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Invalid response'));
if (!tmp.CommandResponse[0].DomainDNSGetHostsResult[0]) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Invalid response'));
var hosts = result.ApiResponse.CommandResponse[0].DomainDNSGetHostsResult[0].host.map(function (h) {
return h['$'];
});
callback(null, hosts);
});
return callback(null, result['DomainDNSGetHostsResult']['host']);
});
});
}
@@ -91,42 +93,15 @@ function setInternal(dnsConfig, zoneName, hosts, callback) {
assert(Array.isArray(hosts));
assert.strictEqual(typeof callback, 'function');
getQuery(dnsConfig, function (error, query) {
let mappedHosts = mapHosts(hosts);
getApi(dnsConfig, function (error, namecheap) {
if (error) return callback(error);
query.Command = 'namecheap.domains.dns.setHosts';
query.SLD = zoneName.split('.')[0];
query.TLD = zoneName.split('.')[1];
namecheap.domains.dns.setHosts(zoneName, mappedHosts, function (error, result) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(error)));
// Map to query params https://www.namecheap.com/support/api/methods/domains-dns/set-hosts.aspx
hosts.forEach(function (host, i) {
var n = i+1; // api starts with 1 not 0
query['TTL' + n] = '300'; // keep it low
query['HostName' + n] = host.HostName || host.Name;
query['RecordType' + n] = host.RecordType || host.Type;
query['Address' + n] = host.Address;
if (host.Type === 'MX') {
query['EmailType' + n] = 'MX';
if (host.MXPref) query['MXPref' + n] = host.MXPref;
}
});
superagent.post(ENDPOINT).query(query).end(function (error, result) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error));
var parser = new xml2js.Parser();
parser.parseString(result.text, function (error, result) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error));
var tmp = result.ApiResponse;
if (tmp['$'].Status !== 'OK') return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, safe.query(tmp, 'Errors[0].Error[0]._', 'Invalid response')));
if (!tmp.CommandResponse[0]) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Invalid response'));
if (!tmp.CommandResponse[0].DomainDNSSetHostsResult[0]) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Invalid response'));
if (tmp.CommandResponse[0].DomainDNSSetHostsResult[0]['$'].IsSuccess !== 'true') return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Invalid response'));
callback(null);
});
return callback(null, result);
});
});
}
+4 -4
View File
@@ -131,7 +131,7 @@ function getInternal(dnsConfig, zoneName, name, type, callback) {
result.body.records.forEach(function (r) {
// name.com api simply strips empty properties
r.host = r.host || '';
r.host = r.host || '@';
});
var results = result.body.records.filter(function (r) {
@@ -153,7 +153,7 @@ function upsert(domainObject, location, type, values, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '';
name = domains.getName(domainObject, location, type) || '@';
debug(`upsert: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
@@ -174,7 +174,7 @@ function get(domainObject, location, type, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '';
name = domains.getName(domainObject, location, type) || '@';
getInternal(dnsConfig, zoneName, name, type, function (error, result) {
if (error) return callback(error);
@@ -196,7 +196,7 @@ function del(domainObject, location, type, values, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '';
name = domains.getName(domainObject, location, type) || '@';
debug(`del: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
+9 -19
View File
@@ -181,17 +181,14 @@ function createSubcontainer(app, name, cmd, options, callback) {
var manifest = app.manifest;
var exposedPorts = {}, dockerPortBindings = { };
var domain = app.fqdn;
const envPrefix = manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
let stdEnv = [
// TODO: these should all have the CLOUDRON_ prefix
var stdEnv = [
'CLOUDRON=1',
'CLOUDRON_PROXY_IP=172.18.0.1',
`CLOUDRON_APP_HOSTNAME=${name}`,
`${envPrefix}WEBADMIN_ORIGIN=${config.adminOrigin()}`,
`${envPrefix}API_ORIGIN=${config.adminOrigin()}`,
`${envPrefix}APP_ORIGIN=https://${domain}`,
`${envPrefix}APP_DOMAIN=${domain}`
'WEBADMIN_ORIGIN=' + config.adminOrigin(),
'API_ORIGIN=' + config.adminOrigin(),
'APP_ORIGIN=https://' + domain,
'APP_DOMAIN=' + domain
];
// docker portBindings requires ports to be exposed
@@ -238,9 +235,9 @@ function createSubcontainer(app, name, cmd, options, callback) {
// Note that Hostname has no effect on DNS. We have to use the --net-alias for dns.
// Hostname cannot be set with container NetworkMode
var containerOptions = {
name: name, // for referencing containers
name: name, // used for filtering logs
Tty: isAppContainer,
Hostname: name,
Hostname: app.id, // set to something 'constant' so app containers can use this to communicate (across app updates)
Image: app.manifest.dockerImage,
Cmd: (isAppContainer && app.debugMode && app.debugMode.cmd) ? app.debugMode.cmd : cmd,
Env: stdEnv.concat(addonEnv).concat(portEnv).concat(appEnv),
@@ -276,17 +273,10 @@ function createSubcontainer(app, name, cmd, options, callback) {
},
CpuShares: 512, // relative to 1024 for system processes
VolumesFrom: isAppContainer ? null : [ app.containerId + ':rw' ],
NetworkMode: 'cloudron', // user defined bridge network
NetworkMode: 'cloudron',
Dns: ['172.18.0.1'], // use internal dns
DnsSearch: ['.'], // use internal dns
SecurityOpt: [ 'apparmor=docker-cloudron-app' ]
},
NetworkingConfig: {
EndpointsConfig: {
cloudron: {
Aliases: [ name ] // this allows sub-containers reach app containers by name
}
}
}
};
+1
View File
@@ -145,6 +145,7 @@ function validateHostname(location, domainObject) {
const hostname = fqdn(location, domainObject);
const RESERVED_LOCATIONS = [
constants.API_LOCATION,
constants.SMTP_LOCATION,
constants.IMAP_LOCATION
];
-1
View File
@@ -19,7 +19,6 @@ function startGraphite(existingInfra, callback) {
if (existingInfra.version === infra.version && infra.images.graphite.tag === existingInfra.images.graphite.tag) return callback();
const cmd = `docker run --restart=always -d --name="graphite" \
--hostname graphite \
--net cloudron \
--net-alias graphite \
--log-driver syslog \
+2 -2
View File
@@ -6,7 +6,7 @@
exports = module.exports = {
// a version change recreates all containers with latest docker config
'version': '48.15.0',
'version': '48.14.0',
'baseImages': [
{ repo: 'cloudron/base', tag: 'cloudron/base:1.0.0@sha256:147a648a068a2e746644746bbfb42eb7a50d682437cead3c67c933c546357617' }
@@ -19,7 +19,7 @@ exports = module.exports = {
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:2.0.2@sha256:6dcee0731dfb9b013ed94d56205eee219040ee806c7e251db3b3886eaa4947ff' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:2.0.2@sha256:95e006390ddce7db637e1672eb6f3c257d3c2652747424f529b1dee3cbe6728c' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.0.0@sha256:8a88dd334b62b578530a014ca1a2425a54cb9df1e475f5d3a36806e5cfa22121' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.3.1@sha256:9693e3ae42a12a7ac8cf5df94d828d46f5b22b4e2e1c7d1bc614d6ee2a22c365' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.2.0@sha256:20e4d2508dcf712eb56481067993ae39bf541d793d44f99f6a41d630ad941d9e' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.0.2@sha256:454f035d60b768153d4f31210380271b5ba1c09367c9d95c7fa37f9e39d2f59c' },
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:0.1.0@sha256:e177c5bf5f38c84ce1dea35649c22a1b05f96eec67a54a812c5a35e585670f0f' }
}
+14 -17
View File
@@ -520,25 +520,22 @@ function userSearchSftp(req, res, next) {
users.getByUsername(username, function (error, user) {
if (error) return next(new ldap.OperationsError(error.toString()));
apps.hasAccessTo(app, user, function (error, hasAccess) {
if (error) return next(new ldap.OperationsError(error.toString()));
if (!hasAccess) return next(new ldap.InsufficientAccessRightsError('Not authorized'));
if (!user.admin) return next(new ldap.InsufficientAccessRightsError('Not authorized'));
var obj = {
dn: ldap.parseDN(`cn=${username}@${appFqdn},ou=sftp,dc=cloudron`).toString(),
attributes: {
homeDirectory: path.join('/app/data', app.id, 'data'),
objectclass: ['user'],
objectcategory: 'person',
cn: user.id,
uid: `${username}@${appFqdn}`, // for bind after search
uidNumber: uidNumber, // unix uid for ftp access
gidNumber: uidNumber // unix gid for ftp access
}
};
var obj = {
dn: ldap.parseDN(`cn=${username}@${appFqdn},ou=sftp,dc=cloudron`).toString(),
attributes: {
homeDirectory: path.join('/app/data', app.id, 'data'),
objectclass: ['user'],
objectcategory: 'person',
cn: user.id,
uid: `${username}@${appFqdn}`, // for bind after search
uidNumber: uidNumber, // unix uid for ftp access
gidNumber: uidNumber // unix gid for ftp access
}
};
finalSend([ obj ], req, res, next);
});
finalSend([ obj ], req, res, next);
});
});
}
+24 -47
View File
@@ -210,14 +210,10 @@ function verifyRelay(relay, callback) {
});
}
function checkDkim(mailDomain, callback) {
assert.strictEqual(typeof mailDomain, 'object');
assert.strictEqual(typeof callback, 'function');
const domain = mailDomain.domain;
let dkim = {
domain: `${mailDomain.dkimSelector}._domainkey.${domain}`,
name: `${mailDomain.dkimSelector}._domainkey`,
function checkDkim(domain, callback) {
var dkim = {
domain: `${constants.DKIM_SELECTOR}._domainkey.${domain}`,
name: `${constants.DKIM_SELECTOR}._domainkey`,
type: 'TXT',
expected: null,
value: null,
@@ -293,25 +289,13 @@ function checkMx(domain, mailFqdn, callback) {
dns.resolve(mx.domain, mx.type, DNS_OPTIONS, function (error, mxRecords) {
if (error) return callback(error, mx);
if (mxRecords.length === 0) return callback(null, mx);
mx.status = mxRecords.length == 1 && mxRecords[0].exchange === mailFqdn;
mx.value = mxRecords.map(function (r) { return r.priority + ' ' + r.exchange + '.'; }).join(' ');
if (mxRecords.length !== 0) {
mx.status = mxRecords.length == 1 && mxRecords[0].exchange === mailFqdn;
mx.value = mxRecords.map(function (r) { return r.priority + ' ' + r.exchange + '.'; }).join(' ');
}
if (mx.status) return callback(null, mx); // MX record is "my."
// cloudflare might create a conflict subdomain (https://support.cloudflare.com/hc/en-us/articles/360020296512-DNS-Troubleshooting-FAQ)
dns.resolve(mxRecords[0].exchange, 'A', DNS_OPTIONS, function (error, mxIps) {
if (error || mxIps.length !== 1) return callback(null, mx);
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(null, mx);
mx.status = mxIps[0] === ip;
callback(null, mx);
});
});
callback(null, mx);
});
}
@@ -499,28 +483,28 @@ function getStatus(domain, callback) {
const mailFqdn = config.mailFqdn();
getDomain(domain, function (error, mailDomain) {
getDomain(domain, function (error, result) {
if (error) return callback(error);
let checks = [];
if (mailDomain.enabled) {
if (result.enabled) {
checks.push(
recordResult('dns.mx', checkMx.bind(null, domain, mailFqdn)),
recordResult('dns.dmarc', checkDmarc.bind(null, domain))
);
}
if (mailDomain.relay.provider === 'cloudron-smtp') {
if (result.relay.provider === 'cloudron-smtp') {
// these tests currently only make sense when using Cloudron's SMTP server at this point
checks.push(
recordResult('dns.spf', checkSpf.bind(null, domain, mailFqdn)),
recordResult('dns.dkim', checkDkim.bind(null, mailDomain)),
recordResult('dns.dkim', checkDkim.bind(null, domain)),
recordResult('dns.ptr', checkPtr.bind(null, mailFqdn)),
recordResult('relay', checkOutboundPort25),
recordResult('rbl', checkRblStatus.bind(null, domain))
);
} else if (mailDomain.relay.provider !== 'noop') {
checks.push(recordResult('relay', checkSmtpRelay.bind(null, mailDomain.relay)));
} else if (result.relay.provider !== 'noop') {
checks.push(recordResult('relay', checkSmtpRelay.bind(null, result.relay)));
}
async.parallel(checks, function () {
@@ -612,14 +596,13 @@ function createMailConfig(mailFqdn, callback) {
const enableRelay = relay.provider !== 'cloudron-smtp' && relay.provider !== 'noop',
host = relay.host || '',
port = relay.port || 25,
authType = relay.username ? 'plain' : '',
username = relay.username || '',
password = relay.password || '';
if (!enableRelay) return;
if (!safe.fs.appendFileSync(paths.ADDON_CONFIG_DIR + '/mail/smtp_forward.ini',
`[${domain.domain}]\nenable_outbound=true\nhost=${host}\nport=${port}\nenable_tls=true\nauth_type=${authType}\nauth_user=${username}\nauth_pass=${password}\n\n`, 'utf8')) {
`[${domain.domain}]\nenable_outbound=true\nhost=${host}\nport=${port}\nenable_tls=true\nauth_type=plain\nauth_user=${username}\nauth_pass=${password}\n\n`, 'utf8')) {
return callback(new Error('Could not create mail var file:' + safe.error.message));
}
});
@@ -774,10 +757,9 @@ function txtRecordsWithSpf(domain, mailFqdn, callback) {
});
}
function ensureDkimKeySync(mailDomain) {
assert.strictEqual(typeof mailDomain, 'object');
function ensureDkimKeySync(domain) {
assert.strictEqual(typeof domain, 'string');
const domain = mailDomain.domain;
const dkimPath = path.join(paths.MAIL_DATA_DIR, `dkim/${domain}`);
const dkimPrivateKeyFile = path.join(dkimPath, 'private');
const dkimPublicKeyFile = path.join(dkimPath, 'public');
@@ -800,10 +782,7 @@ function ensureDkimKeySync(mailDomain) {
if (!safe.child_process.execSync('openssl genrsa -out ' + dkimPrivateKeyFile + ' 1024')) return new MailError(MailError.INTERNAL_ERROR, safe.error);
if (!safe.child_process.execSync('openssl rsa -in ' + dkimPrivateKeyFile + ' -out ' + dkimPublicKeyFile + ' -pubout -outform PEM')) return new MailError(MailError.INTERNAL_ERROR, safe.error);
if (!safe.fs.writeFileSync(dkimSelectorFile, mailDomain.dkimSelector, 'utf8')) return new MailError(MailError.INTERNAL_ERROR, safe.error);
// if the 'yellowtent' user of OS and the 'cloudron' user of mail container don't match, the keys become inaccessible by mail code
if (!safe.fs.chmodSync(dkimPrivateKeyFile, 0o644)) return new MailError(MailError.INTERNAL_ERROR, safe.error);
if (!safe.fs.writeFileSync(dkimSelectorFile, constants.DKIM_SELECTOR, 'utf8')) return new MailError(MailError.INTERNAL_ERROR, safe.error);
return null;
}
@@ -834,11 +813,11 @@ function upsertDnsRecords(domain, mailFqdn, callback) {
debug(`upsertDnsRecords: updating mail dns records of domain ${domain} and mail fqdn ${mailFqdn}`);
maildb.get(domain, function (error, mailDomain) {
maildb.get(domain, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
error = ensureDkimKeySync(mailDomain);
error = ensureDkimKeySync(domain);
if (error) return callback(error);
if (process.env.BOX_ENV === 'test') return callback();
@@ -847,11 +826,11 @@ function upsertDnsRecords(domain, mailFqdn, callback) {
if (!dkimKey) return callback(new MailError(MailError.INTERNAL_ERROR, new Error('Failed to read dkim public key')));
// t=s limits the domainkey to this domain and not it's subdomains
var dkimRecord = { subdomain: `${mailDomain.dkimSelector}._domainkey`, domain: domain, type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] };
var dkimRecord = { subdomain: `${constants.DKIM_SELECTOR}._domainkey`, domain: domain, type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] };
var records = [ ];
records.push(dkimRecord);
if (mailDomain.enabled) {
if (result.enabled) {
records.push({ subdomain: '_dmarc', domain: domain, type: 'TXT', values: [ '"v=DMARC1; p=reject; pct=100"' ] });
records.push({ subdomain: '', domain: domain, type: 'MX', values: [ '10 ' + mailFqdn + '.' ] });
}
@@ -909,9 +888,7 @@ function addDomain(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
const dkimSelector = domain === config.adminDomain() ? 'cloudron' : ('cloudron-' + config.adminDomain().replace(/\./g, ''));
maildb.add(domain, { dkimSelector }, function (error) {
maildb.add(domain, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new MailError(MailError.ALREADY_EXISTS, 'Domain already exists'));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'No such domain'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
-27
View File
@@ -4,37 +4,10 @@ Dear Cloudron Admin,
The application '<%= title %>' installed at <%= appFqdn %> was updated to app package version <%= version %>.
Changes:
<%= changelog %>
Powered by https://cloudron.io
Sent at: <%= new Date().toUTCString() %>
<% } else { %>
<center>
<img src="<%= cloudronAvatarUrl %>" width="128px" height="128px"/>
<h3>Dear <%= cloudronName %> Admin,</h3>
<br/>
<div style="width: 650px; text-align: left;">
The application '<%= title %>' installed at <%= appFqdn %> was updated to app package version <%= version %>.
<h5>Changelog:</h5>
<%- changelogHTML %>
</div>
<br/>
<br/>
<div style="font-size: 10px; color: #333333; background: #ffffff;">
Powered by <a href="https://cloudron.io">Cloudron</a>.<br/>
Sent at: <%= new Date().toUTCString() %>
</div>
</center>
<% } %>
+8 -9
View File
@@ -3,7 +3,7 @@
Dear Cloudron Admin,
<% for (var i = 0; i < apps.length; i++) { -%>
A new version <%= apps[i].updateInfo.manifest.version %> of the app '<%= apps[i].app.manifest.title %>' installed at <%= apps[i].app.fqdn %> is available.
A new version <%= apps[i].updateInfo.manifest.version %> of the app '<%= apps[i].app.manifest.title %>' installed at <%= apps[i].app.fqdn %> is available!
Changes:
<%= apps[i].updateInfo.manifest.changelog %>
@@ -28,12 +28,10 @@ Sent at: <%= new Date().toUTCString() %>
<h3>Dear <%= cloudronName %> Admin,</h3>
<br/>
<div style="width: 650px; text-align: left;">
<% for (var i = 0; i < apps.length; i++) { -%>
<p>
A new version <%= apps[i].updateInfo.manifest.version %> of the app '<%= apps[i].app.manifest.title %>' installed at <a href="https://<%= apps[i].app.fqdn %>"><%= apps[i].app.fqdn %></a> is available.
A new version <%= apps[i].updateInfo.manifest.version %> of the app '<%= apps[i].app.manifest.title %>' installed at <a href="https://<%= apps[i].app.fqdn %>"><%= apps[i].app.fqdn %></a> is available!
</p>
<h5>Changelog:</h5>
@@ -42,20 +40,21 @@ Sent at: <%= new Date().toUTCString() %>
<br/>
<% } -%>
<% if (!hasSubscription) { -%>
<% if (!hasSubscription) { %>
<p>Keep your Cloudron automatically up-to-date and secure by upgrading to a <a href="<%= webadminUrl %>/#/settings">paid plan</a>.</p>
<% } else { -%>
<% } else { -%>
<p>
<br/>
<center><a href="<%= webadminUrl %>">Update now</a></center>
<br/>
</p>
<% } -%>
<% } -%>
<br/>
</div>
<div style="font-size: 10px; color: #333333; background: #ffffff;">
Powered by <a href="https://cloudron.io">Cloudron</a>.<br/>
Sent at: <%= new Date().toUTCString() %>
Powered by <a href="https://cloudron.io">Cloudron</a>.
</div>
</center>
+52 -42
View File
@@ -1,48 +1,58 @@
{
"app_updated.ejs": {
"format": "html",
"title": "WordPress",
"appFqdn": "updated.smartserver.io",
"version": "1.3.4",
"changelog": "* This has changed\n * and that as well",
"changelogHTML": "<ul><li>This has changed</li><li>and that as well</li></ul>",
"cloudronName": "Smartserver",
"cloudronAvatarUrl": "https://cloudron.io/img/logo.png"
},
"app_updates_available.ejs": {
"format": "html",
"webadminUrl": "https://my.cloudron.io",
"cloudronName": "Smartserver",
"cloudronAvatarUrl": "https://cloudron.io/img/logo.png",
"hasSubscription": true,
"apps": [{
"updateInfo": {
"manifest": {
"version": "1.4.3"
}
},
"app": {
"fqdn": "site.smartserver.io",
"manifest": {
"title": "WordPress"
}
},
"changelog": "* This has changed\n * and that as well",
"changelogHTML": "<ul><li>This has changed</li><li>and that as well</li></ul>"
}, {
"updateInfo": {
"manifest": {
"version": "0.1.3"
}
},
"app": {
"fqdn": "another.smartserver.io",
"manifest": {
"title": "RocketChat"
}
},
"changelog": "* This has changed\n * and that as well",
"changelogHTML": "<ul><li>This has changed</li><li>and that as well</li></ul>"
}]
}
"info": {
"pendingBoxUpdate": {
"version": "1.3.7",
"changelog": [
"Feature one",
"Feature two"
]
},
"pendingAppUpdates": [{
"manifest": {
"title": "Wordpress",
"version": "1.2.3",
"changelog": "* This has changed\n * and that as well"
}
}],
"finishedBoxUpdates": [{
"boxUpdateInfo": {
"version": "1.0.1",
"changelog": [
"Feature one",
"Feature two"
]
}
}, {
"boxUpdateInfo": {
"version": "1.0.2",
"changelog": [
"Feature one",
"Feature two",
"Feature three"
]
}
}],
"finishedAppUpdates": [{
"toManifest": {
"title": "Rocket.Chat",
"version": "0.2.1",
"changelog": "* This has changed\n * and that as well\n * some more"
}
}, {
"toManifest": {
"title": "Redmine",
"version": "1.2.1",
"changelog": "* This has changed\n * and that as well\n * some more"
}
}],
"certRenewals": [],
"finishedBackups": [],
"usersAdded": [],
"usersRemoved": [],
"hasSubscription": false
}
}
+3 -5
View File
@@ -19,7 +19,7 @@ var assert = require('assert'),
DatabaseError = require('./databaseerror.js'),
safe = require('safetydance');
var MAILDB_FIELDS = [ 'domain', 'enabled', 'mailFromValidation', 'catchAllJson', 'relayJson', 'dkimSelector' ].join(',');
var MAILDB_FIELDS = [ 'domain', 'enabled', 'mailFromValidation', 'catchAllJson', 'relayJson' ].join(',');
function postProcess(data) {
data.enabled = !!data.enabled; // int to boolean
@@ -34,12 +34,10 @@ function postProcess(data) {
return data;
}
function add(domain, data, callback) {
function add(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO mail (domain, dkimSelector) VALUES (?, ?)', [ domain, data.dkimSelector || 'cloudron' ], function (error) {
database.query('INSERT INTO mail (domain) VALUES (?)', [ domain ], function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, 'mail domain already exists'));
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND), 'no such domain');
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
+3 -21
View File
@@ -289,7 +289,7 @@ function appDied(mailTo, app) {
from: mailConfig.notificationFrom,
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, supportEmail: custom.spec().support.email, format: 'text' })
text: render('app_down.ejs', { title: app.manifest.title, appFqdn: app.fqdn, supportEmail: custom.supportEmail(), format: 'text' })
};
sendMail(mailOptions);
@@ -306,29 +306,11 @@ function appUpdated(mailTo, app, callback) {
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
var converter = new showdown.Converter();
var templateData = {
title: app.manifest.title,
appFqdn: app.fqdn,
version: app.manifest.version,
changelog: app.manifest.changelog,
changelogHTML: converter.makeHtml(app.manifest.changelog),
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateDataText = JSON.parse(JSON.stringify(templateData));
templateDataText.format = 'text';
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
templateDataHTML.format = 'html';
var mailOptions = {
from: mailConfig.notificationFrom,
to: mailTo,
subject: `[${mailConfig.cloudronName}] App ${app.fqdn} was updated`,
text: render('app_updated.ejs', templateDataText),
html: render('app_updated.ejs', templateDataHTML)
subject: util.format('[%s] App %s was updated', mailConfig.cloudronName, app.fqdn),
text: render('app_updated.ejs', { title: app.manifest.title, appFqdn: app.fqdn, version: app.manifest.version, format: 'text' })
};
sendMail(mailOptions, callback);
+10 -10
View File
@@ -193,8 +193,8 @@ function oomEvent(eventId, app, addon, containerId, event, callback) {
message = 'The container has been restarted automatically. Consider increasing the [memory limit](https://docs.docker.com/v17.09/edge/engine/reference/commandline/update/#update-a-containers-kernel-memory-constraints)';
}
if (custom.spec().alerts.email) mailer.oomEvent(custom.spec().alerts.email, program, event);
if (!custom.spec().alerts.notifyCloudronAdmins) return callback();
if (custom.alertsEmail()) mailer.oomEvent(custom.alertsEmail(), program, event);
if (!custom.sendAlertsToCloudronAdmins()) return callback();
actionForAllAdmins([], function (admin, done) {
mailer.oomEvent(admin.email, program, event);
@@ -208,8 +208,8 @@ function appUp(eventId, app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (custom.spec().alerts.email) mailer.appUp(custom.spec().alerts.email, app);
if (!custom.spec().alerts.notifyCloudronAdmins) return callback();
if (custom.alertsEmail()) mailer.appUp(custom.alertsEmail(), app);
if (!custom.sendAlertsToCloudronAdmins()) return callback();
actionForAllAdmins([], function (admin, done) {
mailer.appUp(admin.email, app);
@@ -222,8 +222,8 @@ function appDied(eventId, app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (custom.spec().alerts.email) mailer.appDied(custom.spec().alerts.email, app);
if (!custom.spec().alerts.notifyCloudronAdmins) return callback();
if (custom.alertsEmail()) mailer.appDied(custom.alertsEmail(), app);
if (!custom.sendAlertsToCloudronAdmins()) return callback();
actionForAllAdmins([], function (admin, callback) {
mailer.appDied(admin.email, app);
@@ -254,8 +254,8 @@ function certificateRenewalError(eventId, vhost, errorMessage, callback) {
assert.strictEqual(typeof errorMessage, 'string');
assert.strictEqual(typeof callback, 'function');
if (custom.spec().alerts.email) mailer.certificateRenewalError(custom.spec().alerts.email, vhost, errorMessage);
if (!custom.spec().alerts.notifyCloudronAdmins) return callback();
if (custom.alertsEmail()) mailer.certificateRenewalError(custom.alertsEmail(), vhost, errorMessage);
if (!custom.sendAlertsToCloudronAdmins()) return callback();
actionForAllAdmins([], function (admin, callback) {
mailer.certificateRenewalError(admin.email, vhost, errorMessage);
@@ -269,8 +269,8 @@ function backupFailed(eventId, taskId, errorMessage, callback) {
assert.strictEqual(typeof errorMessage, 'string');
assert.strictEqual(typeof callback, 'function');
if (custom.spec().alerts.email) mailer.backupFailed(custom.spec().alerts.email, errorMessage, `${config.adminOrigin()}/logs.html?taskId=${taskId}`);
if (!custom.spec().alerts.notifyCloudronAdmins) return callback();
if (custom.alertsEmail()) mailer.backupFailed(custom.alertsEmail(), errorMessage, `${config.adminOrigin()}/logs.html?taskId=${taskId}`);
if (!custom.sendAlertsToCloudronAdmins()) return callback();
actionForAllAdmins([], function (admin, callback) {
mailer.backupFailed(admin.email, errorMessage, `${config.adminOrigin()}/logs.html?taskId=${taskId}`);
+2 -5
View File
@@ -119,13 +119,10 @@ function pruneInfraImages(callback) {
if (!line) continue;
let parts = line.split(' '); // [ ID, Repo:Tag@Digest ]
if (image.tag === parts[1]) continue; // keep
debug(`pruneInfraImages: removing unused image of ${image.repo}: tag: ${parts[1]} id: ${parts[0]}`);
debug(`pruneInfraImages: removing unused image of ${image.repo}: ${line}`);
let result = safe.child_process.execSync(`docker rmi ${parts[0]}`, { encoding: 'utf8' });
if (result === null) debug(`Erroring removing image ${parts[0]}: ${safe.error.mesage}`);
shell.exec('pruneInfraImages', `docker rmi ${parts[0]}`, iteratorCallback);
}
iteratorCallback();
}, callback);
}
+32 -10
View File
@@ -78,7 +78,6 @@ ProvisionError.BAD_STATE = 'Bad State';
ProvisionError.ALREADY_SETUP = 'Already Setup';
ProvisionError.INTERNAL_ERROR = 'Internal Error';
ProvisionError.EXTERNAL_ERROR = 'External Error';
ProvisionError.LICENSE_ERROR = 'License Error';
ProvisionError.ALREADY_PROVISIONED = 'Already Provisioned';
function setProgress(task, message, callback) {
@@ -87,8 +86,29 @@ function setProgress(task, message, callback) {
callback();
}
function autoRegister(domain, callback) {
assert.strictEqual(typeof domain, 'string');
function autoprovision(autoconf, callback) {
assert.strictEqual(typeof autoconf, 'object');
assert.strictEqual(typeof callback, 'function');
async.eachSeries(Object.keys(autoconf), function (key, iteratorDone) {
debug(`autoprovision: ${key}`);
switch (key) {
case 'backupConfig':
settings.setBackupConfig(autoconf[key], iteratorDone);
break;
default:
debug(`autoprovision: ${key} ignored`);
return iteratorDone();
}
}, function (error) {
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
callback(null);
});
}
function autoRegister(callback) {
assert.strictEqual(typeof callback, 'function');
if (!fs.existsSync(paths.LICENSE_FILE)) return callback();
@@ -98,7 +118,7 @@ function autoRegister(domain, callback) {
debug('Auto-registering cloudron');
appstore.registerWithLicense(license.trim(), domain, function (error) {
appstore.registerWithLicense(license.trim(), function (error) {
if (error && error.reason !== AppstoreError.ALREADY_REGISTERED) {
debug('Failed to auto-register cloudron', error);
return callback(new ProvisionError(ProvisionError.LICENSE_ERROR, 'Failed to auto-register Cloudron with license. Please contact support@cloudron.io'));
@@ -124,9 +144,9 @@ function unprovision(callback) {
}
function setup(dnsConfig, backupConfig, auditSource, callback) {
function setup(dnsConfig, autoconf, auditSource, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof autoconf, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -168,12 +188,12 @@ function setup(dnsConfig, backupConfig, auditSource, callback) {
callback(); // now that args are validated run the task in the background
async.series([
autoRegister.bind(null, domain),
autoRegister,
domains.prepareDashboardDomain.bind(null, domain, auditSource, (progress) => setProgress('setup', progress.message, NOOP_CALLBACK)),
cloudron.setDashboardDomain.bind(null, domain, auditSource), // this sets up the config.fqdn()
mail.addDomain.bind(null, domain), // this relies on config.mailFqdn() and config.adminDomain()
mail.addDomain.bind(null, domain), // this relies on config.mailFqdn()
setProgress.bind(null, 'setup', 'Applying auto-configuration'),
(next) => { if (!backupConfig) return next(); settings.setBackupConfig(backupConfig, next); },
autoprovision.bind(null, autoconf),
setProgress.bind(null, 'setup', 'Done'),
eventlog.add.bind(null, eventlog.ACTION_PROVISION, auditSource, { })
], function (error) {
@@ -244,10 +264,11 @@ function activate(username, password, email, displayName, ip, auditSource, callb
});
}
function restore(backupConfig, backupId, version, auditSource, callback) {
function restore(backupConfig, backupId, version, autoconf, auditSource, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof version, 'string');
assert.strictEqual(typeof autoconf, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -281,6 +302,7 @@ function restore(backupConfig, backupId, version, auditSource, callback) {
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
+5 -2
View File
@@ -392,6 +392,7 @@ function writeAdminNginxConfig(bundle, configFileName, vhost, callback) {
endpoint: 'admin',
certFilePath: bundle.certFilePath,
keyFilePath: bundle.keyFilePath,
xFrameOptions: 'SAMEORIGIN',
robotsTxtQuoted: JSON.stringify('User-agent: *\nDisallow: /\n')
};
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
@@ -456,7 +457,8 @@ function writeAppNginxConfig(app, bundle, callback) {
endpoint: endpoint,
certFilePath: bundle.certFilePath,
keyFilePath: bundle.keyFilePath,
robotsTxtQuoted: app.robotsTxt ? JSON.stringify(app.robotsTxt) : null
robotsTxtQuoted: app.robotsTxt ? JSON.stringify(app.robotsTxt) : null,
xFrameOptions: app.xFrameOptions || 'SAMEORIGIN' // once all apps have been updated/
};
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
@@ -485,7 +487,8 @@ function writeAppRedirectNginxConfig(app, fqdn, bundle, callback) {
endpoint: 'redirect',
certFilePath: bundle.certFilePath,
keyFilePath: bundle.keyFilePath,
robotsTxtQuoted: null
robotsTxtQuoted: null,
xFrameOptions: 'SAMEORIGIN'
};
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
+11 -10
View File
@@ -31,7 +31,9 @@ var apps = require('../apps.js'),
AppsError = apps.AppsError,
assert = require('assert'),
auditSource = require('../auditsource.js'),
config = require('../config.js'),
debug = require('debug')('box:routes/apps'),
fs = require('fs'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
paths = require('../paths.js'),
@@ -65,15 +67,11 @@ function getApps(req, res, next) {
function getAppIcon(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
if (!req.query.original) {
const userIconPath = `${paths.APP_ICONS_DIR}/${req.params.id}.user.png`;
if (safe.fs.existsSync(userIconPath)) return res.sendFile(userIconPath);
}
const appstoreIconPath = `${paths.APP_ICONS_DIR}/${req.params.id}.png`;
if (safe.fs.existsSync(appstoreIconPath)) return res.sendFile(appstoreIconPath);
return next(new HttpError(404, 'No such icon'));
var iconPath = paths.APP_ICONS_DIR + '/' + req.params.id + '.png';
fs.exists(iconPath, function (exists) {
if (!exists) return next(new HttpError(404, 'No such icon'));
res.sendFile(iconPath);
});
}
function installApp(req, res, next) {
@@ -109,6 +107,8 @@ function installApp(req, res, next) {
if ('memoryLimit' in data && typeof data.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
if (data.xFrameOptions && typeof data.xFrameOptions !== 'string') return next(new HttpError(400, 'xFrameOptions must be a string'));
if ('sso' in data && typeof data.sso !== 'boolean') return next(new HttpError(400, 'sso must be a boolean'));
if ('enableBackup' in data && typeof data.enableBackup !== 'boolean') return next(new HttpError(400, 'enableBackup must be a boolean'));
if ('enableAutomaticUpdate' in data && typeof data.enableAutomaticUpdate !== 'boolean') return next(new HttpError(400, 'enableAutomaticUpdate must be a boolean'));
@@ -166,6 +166,7 @@ function configureApp(req, res, next) {
if (!data.cert && data.key) return next(new HttpError(400, 'cert must be provided'));
if ('memoryLimit' in data && typeof data.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
if (data.xFrameOptions && typeof data.xFrameOptions !== 'string') return next(new HttpError(400, 'xFrameOptions must be a string'));
if ('enableBackup' in data && typeof data.enableBackup !== 'boolean') return next(new HttpError(400, 'enableBackup must be a boolean'));
if ('enableAutomaticUpdate' in data && typeof data.enableAutomaticUpdate !== 'boolean') return next(new HttpError(400, 'enableAutomaticUpdate must be a boolean'));
@@ -188,7 +189,6 @@ function configureApp(req, res, next) {
if ('label' in data && typeof data.label !== 'string') return next(new HttpError(400, 'label must be a string'));
if ('dataDir' in data && typeof data.dataDir !== 'string') return next(new HttpError(400, 'dataDir must be a string'));
if ('icon' in data && typeof data.icon !== 'string') return next(new HttpError(400, 'icon is not a string'));
debug('Configuring app id:%s data:%j', req.params.id, data);
@@ -326,6 +326,7 @@ function updateApp(req, res, next) {
if ('appStoreId' in data && typeof data.appStoreId !== 'string') return next(new HttpError(400, 'appStoreId must be a string'));
if (!data.manifest && !data.appStoreId) return next(new HttpError(400, 'appStoreId or manifest is required'));
if ('icon' in data && typeof data.icon !== 'string') return next(new HttpError(400, 'icon is not a string'));
if ('force' in data && typeof data.force !== 'boolean') return next(new HttpError(400, 'force must be a boolean'));
debug('Update app id:%s to manifest:%j', req.params.id, data.manifest);
+1 -18
View File
@@ -12,19 +12,9 @@ exports = module.exports = {
var appstore = require('../appstore.js'),
AppstoreError = appstore.AppstoreError,
assert = require('assert'),
custom = require('../custom.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
function isAppAllowed(appstoreId) {
if (custom.spec().appstore.blacklist.includes(appstoreId)) return false;
if (!custom.spec().appstore.whitelist) return true;
if (!custom.spec().appstore.whitelist[appstoreId]) return false;
return true;
}
function getApps(req, res, next) {
appstore.getApps(function (error, apps) {
if (error && error.reason === AppstoreError.INVALID_TOKEN) return next(new HttpError(402, error.message));
@@ -32,18 +22,13 @@ function getApps(req, res, next) {
if (error && error.reason === AppstoreError.NOT_REGISTERED) return next(new HttpError(412, error.message));
if (error) return next(new HttpError(500, error));
let filteredApps = apps.filter((app) => !custom.spec().appstore.blacklist.includes(app.id));
if (custom.spec().appstore.whitelist) filteredApps = filteredApps.filter((app) => app.id in custom.spec().appstore.whitelist);
next(new HttpSuccess(200, { apps: filteredApps }));
next(new HttpSuccess(200, { apps: apps }));
});
}
function getApp(req, res, next) {
assert.strictEqual(typeof req.params.appstoreId, 'string');
if (!isAppAllowed(req.params.appstoreId)) return next(new HttpError(405, 'feature disabled by admin'));
appstore.getApp(req.params.appstoreId, function (error, app) {
if (error && error.reason === AppstoreError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error && error.reason === AppstoreError.INVALID_TOKEN) return next(new HttpError(402, error.message));
@@ -59,8 +44,6 @@ function getAppVersion(req, res, next) {
assert.strictEqual(typeof req.params.appstoreId, 'string');
assert.strictEqual(typeof req.params.versionId, 'string');
if (!isAppAllowed(req.params.appstoreId)) return next(new HttpError(405, 'feature disabled by admin'));
appstore.getAppVersion(req.params.appstoreId, req.params.versionId, function (error, manifest) {
if (error && error.reason === AppstoreError.NOT_FOUND) return next(new HttpError(404, 'No such app or version'));
if (error && error.reason === AppstoreError.INVALID_TOKEN) return next(new HttpError(402, error.message));
-5
View File
@@ -20,7 +20,6 @@ let assert = require('assert'),
auditSource = require('../auditsource.js'),
cloudron = require('../cloudron.js'),
CloudronError = cloudron.CloudronError,
custom = require('../custom.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
updater = require('../updater.js'),
@@ -153,8 +152,6 @@ function getLogStream(req, res, next) {
function setDashboardAndMailDomain(req, res, next) {
if (!req.body.domain || typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
if (!custom.spec().domains.changeDashboardDomain) return next(new HttpError(405, 'feature disabled by admin'));
cloudron.setDashboardAndMailDomain(req.body.domain, auditSource.fromRequest(req), function (error) {
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
@@ -166,8 +163,6 @@ function setDashboardAndMailDomain(req, res, next) {
function prepareDashboardDomain(req, res, next) {
if (!req.body.domain || typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
if (!custom.spec().domains.changeDashboardDomain) return next(new HttpError(405, 'feature disabled by admin'));
cloudron.prepareDashboardDomain(req.body.domain, auditSource.fromRequest(req), function (error, taskId) {
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(404, error.message));
if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message));
+9 -5
View File
@@ -53,18 +53,19 @@ function setup(req, res, next) {
if ('tlsConfig' in dnsConfig && typeof dnsConfig.tlsConfig !== 'object') return next(new HttpError(400, 'tlsConfig must be an object'));
if (dnsConfig.tlsConfig && (!dnsConfig.tlsConfig.provider || typeof dnsConfig.tlsConfig.provider !== 'string')) return next(new HttpError(400, 'tlsConfig.provider must be a string'));
if ('backupConfig' in req.body && typeof req.body.backupConfig !== 'object') return next(new HttpError(400, 'backupConfig must be an object'));
// TODO: validate subfields of these objects
if (req.body.autoconf && typeof req.body.autoconf !== 'object') return next(new HttpError(400, 'autoconf must be an object'));
// it can take sometime to setup DNS, register cloudron
req.clearTimeout();
provision.setup(dnsConfig, req.body.backupConfig || null, auditSource.fromRequest(req), function (error) {
provision.setup(dnsConfig, req.body.autoconf || {}, auditSource.fromRequest(req), function (error) {
if (error && error.reason === ProvisionError.ALREADY_SETUP) return next(new HttpError(409, error.message));
if (error && error.reason === ProvisionError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === ProvisionError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, {}));
next(new HttpSuccess(200));
});
}
@@ -107,14 +108,17 @@ function restore(req, res, next) {
if (typeof req.body.backupId !== 'string') return next(new HttpError(400, 'backupId must be a string or null'));
if (typeof req.body.version !== 'string') return next(new HttpError(400, 'version must be a string'));
provision.restore(backupConfig, req.body.backupId, req.body.version, auditSource.fromRequest(req), function (error) {
// TODO: validate subfields of these objects
if (req.body.autoconf && typeof req.body.autoconf !== 'object') return next(new HttpError(400, 'autoconf must be an object'));
provision.restore(backupConfig, req.body.backupId, req.body.version, req.body.autoconf || {}, auditSource.fromRequest(req), function (error) {
if (error && error.reason === ProvisionError.ALREADY_SETUP) return next(new HttpError(409, error.message));
if (error && error.reason === ProvisionError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === ProvisionError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === ProvisionError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, {}));
next(new HttpSuccess(200));
});
}
-2
View File
@@ -17,8 +17,6 @@ var addons = require('../addons.js'),
HttpSuccess = require('connect-lastmile').HttpSuccess;
function getAll(req, res, next) {
req.clearTimeout(); // can take a while to get status of all services
addons.getServices(function (error, result) {
if (error) return next(new HttpError(500, error));
+4 -4
View File
@@ -131,8 +131,8 @@ function getBackupConfig(req, res, next) {
settings.getBackupConfig(function (error, config) {
if (error) return next(new HttpError(500, error));
// always send provider as it is used by the UI to figure if backups are disabled ('noop' backend)
if (!custom.spec().backups.configurable) {
// used by the UI to figure if backups are disabled
if (!custom.features().configureBackup) {
return next(new HttpSuccess(200, { provider: config.provider }));
}
@@ -143,7 +143,7 @@ function getBackupConfig(req, res, next) {
function setBackupConfig(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!custom.spec().backups.configurable) return next(new HttpError(405, 'feature disabled by admin'));
if (!custom.features().configureBackup) return next(new HttpError(405, 'feature disabled by admin'));
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required'));
if (typeof req.body.retentionSecs !== 'number') return next(new HttpError(400, 'retentionSecs is required'));
@@ -207,7 +207,7 @@ function getDynamicDnsConfig(req, res, next) {
function setDynamicDnsConfig(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!custom.spec().domains.dynamicDns) return next(new HttpError(405, 'feature disabled by admin'));
if (!custom.features().dynamicDns) return next(new HttpError(405, 'feature disabled by admin'));
if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled boolean is required'));
+3 -5
View File
@@ -18,8 +18,6 @@ var appstore = require('../appstore.js'),
function createTicket(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
if (!custom.spec().support.submitTickets) return next(new HttpError(405, 'feature disabled by admin'));
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'));
@@ -29,16 +27,16 @@ function createTicket(req, res, next) {
if (req.body.appId && typeof req.body.appId !== 'string') return next(new HttpError(400, 'appId must be string'));
appstore.createTicket(_.extend({ }, req.body, { email: req.user.email, displayName: req.user.displayName }), function (error) {
if (error) return next(new HttpError(503, `Error contacting cloudron.io: ${error.message}. Please email ${custom.spec().support.email}`));
if (error) return next(new HttpError(503, `Error contacting cloudron.io: ${error.message}. Please email ${custom.supportEmail()}`));
next(new HttpSuccess(201, { message: `An email for sent to ${custom.spec().support.email}. We will get back shortly!` }));
next(new HttpSuccess(201, {}));
});
}
function enableRemoteSupport(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!custom.spec().support.remoteSupport) return next(new HttpError(405, 'feature disabled by admin'));
if (!custom.features().remoteSupport) return next(new HttpError(405, 'feature disabled by admin'));
if (typeof req.body.enable !== 'boolean') return next(new HttpError(400, 'enabled is required'));
+28 -3
View File
@@ -56,6 +56,8 @@ const DOMAIN_0 = {
tlsConfig: { provider: 'fallback' }
};
const CLOUDRON_ID = 'somecloudronid';
var APP_STORE_ID = 'test', APP_ID;
var APP_LOCATION = 'appslocation';
var APP_LOCATION_2 = 'appslocationtwo';
@@ -381,13 +383,13 @@ describe('App API', function () {
});
});
it('app install fails - reserved smtp location', function (done) {
it('app install fails - reserved api location', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, location: constants.SMTP_LOCATION, accessRestriction: null, domain: DOMAIN_0.domain })
.send({ manifest: APP_MANIFEST, location: constants.API_LOCATION, accessRestriction: null, domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.contain(constants.SMTP_LOCATION + ' is reserved');
expect(res.body.message).to.contain(constants.API_LOCATION + ' is reserved');
done();
});
});
@@ -537,6 +539,7 @@ describe('App API', function () {
it('cannot uninstall invalid app', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/whatever/uninstall')
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
@@ -544,8 +547,28 @@ describe('App API', function () {
});
});
it('cannot uninstall app without password', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('cannot uninstall app with wrong password', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
.send({ password: PASSWORD+PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
});
});
it('non admin cannot uninstall app', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
.send({ password: PASSWORD })
.query({ access_token: token_1 })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
@@ -558,6 +581,7 @@ describe('App API', function () {
var fake2 = nock(config.apiServerOrigin()).delete(function (uri) { return uri.indexOf('/api/v1/cloudronapps/') >= 0; }).reply(204, { });
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
@@ -1124,6 +1148,7 @@ describe('App installation', function () {
}
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
+24
View File
@@ -253,6 +253,7 @@ describe('Domains API', function () {
it('cannot delete locked domain', function (done) {
superagent.delete(SERVER_URL + '/api/v1/domains/' + DOMAIN_0.domain)
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(423);
done();
@@ -261,9 +262,31 @@ describe('Domains API', function () {
});
describe('delete', function () {
it('fails without password', function (done) {
superagent.delete(SERVER_URL + '/api/v1/domains/' + DOMAIN_0.domain)
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with wrong password', function (done) {
superagent.delete(SERVER_URL + '/api/v1/domains/' + DOMAIN_0.domain)
.query({ access_token: token })
.send({ password: PASSWORD + PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(403);
done();
});
});
it('fails for non-existing domain', function (done) {
superagent.delete(SERVER_URL + '/api/v1/domains/' + DOMAIN_0.domain + DOMAIN_0.domain)
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(404);
@@ -274,6 +297,7 @@ describe('Domains API', function () {
it('succeeds', function (done) {
superagent.delete(SERVER_URL + '/api/v1/domains/' + DOMAIN_0.domain)
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
+2
View File
@@ -250,6 +250,7 @@ describe('Groups API', function () {
it('can remove empty group', function (done) {
superagent.del(SERVER_URL + '/api/v1/groups/' + group1Object.id)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
@@ -259,6 +260,7 @@ describe('Groups API', function () {
it('can remove non-empty group', function (done) {
superagent.del(SERVER_URL + '/api/v1/groups/' + groupObject.id)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
+30 -1
View File
@@ -190,9 +190,29 @@ describe('Mail API', function () {
});
});
it('cannot delete domain without password', function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/doesnotexist.com')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('cannot delete domain with wrong password', function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/doesnotexist.com')
.send({ password: PASSWORD+PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
});
});
it('cannot delete non-existing domain', function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/doesnotexist.com')
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
@@ -201,6 +221,7 @@ describe('Mail API', function () {
it('cannot delete admin mail domain', function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + ADMIN_DOMAIN.domain)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
@@ -210,6 +231,7 @@ describe('Mail API', function () {
it('can delete admin mail domain', function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
@@ -239,7 +261,7 @@ describe('Mail API', function () {
callback(null, dnsAnswerQueue[hostname][type]);
};
dkimDomain = 'cloudron-admincom._domainkey.' + DOMAIN_0.domain;
dkimDomain = 'cloudron._domainkey.' + DOMAIN_0.domain;
spfDomain = DOMAIN_0.domain;
mxDomain = DOMAIN_0.domain;
dmarcDomain = '_dmarc.' + DOMAIN_0.domain;
@@ -267,6 +289,7 @@ describe('Mail API', function () {
dns.resolve = resolve;
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
@@ -516,6 +539,7 @@ describe('Mail API', function () {
after(function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
@@ -567,6 +591,7 @@ describe('Mail API', function () {
after(function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
@@ -637,6 +662,7 @@ describe('Mail API', function () {
after(function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
@@ -714,6 +740,7 @@ describe('Mail API', function () {
after(function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
@@ -819,6 +846,7 @@ describe('Mail API', function () {
if (error) return done(error);
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
@@ -944,6 +972,7 @@ describe('Mail API', function () {
if (error) return done(error);
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
+1 -1
View File
@@ -248,7 +248,7 @@ describe('Profile API', function () {
.query({ access_token: token_0 })
.send({ password: 'some wrong password', newPassword: 'MOre#$%34' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.statusCode).to.equal(403);
done();
});
});
+33
View File
@@ -529,6 +529,7 @@ describe('Users API', function () {
it('remove random user fails', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/randomid')
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
@@ -538,15 +539,46 @@ describe('Users API', function () {
it('user removes himself is not allowed', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/' + user_0.id)
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
done();
});
});
it('admin cannot remove normal user without giving a password', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/' + user_1.id)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('admin cannot remove normal user with empty password', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/' + user_1.id)
.query({ access_token: token })
.send({ password: '' })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
});
});
it('admin cannot remove normal user with giving wrong password', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/' + user_1.id)
.query({ access_token: token })
.send({ password: PASSWORD + PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
});
});
it('admin removes normal user', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/' + user_1.id)
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
@@ -556,6 +588,7 @@ describe('Users API', function () {
it('admin removes himself should not be allowed', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/' + user_0.id)
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
done();
+1 -1
View File
@@ -132,7 +132,7 @@ function verifyPassword(req, res, next) {
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'API call requires user password'));
users.verifyWithUsername(req.user.username, req.body.password, function (error) {
if (error && error.reason === UsersError.WRONG_PASSWORD) return next(new HttpError(400, 'Password incorrect'));
if (error && error.reason === UsersError.WRONG_PASSWORD) return next(new HttpError(403, 'Password incorrect')); // not 401 intentionally since the UI redirects for 401
if (error && error.reason === UsersError.NOT_FOUND) return next(new HttpError(404, 'No such user'));
if (error) return next(new HttpError(500, error));
+10 -8
View File
@@ -18,6 +18,7 @@ var accesscontrol = require('./accesscontrol.js'),
middleware = require('./middleware'),
passport = require('passport'),
path = require('path'),
provision = require('./provision.js'),
routes = require('./routes/index.js'),
ws = require('ws');
@@ -167,7 +168,7 @@ function initializeExpressSync() {
router.get ('/api/v1/users', usersReadScope, routes.users.list);
router.post('/api/v1/users', usersManageScope, routes.users.create);
router.get ('/api/v1/users/:userId', usersManageScope, routes.users.get); // this is manage scope because it returns non-restricted fields
router.del ('/api/v1/users/:userId', usersManageScope, routes.users.remove);
router.del ('/api/v1/users/:userId', usersManageScope, routes.users.verifyPassword, routes.users.remove);
router.post('/api/v1/users/:userId', usersManageScope, routes.users.update);
router.post('/api/v1/users/:userId/password', usersManageScope, routes.users.changePassword);
router.put ('/api/v1/users/:userId/groups', usersManageScope, routes.users.setGroups);
@@ -181,7 +182,7 @@ function initializeExpressSync() {
router.get ('/api/v1/groups/:groupId', usersManageScope, routes.groups.get);
router.put ('/api/v1/groups/:groupId/members', usersManageScope, routes.groups.updateMembers);
router.post('/api/v1/groups/:groupId', usersManageScope, routes.groups.update);
router.del ('/api/v1/groups/:groupId', usersManageScope, routes.groups.remove);
router.del ('/api/v1/groups/:groupId', usersManageScope, routes.users.verifyPassword, routes.groups.remove);
// form based login routes used by oauth2 frame
router.get ('/api/v1/session/login', csrf, routes.oauth2.loginForm);
@@ -221,13 +222,13 @@ function initializeExpressSync() {
// app routes
router.get ('/api/v1/apps', appsReadScope, routes.apps.getApps);
router.get ('/api/v1/apps/:id', appsManageScope, routes.apps.getApp);
router.get ('/api/v1/apps/:id/icon', appsReadScope, routes.apps.getAppIcon);
router.get ('/api/v1/apps/:id/icon', routes.apps.getAppIcon);
router.post('/api/v1/apps/install', appsManageScope, routes.apps.installApp);
router.post('/api/v1/apps/:id/uninstall', appsManageScope, routes.apps.uninstallApp);
router.post('/api/v1/apps/:id/uninstall', appsManageScope, routes.users.verifyPassword, routes.apps.uninstallApp);
router.post('/api/v1/apps/:id/configure', appsManageScope, routes.apps.configureApp);
router.post('/api/v1/apps/:id/update', appsManageScope, routes.apps.updateApp);
router.post('/api/v1/apps/:id/restore', appsManageScope, routes.apps.restoreApp);
router.post('/api/v1/apps/:id/restore', appsManageScope, routes.users.verifyPassword, routes.apps.restoreApp);
router.post('/api/v1/apps/:id/backup', appsManageScope, routes.apps.backupApp);
router.get ('/api/v1/apps/:id/backups', appsManageScope, routes.apps.listBackups);
router.post('/api/v1/apps/:id/stop', appsManageScope, routes.apps.stopApp);
@@ -251,8 +252,8 @@ function initializeExpressSync() {
// email routes
router.get ('/api/v1/mail/:domain', mailScope, routes.mail.getDomain);
router.post('/api/v1/mail', mailScope, routes.mail.addDomain);
router.get ('/api/v1/mail/:domain/stats', mailScope, routes.mail.getDomainStats);
router.del ('/api/v1/mail/:domain', mailScope, routes.mail.removeDomain);
router.get ('/api/v1/mail/:domain/stats', mailScope, routes.users.verifyPassword, routes.mail.getDomainStats);
router.del ('/api/v1/mail/:domain', mailScope, routes.users.verifyPassword, routes.mail.removeDomain);
router.get ('/api/v1/mail/:domain/status', mailScope, routes.mail.getStatus);
router.post('/api/v1/mail/:domain/mail_from_validation', mailScope, routes.mail.setMailFromValidation);
router.post('/api/v1/mail/:domain/catch_all', mailScope, routes.mail.setCatchAllAddress);
@@ -284,7 +285,7 @@ function initializeExpressSync() {
router.get ('/api/v1/domains', domainsReadScope, routes.domains.getAll);
router.get ('/api/v1/domains/:domain', domainsManageScope, verifyDomainLock, routes.domains.get); // this is manage scope because it returns non-restricted fields
router.put ('/api/v1/domains/:domain', domainsManageScope, verifyDomainLock, routes.domains.update);
router.del ('/api/v1/domains/:domain', domainsManageScope, verifyDomainLock, routes.domains.del);
router.del ('/api/v1/domains/:domain', domainsManageScope, verifyDomainLock, routes.users.verifyPassword, routes.domains.del);
// addon routes
router.get ('/api/v1/services', cloudronScope, routes.services.getAll);
@@ -369,6 +370,7 @@ function start(callback) {
async.series([
routes.accesscontrol.initialize, // hooks up authentication strategies into passport
database.initialize,
provision.autoRegister,
cloudron.initialize,
gHttpServer.listen.bind(gHttpServer, config.get('port'), '127.0.0.1'),
gSysadminHttpServer.listen.bind(gSysadminHttpServer, config.get('sysadminPort'), '127.0.0.1'),
-1
View File
@@ -19,7 +19,6 @@ function startSftp(existingInfra, callback) {
if (existingInfra.version === infra.version && infra.images.graphite.tag === existingInfra.images.graphite.tag) return callback();
const cmd = `docker run --restart=always -d --name="sftp" \
--hostname sftp \
--net cloudron \
--net-alias sftp \
--log-driver syslog \
-1
View File
@@ -14,7 +14,6 @@ let assert = require('assert'),
path = require('path'),
util = require('util');
// the logic here is also used in the cloudron-support tool
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');
+7 -5
View File
@@ -412,6 +412,7 @@ describe('database', function () {
oldConfig: null,
newConfig: null,
memoryLimit: 4294967296,
xFrameOptions: 'DENY',
sso: true,
debugMode: null,
robotsTxt: null,
@@ -992,6 +993,7 @@ describe('database', function () {
oldConfig: null,
updateConfig: null,
memoryLimit: 4294967296,
xFrameOptions: 'DENY',
sso: true,
debugMode: null,
robotsTxt: null,
@@ -1026,6 +1028,7 @@ describe('database', function () {
oldConfig: null,
updateConfig: null,
memoryLimit: 0,
xFrameOptions: 'SAMEORIGIN',
sso: true,
debugMode: null,
robotsTxt: null,
@@ -2044,7 +2047,7 @@ describe('database', function () {
before(function (done) {
async.series([
domaindb.add.bind(null, DOMAIN_0.domain, { zoneName: DOMAIN_0.zoneName, provider: DOMAIN_0.provider, config: DOMAIN_0.config, tlsConfig: DOMAIN_0.tlsConfig }),
maildb.add.bind(null, DOMAIN_0.domain, {})
maildb.add.bind(null, DOMAIN_0.domain)
], done);
});
@@ -2206,8 +2209,7 @@ describe('database', function () {
enabled: false,
relay: { provider: 'cloudron-smtp' },
catchAll: [ ],
mailFromValidation: true,
dkimSelector: 'cloudron'
mailFromValidation: true
};
before(function (done) {
@@ -2219,7 +2221,7 @@ describe('database', function () {
});
it('cannot add non-existing domain', function (done) {
maildb.add(MAIL_DOMAIN_0.domain + 'nope', {}, function (error) {
maildb.add(MAIL_DOMAIN_0.domain + 'nope', function (error) {
expect(error).to.be.ok();
expect(error.reason).to.be(DatabaseError.NOT_FOUND);
@@ -2228,7 +2230,7 @@ describe('database', function () {
});
it('can add domain', function (done) {
maildb.add(MAIL_DOMAIN_0.domain, {}, function (error) {
maildb.add(MAIL_DOMAIN_0.domain, function (error) {
expect(error).to.equal(null);
done();
+520 -280
View File
@@ -2,19 +2,20 @@
/* global it:false */
/* global describe:false */
/* global before:false */
/* global beforeEach:false */
/* global after:false */
'use strict';
var async = require('async'),
AWS = require('aws-sdk'),
GCDNS = require('@google-cloud/dns').DNS,
GCDNS = require('@google-cloud/dns'),
config = require('../config.js'),
database = require('../database.js'),
domains = require('../domains.js'),
expect = require('expect.js'),
namecheap = require('namecheap'),
nock = require('nock'),
sinon = require('sinon'),
util = require('util');
var DOMAIN_0 = {
@@ -595,282 +596,465 @@ describe('dns provider', function () {
});
});
describe('namecheap', function () {
const NAMECHEAP_ENDPOINT = 'https://api.namecheap.com';
const username = 'namecheapuser';
const token = 'namecheaptoken';
xdescribe('namecheap', function () {
let sandbox = require('sinon').createSandbox();
// the success answer is always the same
const SET_HOSTS_RETURN = `<?xml version="1.0" encoding="utf-8"?>
<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
<Errors />
<Warnings />
<RequestedCommand>namecheap.domains.dns.sethosts</RequestedCommand>
<CommandResponse Type="namecheap.domains.dns.setHosts">
<DomainDNSSetHostsResult Domain="cloudron.space" IsSuccess="true">
<Warnings />
</DomainDNSSetHostsResult>
</CommandResponse>
<Server>PHX01APIEXT03</Server>
<GMTTimeDifference>--4:00</GMTTimeDifference>
<ExecutionTime>0.408</ExecutionTime>
</ApiResponse>`;
let username = 'namecheapuser';
let apiKey = 'API_KEY';
before(function (done) {
DOMAIN_0.provider = 'namecheap';
DOMAIN_0.config = {
username: username,
token: token
username,
apiKey
};
domains.update(DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE, done);
});
beforeEach(function () {
nock.cleanAll();
after(function() {
sandbox.restore();
});
it('upsert non-existing record succeeds', function (done) {
const GET_HOSTS_RETURN = `<?xml version="1.0" encoding="utf-8"?>
<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
<Errors />
<Warnings />
<RequestedCommand>namecheap.domains.dns.gethosts</RequestedCommand>
<CommandResponse Type="namecheap.domains.dns.getHosts">
<DomainDNSGetHostsResult Domain="cloudron.space" EmailType="MX" IsUsingOurDNS="true">
<host HostId="160859434" Name="@" Type="MX" Address="my.nebulon.space." MXPref="10" TTL="300" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
<host HostId="173400612" Name="@" Type="TXT" Address="v=spf1 a:my.nebulon.space ~all" MXPref="10" TTL="300" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
</DomainDNSGetHostsResult>
</CommandResponse>
<Server>PHX01APIEXT04</Server>
<GMTTimeDifference>--4:00</GMTTimeDifference>
<ExecutionTime>0.16</ExecutionTime>
</ApiResponse>`;
var req1 = nock(NAMECHEAP_ENDPOINT).get('/xml.response')
.query({
ApiUser: username,
ApiKey: token,
UserName: username,
ClientIp: '127.0.0.1',
Command: 'namecheap.domains.dns.getHosts',
SLD: DOMAIN_0.zoneName.split('.')[0],
TLD: DOMAIN_0.zoneName.split('.')[1]
})
.reply(200, GET_HOSTS_RETURN);
let getHostsReturn = {
'Type': 'namecheap.domains.dns.getHosts',
'DomainDNSGetHostsResult': {
'Domain': 'example-dns-test.com',
'EmailType': 'FWD',
'IsUsingOurDNS': 'true',
'host': [
{
'HostId': '614433',
'Name': 'www',
'Type': 'CNAME',
'Address': 'parkingpage.namecheap.com.',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': '',
'FriendlyName': 'CNAME Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
'HostId': '614432',
'Name': '@',
'Type': 'URL',
'Address': 'http://www.example-dns-test.com/',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': 'URL Forwarding',
'FriendlyName': 'URL Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
}
]
}
};
var req2 = nock(NAMECHEAP_ENDPOINT).post('/xml.response')
.query({
ApiUser: username,
ApiKey: token,
UserName: username,
ClientIp: '127.0.0.1',
Command: 'namecheap.domains.dns.setHosts',
SLD: DOMAIN_0.zoneName.split('.')[0],
TLD: DOMAIN_0.zoneName.split('.')[1],
let setInternalExpect = [
{
'HostId': '614433',
'HostName': 'www',
'RecordType': 'CNAME',
'Address': 'parkingpage.namecheap.com.',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': '',
'FriendlyName': 'CNAME Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
'HostId': '614432',
'HostName': '@',
'RecordType': 'URL',
'Address': 'http://www.example-dns-test.com/',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': 'URL Forwarding',
'FriendlyName': 'URL Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
RecordType: 'A',
HostName: 'test',
Address: '1.2.3.4'
}
];
TTL1: '300',
HostName1: '@',
RecordType1: 'MX',
Address1: 'my.nebulon.space.',
EmailType1: 'MX',
MXPref1: '10',
let getHostsFake = sinon.fake.yields(null, getHostsReturn);
let setHostsFake = sinon.fake.yields(null, true);
let mockObj = {
dns: {
getHosts: getHostsFake,
setHosts: setHostsFake
}
};
TTL2: '300',
HostName2: '@',
RecordType2: 'TXT',
Address2: 'v=spf1 a:my.nebulon.space ~all',
TTL3: '300',
HostName3: 'test',
RecordType3: 'A',
Address3: '1.2.3.4',
})
.reply(200, SET_HOSTS_RETURN);
sandbox.stub(namecheap.prototype, 'domains').value(mockObj);
domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) {
expect(error).to.eql(null);
expect(req1.isDone()).to.be.ok();
expect(req2.isDone()).to.be.ok();
expect(setHostsFake.calledOnce).to.eql(true);
expect(setHostsFake.calledWith(DOMAIN_0.domain, setInternalExpect)).to.eql(true);
done();
});
});
it('upsert multiple non-existing records succeeds', function (done) {
const GET_HOSTS_RETURN = `<?xml version="1.0" encoding="utf-8"?>
<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
<Errors />
<Warnings />
<RequestedCommand>namecheap.domains.dns.gethosts</RequestedCommand>
<CommandResponse Type="namecheap.domains.dns.getHosts">
<DomainDNSGetHostsResult Domain="cloudron.space" EmailType="MX" IsUsingOurDNS="true">
<host HostId="160859434" Name="@" Type="MX" Address="my.nebulon.space." MXPref="10" TTL="300" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
<host HostId="173400612" Name="@" Type="TXT" Address="v=spf1 a:my.nebulon.space ~all" MXPref="10" TTL="300" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
</DomainDNSGetHostsResult>
</CommandResponse>
<Server>PHX01APIEXT04</Server>
<GMTTimeDifference>--4:00</GMTTimeDifference>
<ExecutionTime>0.16</ExecutionTime>
</ApiResponse>`;
var req1 = nock(NAMECHEAP_ENDPOINT).get('/xml.response')
.query({
ApiUser: username,
ApiKey: token,
UserName: username,
ClientIp: '127.0.0.1',
Command: 'namecheap.domains.dns.getHosts',
SLD: DOMAIN_0.zoneName.split('.')[0],
TLD: DOMAIN_0.zoneName.split('.')[1]
})
.reply(200, GET_HOSTS_RETURN);
let getHostsReturn = {
'Type': 'namecheap.domains.dns.getHosts',
'DomainDNSGetHostsResult': {
'Domain': 'example-dns-test.com',
'EmailType': 'FWD',
'IsUsingOurDNS': 'true',
'host': [
{
'HostId': '614433',
'Name': 'www',
'Type': 'CNAME',
'Address': 'parkingpage.namecheap.com.',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': '',
'FriendlyName': 'CNAME Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
'HostId': '614432',
'Name': '@',
'Type': 'URL',
'Address': 'http://www.example-dns-test.com/',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': 'URL Forwarding',
'FriendlyName': 'URL Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
}
]
}
};
var req2 = nock(NAMECHEAP_ENDPOINT).post('/xml.response')
.query({
ApiUser: username,
ApiKey: token,
UserName: username,
ClientIp: '127.0.0.1',
Command: 'namecheap.domains.dns.setHosts',
SLD: DOMAIN_0.zoneName.split('.')[0],
TLD: DOMAIN_0.zoneName.split('.')[1],
let setInternalExpect = [
{
'HostId': '614433',
'HostName': 'www',
'RecordType': 'CNAME',
'Address': 'parkingpage.namecheap.com.',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': '',
'FriendlyName': 'CNAME Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
'HostId': '614432',
'HostName': '@',
'RecordType': 'URL',
'Address': 'http://www.example-dns-test.com/',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': 'URL Forwarding',
'FriendlyName': 'URL Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
RecordType: 'TXT',
HostName: 'test',
Address: '1.2.3.4'
},
{
RecordType: 'TXT',
HostName: 'test',
Address: '2.3.4.5'
},
{
RecordType: 'TXT',
HostName: 'test',
Address: '3.4.5.6'
}
];
TTL1: '300',
HostName1: '@',
RecordType1: 'MX',
Address1: 'my.nebulon.space.',
EmailType1: 'MX',
MXPref1: '10',
let getHostsFake = sinon.fake.yields(null, getHostsReturn);
let setHostsFake = sinon.fake.yields(null, true);
let mockObj = {
dns: {
getHosts: getHostsFake,
setHosts: setHostsFake
}
};
TTL2: '300',
HostName2: '@',
RecordType2: 'TXT',
Address2: 'v=spf1 a:my.nebulon.space ~all',
TTL3: '300',
HostName3: 'test',
RecordType3: 'TXT',
Address3: '1.2.3.4',
TTL4: '300',
HostName4: 'test',
RecordType4: 'TXT',
Address4: '2.3.4.5',
TTL5: '300',
HostName5: 'test',
RecordType5: 'TXT',
Address5: '3.4.5.6',
})
.reply(200, SET_HOSTS_RETURN);
sandbox.stub(namecheap.prototype, 'domains').value(mockObj);
domains.upsertDnsRecords('test', DOMAIN_0.domain, 'TXT', ['1.2.3.4', '2.3.4.5', '3.4.5.6'], function (error) {
expect(error).to.eql(null);
expect(req1.isDone()).to.be.ok();
expect(req2.isDone()).to.be.ok();
expect(setHostsFake.calledOnce).to.eql(true);
expect(setHostsFake.calledWith(DOMAIN_0.domain, setInternalExpect)).to.eql(true);
done();
});
});
it('upsert multiple non-existing MX records succeeds', function (done) {
let getHostsReturn = {
'Type': 'namecheap.domains.dns.getHosts',
'DomainDNSGetHostsResult': {
'Domain': 'example-dns-test.com',
'EmailType': 'FWD',
'IsUsingOurDNS': 'true',
'host': [
{
'HostId': '614433',
'Name': 'www',
'Type': 'CNAME',
'Address': 'parkingpage.namecheap.com.',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': '',
'FriendlyName': 'CNAME Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
'HostId': '614432',
'Name': '@',
'Type': 'URL',
'Address': 'http://www.example-dns-test.com/',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': 'URL Forwarding',
'FriendlyName': 'URL Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
}
]
}
};
let setInternalExpect = [
{
'HostId': '614433',
'HostName': 'www',
'RecordType': 'CNAME',
'Address': 'parkingpage.namecheap.com.',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': '',
'FriendlyName': 'CNAME Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
'HostId': '614432',
'HostName': '@',
'RecordType': 'URL',
'Address': 'http://www.example-dns-test.com/',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': 'URL Forwarding',
'FriendlyName': 'URL Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
RecordType: 'MX',
HostName: 'test',
Address: '1.2.3.4',
MXPref: '10'
},
{
RecordType: 'MX',
HostName: 'test',
Address: '2.3.4.5',
MXPref: '20'
},
{
RecordType: 'MX',
HostName: 'test',
Address: '3.4.5.6',
MXPref: '30'
}
];
let getHostsFake = sinon.fake.yields(null, getHostsReturn);
let setHostsFake = sinon.fake.yields(null, true);
let mockObj = {
dns: {
getHosts: getHostsFake,
setHosts: setHostsFake
}
};
sandbox.stub(namecheap.prototype, 'domains').value(mockObj);
domains.upsertDnsRecords('test', DOMAIN_0.domain, 'MX', ['10 1.2.3.4', '20 2.3.4.5', '30 3.4.5.6'], function (error) {
expect(error).to.eql(null);
expect(setHostsFake.calledOnce).to.eql(true);
expect(setHostsFake.calledWith(DOMAIN_0.domain, setInternalExpect)).to.eql(true);
done();
});
});
it('upsert existing record succeeds', function (done) {
const GET_HOSTS_RETURN = `<?xml version="1.0" encoding="utf-8"?>
<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
<Errors />
<Warnings />
<RequestedCommand>namecheap.domains.dns.gethosts</RequestedCommand>
<CommandResponse Type="namecheap.domains.dns.getHosts">
<DomainDNSGetHostsResult Domain="cloudron.space" EmailType="MX" IsUsingOurDNS="true">
<host HostId="160859434" Name="@" Type="MX" Address="my.nebulon.space." MXPref="10" TTL="300" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
<host HostId="173400612" Name="www" Type="CNAME" Address="1.2.3.4" MXPref="10" TTL="300" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
</DomainDNSGetHostsResult>
</CommandResponse>
<Server>PHX01APIEXT04</Server>
<GMTTimeDifference>--4:00</GMTTimeDifference>
<ExecutionTime>0.16</ExecutionTime>
</ApiResponse>`;
var req1 = nock(NAMECHEAP_ENDPOINT).get('/xml.response')
.query({
ApiUser: username,
ApiKey: token,
UserName: username,
ClientIp: '127.0.0.1',
Command: 'namecheap.domains.dns.getHosts',
SLD: DOMAIN_0.zoneName.split('.')[0],
TLD: DOMAIN_0.zoneName.split('.')[1]
})
.reply(200, GET_HOSTS_RETURN);
let getHostsReturn = {
'Type': 'namecheap.domains.dns.getHosts',
'DomainDNSGetHostsResult': {
'Domain': 'example-dns-test.com',
'EmailType': 'FWD',
'IsUsingOurDNS': 'true',
'host': [
{
'HostId': '614433',
'Name': 'www',
'Type': 'CNAME',
'Address': DOMAIN_0.domain,
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': '',
'FriendlyName': 'CNAME Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
'HostId': '614432',
'Name': '@',
'Type': 'URL',
'Address': 'http://www.example-dns-test.com/',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': 'URL Forwarding',
'FriendlyName': 'URL Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
}
]
}
};
var req2 = nock(NAMECHEAP_ENDPOINT).post('/xml.response')
.query({
ApiUser: username,
ApiKey: token,
UserName: username,
ClientIp: '127.0.0.1',
Command: 'namecheap.domains.dns.setHosts',
SLD: DOMAIN_0.zoneName.split('.')[0],
TLD: DOMAIN_0.zoneName.split('.')[1],
let setInternalExpect = [
{
'HostId': '614433',
'HostName': 'www',
'RecordType': 'CNAME',
'Address': '1.2.3.4',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': '',
'FriendlyName': 'CNAME Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
'HostId': '614432',
'HostName': '@',
'RecordType': 'URL',
'Address': 'http://www.example-dns-test.com/',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': 'URL Forwarding',
'FriendlyName': 'URL Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
}
];
TTL1: '300',
HostName1: '@',
RecordType1: 'MX',
Address1: 'my.nebulon.space.',
EmailType1: 'MX',
MXPref1: '10',
let getHostsFake = sinon.fake.yields(null, getHostsReturn);
let setHostsFake = sinon.fake.yields(null, true);
let mockObj = {
dns: {
getHosts: getHostsFake,
setHosts: setHostsFake
}
};
TTL2: '300',
HostName2: 'www',
RecordType2: 'CNAME',
Address2: '1.2.3.4'
})
.reply(200, SET_HOSTS_RETURN);
sandbox.stub(namecheap.prototype, 'domains').value(mockObj);
domains.upsertDnsRecords('www', DOMAIN_0.domain, 'CNAME', ['1.2.3.4'], function (error) {
expect(error).to.eql(null);
expect(req1.isDone()).to.be.ok();
expect(req2.isDone()).to.be.ok();
expect(setHostsFake.calledOnce).to.eql(true);
expect(setHostsFake.calledWith(DOMAIN_0.domain, setInternalExpect)).to.eql(true);
done();
});
});
it('get succeeds', function(done) {
const GET_HOSTS_RETURN = `<?xml version="1.0" encoding="utf-8"?>
<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
<Errors />
<Warnings />
<RequestedCommand>namecheap.domains.dns.gethosts</RequestedCommand>
<CommandResponse Type="namecheap.domains.dns.getHosts">
<DomainDNSGetHostsResult Domain="cloudron.space" EmailType="MX" IsUsingOurDNS="true">
<host HostId="160859434" Name="@" Type="MX" Address="my.nebulon.space." MXPref="10" TTL="300" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
<host HostId="173400612" Name="test" Type="A" Address="1.2.3.4" MXPref="10" TTL="300" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
<host HostId="173400613" Name="test" Type="A" Address="2.3.4.5" MXPref="10" TTL="300" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
</DomainDNSGetHostsResult>
</CommandResponse>
<Server>PHX01APIEXT04</Server>
<GMTTimeDifference>--4:00</GMTTimeDifference>
<ExecutionTime>0.16</ExecutionTime>
</ApiResponse>`;
let getHostsReturn = {
'Type': 'namecheap.domains.dns.getHosts',
'DomainDNSGetHostsResult': {
'Domain': 'example-dns-test.com',
'EmailType': 'FWD',
'IsUsingOurDNS': 'true',
'host': [
{
'HostId': '614433',
'Name': 'www',
'Type': 'CNAME',
'Address': '1.2.3.4',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': '',
'FriendlyName': 'CNAME Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
'HostId': '614432',
'Name': 'test',
'Type': 'A',
'Address': '1.2.3.4',
'MXPref': '10',
'TTL': '1800',
'FriendlyName': 'A Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
'HostId': '614431',
'Name': 'test',
'Type': 'A',
'Address': '2.3.4.5',
'MXPref': '10',
'TTL': '1800',
'FriendlyName': 'A Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
}
]
}
};
var req1 = nock(NAMECHEAP_ENDPOINT).get('/xml.response')
.query({
ApiUser: username,
ApiKey: token,
UserName: username,
ClientIp: '127.0.0.1',
Command: 'namecheap.domains.dns.getHosts',
SLD: DOMAIN_0.zoneName.split('.')[0],
TLD: DOMAIN_0.zoneName.split('.')[1]
})
.reply(200, GET_HOSTS_RETURN);
let getHostsFake = sinon.fake.yields(null, getHostsReturn);
let mockObj = {
dns: {
getHosts: getHostsFake
}
};
sandbox.stub(namecheap.prototype, 'domains').value(mockObj);
domains.getDnsRecords('test', DOMAIN_0.domain, 'A', function (error, result) {
expect(error).to.eql(null);
expect(req1.isDone()).to.be.ok();
expect(result).to.be.an(Array);
expect(result.length).to.eql(2);
expect(getHostsFake.calledOnce).to.eql(true);
expect(result).to.eql(['1.2.3.4', '2.3.4.5']);
done();
@@ -878,74 +1062,130 @@ describe('dns provider', function () {
});
it('del succeeds', function (done) {
const GET_HOSTS_RETURN = `<?xml version="1.0" encoding="utf-8"?>
<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
<Errors />
<Warnings />
<RequestedCommand>namecheap.domains.dns.gethosts</RequestedCommand>
<CommandResponse Type="namecheap.domains.dns.getHosts">
<DomainDNSGetHostsResult Domain="cloudron.space" EmailType="MX" IsUsingOurDNS="true">
<host HostId="160859434" Name="@" Type="MX" Address="my.nebulon.space." MXPref="10" TTL="300" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
<host HostId="173400612" Name="www" Type="CNAME" Address="1.2.3.4" MXPref="10" TTL="300" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
</DomainDNSGetHostsResult>
</CommandResponse>
<Server>PHX01APIEXT04</Server>
<GMTTimeDifference>--4:00</GMTTimeDifference>
<ExecutionTime>0.16</ExecutionTime>
</ApiResponse>`;
var req1 = nock(NAMECHEAP_ENDPOINT).get('/xml.response')
.query({
ApiUser: username,
ApiKey: token,
UserName: username,
ClientIp: '127.0.0.1',
Command: 'namecheap.domains.dns.getHosts',
SLD: DOMAIN_0.zoneName.split('.')[0],
TLD: DOMAIN_0.zoneName.split('.')[1]
})
.reply(200, GET_HOSTS_RETURN);
let getHostsReturn = {
'Type': 'namecheap.domains.dns.getHosts',
'DomainDNSGetHostsResult': {
'Domain': 'example-dns-test.com',
'EmailType': 'FWD',
'IsUsingOurDNS': 'true',
'host': [
{
'HostId': '614433',
'Name': 'www',
'Type': 'CNAME',
'Address': '1.2.3.4',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': '',
'FriendlyName': 'CNAME Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
'HostId': '614432',
'Name': '@',
'Type': 'URL',
'Address': 'http://www.example-dns-test.com/',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': 'URL Forwarding',
'FriendlyName': 'URL Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
}
]
}
};
let setInternalExpect = [
{
'HostId': '614432',
'HostName': '@',
'RecordType': 'URL',
'Address': 'http://www.example-dns-test.com/',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': 'URL Forwarding',
'FriendlyName': 'URL Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
}
];
let getHostsFake = sinon.fake.yields(null, getHostsReturn);
let setHostsFake = sinon.fake.yields(null, true);
let mockObj = {
dns: {
getHosts: getHostsFake,
setHosts: setHostsFake
}
};
sandbox.stub(namecheap.prototype, 'domains').value(mockObj);
domains.removeDnsRecords('www', DOMAIN_0.domain, 'CNAME', ['1.2.3.4'], function (error) {
expect(error).to.eql(null);
expect(req1.isDone()).to.be.ok();
expect(setHostsFake.calledOnce).to.eql(true);
expect(setHostsFake.calledWith(DOMAIN_0.domain, setInternalExpect)).to.eql(true);
done();
});
});
it('del succeeds with non-existing domain', function (done) {
const GET_HOSTS_RETURN = `<?xml version="1.0" encoding="utf-8"?>
<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
<Errors />
<Warnings />
<RequestedCommand>namecheap.domains.dns.gethosts</RequestedCommand>
<CommandResponse Type="namecheap.domains.dns.getHosts">
<DomainDNSGetHostsResult Domain="cloudron.space" EmailType="MX" IsUsingOurDNS="true">
<host HostId="160859434" Name="@" Type="MX" Address="my.nebulon.space." MXPref="10" TTL="300" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
<host HostId="173400612" Name="www" Type="CNAME" Address="1.2.3.4" MXPref="10" TTL="300" AssociatedAppTitle="" FriendlyName="" IsActive="true" IsDDNSEnabled="false" />
</DomainDNSGetHostsResult>
</CommandResponse>
<Server>PHX01APIEXT04</Server>
<GMTTimeDifference>--4:00</GMTTimeDifference>
<ExecutionTime>0.16</ExecutionTime>
</ApiResponse>`;
it('del succeeds w/ non-present host', function (done) {
var req1 = nock(NAMECHEAP_ENDPOINT).get('/xml.response')
.query({
ApiUser: username,
ApiKey: token,
UserName: username,
ClientIp: '127.0.0.1',
Command: 'namecheap.domains.dns.getHosts',
SLD: DOMAIN_0.zoneName.split('.')[0],
TLD: DOMAIN_0.zoneName.split('.')[1]
})
.reply(200, GET_HOSTS_RETURN);
let getHostsReturn = {
'Type': 'namecheap.domains.dns.getHosts',
'DomainDNSGetHostsResult': {
'Domain': 'example-dns-test.com',
'EmailType': 'FWD',
'IsUsingOurDNS': 'true',
'host': [
{
'HostId': '614433',
'Name': 'www',
'Type': 'CNAME',
'Address': 'parkingpage.namecheap.com.',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': '',
'FriendlyName': 'CNAME Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
},
{
'HostId': '614432',
'Name': '@',
'Type': 'URL',
'Address': 'http://www.example-dns-test.com/',
'MXPref': '10',
'TTL': '1800',
'AssociatedAppTitle': 'URL Forwarding',
'FriendlyName': 'URL Record',
'IsActive': 'true',
'IsDDNSEnabled': 'false'
}
]
}
};
let getHostsFake = sinon.fake.yields(null, getHostsReturn);
let setHostsFake = sinon.fake.yields(null, true);
let mockObj = {
dns: {
getHosts: getHostsFake,
setHosts: setHostsFake
}
};
sandbox.stub(namecheap.prototype, 'domains').value(mockObj);
domains.removeDnsRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) {
expect(error).to.eql(null);
expect(req1.isDone()).to.be.ok();
expect(setHostsFake.notCalled).to.eql(true);
done();
});
@@ -1135,7 +1375,7 @@ describe('dns provider', function () {
});
});
describe('gcdns', function () {
xdescribe('gcdns', function () {
var HOSTED_ZONES = [];
var zoneQueue = [];
var _OriginalGCDNS;
@@ -1173,7 +1413,7 @@ describe('dns provider', function () {
}
function fakeZone(name, ns, recordQueue) {
var zone = new GCDNS().zone(name.replace('.', '-'));
var zone = GCDNS().zone(name.replace('.', '-'));
zone.metadata.dnsName = name + '.';
zone.metadata.nameServers = ns || ['8.8.8.8', '8.8.4.4'];
zone.getRecords = mockery(recordQueue || zoneQueue);
@@ -1210,7 +1450,7 @@ describe('dns provider', function () {
it('upsert existing record succeeds', function (done) {
zoneQueue.push([null, HOSTED_ZONES]);
zoneQueue.push([null, [new GCDNS().zone('test').record('A', { 'name': 'test', data: ['5.6.7.8'], ttl: 1 })]]);
zoneQueue.push([null, [GCDNS().zone('test').record('A', { 'name': 'test', data: ['5.6.7.8'], ttl: 1 })]]);
zoneQueue.push([null, { id: '2' }]);
domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) {
@@ -1236,7 +1476,7 @@ describe('dns provider', function () {
it('get succeeds', function (done) {
zoneQueue.push([null, HOSTED_ZONES]);
zoneQueue.push([null, [new GCDNS().zone('test').record('A', { 'name': 'test', data: ['1.2.3.4', '5.6.7.8'], ttl: 1 })]]);
zoneQueue.push([null, [GCDNS().zone('test').record('A', { 'name': 'test', data: ['1.2.3.4', '5.6.7.8'], ttl: 1 })]]);
domains.getDnsRecords('test', DOMAIN_0.domain, 'A', function (error, result) {
expect(error).to.eql(null);
@@ -1251,7 +1491,7 @@ describe('dns provider', function () {
it('del succeeds', function (done) {
zoneQueue.push([null, HOSTED_ZONES]);
zoneQueue.push([null, [new GCDNS().zone('test').record('A', { 'name': 'test', data: ['5.6.7.8'], ttl: 1 })]]);
zoneQueue.push([null, [GCDNS().zone('test').record('A', { 'name': 'test', data: ['5.6.7.8'], ttl: 1 })]]);
zoneQueue.push([null, { id: '5' }]);
domains.removeDnsRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) {
+1 -1
View File
@@ -100,7 +100,7 @@ function setup(done) {
database._clear.bind(null),
ldapServer.start.bind(null),
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE),
maildb.add.bind(null, DOMAIN_0.domain, {}),
maildb.add.bind(null, DOMAIN_0.domain),
function (callback) {
users.createOwner(USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE, function (error, result) {
if (error) return callback(error);