Compare commits
76 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f45e1db06 | |||
| 2ab2255115 | |||
| 515b1db9d0 | |||
| a7fe7b0aa3 | |||
| 89389258d7 | |||
| 1aacf65372 | |||
| 7ffcfc5206 | |||
| 5ab2d9da8a | |||
| cd302a7621 | |||
| 1c8e699a71 | |||
| c4db0d746d | |||
| b7c5c99301 | |||
| 132c1872f4 | |||
| 0f04933dbf | |||
| 6d864d3621 | |||
| b6ee1fb662 | |||
| 649cd896fc | |||
| 39be267805 | |||
| f6356b2dff | |||
| 48574ce350 | |||
| 40a3145d92 | |||
| f42430b7c4 | |||
| 178d93033f | |||
| 01a1803625 | |||
| 42eef42cf3 | |||
| 9c096b18e1 | |||
| aa3ee2e180 | |||
| fdefc780b4 | |||
| 3826ae64c6 | |||
| dcdafda124 | |||
| fc2cc25861 | |||
| 68db4524f1 | |||
| 48b75accdd | |||
| 0313a60f44 | |||
| 9897b5d18a | |||
| e4cc431d35 | |||
| 535a755e74 | |||
| 2ae77a5ab7 | |||
| e36d7665fa | |||
| 786b627bad | |||
| c7ddbea8ed | |||
| af2a8ba07f | |||
| 4ffe03553a | |||
| f505fdd5cb | |||
| ce4f5c0ad6 | |||
| de2c596394 | |||
| 6cb041bcb2 | |||
| 0c0aeeae4c | |||
| 8bfb3d6b6d | |||
| f803754e08 | |||
| 09cfce79fb | |||
| 6479e333de | |||
| 28d1d5e960 | |||
| 15d8f4e89c | |||
| 8fdbd7bd5f | |||
| 7b5ed0b2a1 | |||
| b69c5f62c0 | |||
| 63f6f065ba | |||
| 92f0f56fae | |||
| cb8aa15e62 | |||
| 4356d673bc | |||
| 5ece159fba | |||
| b59776bf9b | |||
| 475795a107 | |||
| 9a80049d36 | |||
| daf212468f | |||
| 2f510c2625 | |||
| 7a977fa76b | |||
| f5e025c213 | |||
| 971b73f853 | |||
| 0103b21724 | |||
| cef5c1e78c | |||
| 50ff6b99e0 | |||
| 26dbd50cf2 | |||
| 84884b969e | |||
| 62174c5328 |
@@ -2367,3 +2367,35 @@
|
||||
[7.0.1]
|
||||
* Fix matrix wellKnown client migration
|
||||
|
||||
[7.0.2]
|
||||
* mail: POP3 flag was not returned correctly
|
||||
* external ldap: fix crash preventing users from logging in
|
||||
* volumes: ensure we don't crash if mount status is unexpected
|
||||
* backups: set default backup memory limit to 800
|
||||
* users: allow admins to specify password recovery email
|
||||
* retry startup tasks on database error
|
||||
|
||||
[7.0.3]
|
||||
* support: fix remoe support not working for 'root' user
|
||||
* Fix cog icon on app grid item hover for darkmode
|
||||
* Disable password reset and impersonate button for self user instead of hiding them
|
||||
* pop3: fix crash with auth of non-existent mailbox
|
||||
* mail: fix direction field in eventlog of deferred mails
|
||||
* mail: fix eventlog search
|
||||
* mail: save message-id in eventlog
|
||||
* backups: fix issue which resulted in incomplete backups when an app has backups disabled
|
||||
* restore: do not redirect until mail data has been restored
|
||||
* proxyauth: set viewport meta tag in login view
|
||||
|
||||
[7.0.4]
|
||||
* Add password reveal button to login pages
|
||||
* appstore: fix crash if account already registered
|
||||
* Do not nuke all the logrotate configs on update
|
||||
* Remove unused httpPaths from manifest
|
||||
* cloudron-support: add option to reset cloudron.io account
|
||||
* Fix flicker in login page
|
||||
* Fix LE account key re-use issue in DO 1-click image
|
||||
* mail: add non-tls ports for recvmail addon
|
||||
* backups: fix issue where mail backups where not cleaned up
|
||||
* notifications: fix automatic app update notifications
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('DELETE FROM blobs WHERE id=?', [ 'dhparams' ], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
Generated
+31
-31
@@ -246,7 +246,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
|
||||
},
|
||||
@@ -320,7 +320,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="
|
||||
},
|
||||
"async": {
|
||||
@@ -391,7 +391,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"
|
||||
@@ -526,7 +526,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": {
|
||||
@@ -628,9 +628,9 @@
|
||||
}
|
||||
},
|
||||
"cloudron-manifestformat": {
|
||||
"version": "5.10.2",
|
||||
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-5.10.2.tgz",
|
||||
"integrity": "sha512-RU+uwwdPDXJ+zoORBVCkA6+mDEgyYlqtHjZWnvomdcemfHnxsE54A1r7+spm2Tz3tS1aVUQN7Vd5l1PD21PUzg==",
|
||||
"version": "5.11.0",
|
||||
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-5.11.0.tgz",
|
||||
"integrity": "sha512-F+bOvZFNvmQQxpXYXNWtjkjEVpVvO7ZJlWj1RlDB9zyP31Uq1P9RdFBbvEih3PRx4NovKcENT28Rd59wCwWqpw==",
|
||||
"requires": {
|
||||
"cron": "^1.8.2",
|
||||
"java-packagename-regex": "^1.0.0",
|
||||
@@ -649,7 +649,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=",
|
||||
"dev": true
|
||||
},
|
||||
@@ -694,7 +694,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="
|
||||
},
|
||||
"configstore": {
|
||||
@@ -869,7 +869,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": {
|
||||
@@ -902,7 +902,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"
|
||||
@@ -1114,7 +1114,7 @@
|
||||
},
|
||||
"decamelize": {
|
||||
"version": "1.2.0",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
|
||||
},
|
||||
"decamelize-keys": {
|
||||
@@ -1374,7 +1374,7 @@
|
||||
},
|
||||
"ent": {
|
||||
"version": "2.2.0",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz",
|
||||
"integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0="
|
||||
},
|
||||
"env-paths": {
|
||||
@@ -1441,7 +1441,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
|
||||
},
|
||||
@@ -2144,7 +2144,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",
|
||||
@@ -2168,7 +2168,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
|
||||
},
|
||||
@@ -2245,12 +2245,12 @@
|
||||
},
|
||||
"isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
|
||||
},
|
||||
"isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
|
||||
},
|
||||
"isstream": {
|
||||
@@ -2723,14 +2723,14 @@
|
||||
"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="
|
||||
},
|
||||
"minimist-options": {
|
||||
@@ -3403,7 +3403,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=",
|
||||
"dev": true
|
||||
},
|
||||
@@ -3514,7 +3514,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": {
|
||||
@@ -3549,7 +3549,7 @@
|
||||
},
|
||||
"precond": {
|
||||
"version": "0.2.3",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz",
|
||||
"integrity": "sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw="
|
||||
},
|
||||
"pretty-bytes": {
|
||||
@@ -3941,7 +3941,7 @@
|
||||
},
|
||||
"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": {
|
||||
@@ -4153,7 +4153,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": {
|
||||
@@ -4176,7 +4176,7 @@
|
||||
},
|
||||
"signal-exit": {
|
||||
"version": "3.0.2",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
|
||||
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
|
||||
},
|
||||
"smtp-connection": {
|
||||
@@ -4395,7 +4395,7 @@
|
||||
},
|
||||
"stubs": {
|
||||
"version": "3.0.0",
|
||||
"resolved": false,
|
||||
"resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz",
|
||||
"integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls="
|
||||
},
|
||||
"superagent": {
|
||||
@@ -4794,7 +4794,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": {
|
||||
@@ -4865,7 +4865,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",
|
||||
@@ -4888,7 +4888,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": {
|
||||
@@ -4975,7 +4975,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": {
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
"aws-sdk": "^2.936.0",
|
||||
"basic-auth": "^2.0.1",
|
||||
"body-parser": "^1.19.0",
|
||||
"cloudron-manifestformat": "^5.10.2",
|
||||
"cloudron-manifestformat": "^5.11.0",
|
||||
"connect": "^3.7.0",
|
||||
"connect-lastmile": "^2.1.1",
|
||||
"connect-timeout": "^1.9.0",
|
||||
|
||||
@@ -45,7 +45,7 @@ else
|
||||
fi
|
||||
|
||||
# create docker network (while the infra code does this, most tests skip infra setup)
|
||||
docker network create --subnet=172.18.0.0/16 --ip-range=172.18.0.0/20 cloudron || true
|
||||
docker network create --subnet=172.18.0.0/16 --ip-range=172.18.0.0/20 --gateway 172.18.0.1 cloudron || true
|
||||
|
||||
# create the same mysql server version to test with
|
||||
OUT=`docker inspect mysql-server` || true
|
||||
|
||||
@@ -10,12 +10,13 @@ OUT="/tmp/cloudron-support.log"
|
||||
LINE="\n========================================================\n"
|
||||
CLOUDRON_SUPPORT_PUBLIC_KEY="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQVilclYAIu+ioDp/sgzzFz6YU0hPcRYY7ze/LiF/lC7uQqK062O54BFXTvQ3ehtFZCx3bNckjlT2e6gB8Qq07OM66De4/S/g+HJW4TReY2ppSPMVNag0TNGxDzVH8pPHOysAm33LqT2b6L/wEXwC6zWFXhOhHjcMqXvi8Ejaj20H1HVVcf/j8qs5Thkp9nAaFTgQTPu8pgwD8wDeYX1hc9d0PYGesTADvo6HF4hLEoEnefLw7PaStEbzk2fD3j7/g5r5HcgQQXBe74xYZ/1gWOX2pFNuRYOBSEIrNfJEjFJsqk3NR1+ZoMGK7j+AZBR4k0xbrmncQLcQzl6MMDzkp support@cloudron.io"
|
||||
HELP_MESSAGE="
|
||||
This script collects diagnostic information to help debug server related issues
|
||||
This script collects diagnostic information to help debug server related issues.
|
||||
|
||||
Options:
|
||||
--owner-login Login as owner
|
||||
--enable-ssh Enable SSH access for the Cloudron support team
|
||||
--help Show this message
|
||||
--owner-login Login as owner
|
||||
--enable-ssh Enable SSH access for the Cloudron support team
|
||||
--reset-appstore-account Reset associated cloudron.io account
|
||||
--help Show this message
|
||||
"
|
||||
|
||||
# We require root
|
||||
@@ -26,7 +27,7 @@ fi
|
||||
|
||||
enableSSH="false"
|
||||
|
||||
args=$(getopt -o "" -l "help,enable-ssh,admin-login,owner-login" -n "$0" -- "$@")
|
||||
args=$(getopt -o "" -l "help,enable-ssh,admin-login,owner-login,reset-appstore-account" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
@@ -44,6 +45,15 @@ while true; do
|
||||
echo "Login at https://${dashboard_domain} as ${admin_username} / ${admin_password} . This password may only be used once."
|
||||
exit 0
|
||||
;;
|
||||
--reset-appstore-account)
|
||||
echo -e "This will reset the Cloudron.io account associated with this Cloudron. Once reset, you can re-login with a different account in the Cloudron Dashboard. See https://docs.cloudron.io/appstore/#change-account for more information.\n"
|
||||
read -e -p "Reset the Cloudron.io account? [y/N] " choice
|
||||
[[ "$choice" != [Yy]* ]] && exit 1
|
||||
mysql -uroot -ppassword -e "DELETE FROM box.settings WHERE name='cloudron_token';" 2>/dev/null
|
||||
dashboard_domain=$(mysql -NB -uroot -ppassword -e "SELECT value FROM box.settings WHERE name='admin_fqdn'" 2>/dev/null)
|
||||
echo "Account reset. Please re-login at https://${dashboard_domain}/#/appstore"
|
||||
exit 0
|
||||
;;
|
||||
--) break;;
|
||||
*) echo "Unknown option $1"; exit 1;;
|
||||
esac
|
||||
@@ -141,8 +151,7 @@ if [[ "${enableSSH}" == "true" ]]; then
|
||||
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'])")
|
||||
paste_key=$(curl -X POST ${PASTEBIN}/documents --silent --data-binary "@$OUT" | python3 -c "import sys, json; print(json.load(sys.stdin)['key'])")
|
||||
echo "Done"
|
||||
|
||||
echo ""
|
||||
|
||||
+1
-2
@@ -38,7 +38,7 @@ systemctl restart apparmor
|
||||
|
||||
usermod ${USER} -a -G docker
|
||||
# unbound (which starts after box code) relies on this interface to exist. dockerproxy also relies on this.
|
||||
docker network create --subnet=172.18.0.0/16 --ip-range=172.18.0.0/20 cloudron || true
|
||||
docker network create --subnet=172.18.0.0/16 --ip-range=172.18.0.0/20 --gateway 172.18.0.1 cloudron || true
|
||||
|
||||
mkdir -p "${BOX_DATA_DIR}"
|
||||
mkdir -p "${APPS_DATA_DIR}"
|
||||
@@ -146,7 +146,6 @@ log "Configuring logrotate"
|
||||
if ! grep -q "^include ${PLATFORM_DATA_DIR}/logrotate.d" /etc/logrotate.conf; then
|
||||
echo -e "\ninclude ${PLATFORM_DATA_DIR}/logrotate.d\n" >> /etc/logrotate.conf
|
||||
fi
|
||||
rm -f "${PLATFORM_DATA_DIR}/logrotate.d/"*
|
||||
cp "${script_dir}/start/logrotate/"* "${PLATFORM_DATA_DIR}/logrotate.d/"
|
||||
|
||||
# logrotate files have to be owned by root, this is here to fixup existing installations where we were resetting the owner to yellowtent
|
||||
|
||||
+50
-29
@@ -9,6 +9,7 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
const assert = require('assert'),
|
||||
blobs = require('./blobs.js'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
crypto = require('crypto'),
|
||||
debug = require('debug')('box:cert/acme2'),
|
||||
@@ -31,7 +32,7 @@ const CA_PROD_DIRECTORY_URL = 'https://acme-v02.api.letsencrypt.org/directory',
|
||||
function Acme2(options) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
|
||||
this.accountKeyPem = options.accountKeyPem; // Buffer
|
||||
this.accountKeyPem = null; // Buffer .
|
||||
this.email = options.email;
|
||||
this.keyId = null;
|
||||
this.caDirectory = options.prod ? CA_PROD_DIRECTORY_URL : CA_STAGING_DIRECTORY_URL;
|
||||
@@ -87,10 +88,10 @@ Acme2.prototype.sendSignedRequest = async function (url, payload) {
|
||||
|
||||
let [error, response] = await safe(superagent.get(this.directory.newNonce).timeout(30000).ok(() => true));
|
||||
if (error) throw new BoxError(BoxError.NETWORK_ERROR, `Network error sending signed request: ${error.message}`);
|
||||
if (response.status !== 204) throw new BoxError(BoxError.EXTERNAL_ERROR, `Invalid response code when fetching nonce : ${response.status}`);
|
||||
if (response.status !== 204) throw new BoxError(BoxError.ACME_ERROR, `Invalid response code when fetching nonce : ${response.status}`);
|
||||
|
||||
const nonce = response.headers['Replay-Nonce'.toLowerCase()];
|
||||
if (!nonce) throw new BoxError(BoxError.EXTERNAL_ERROR, 'No nonce in response');
|
||||
if (!nonce) throw new BoxError(BoxError.ACME_ERROR, 'No nonce in response');
|
||||
|
||||
debug('sendSignedRequest: using nonce %s for url %s', nonce, url);
|
||||
|
||||
@@ -128,23 +129,43 @@ Acme2.prototype.updateContact = async function (registrationUri) {
|
||||
};
|
||||
|
||||
const result = await this.sendSignedRequest(registrationUri, JSON.stringify(payload));
|
||||
if (result.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Failed to update contact. Expecting 200, got ${result.status} ${JSON.stringify(result.body)}`);
|
||||
if (result.status !== 200) throw new BoxError(BoxError.ACME_ERROR, `Failed to update contact. Expecting 200, got ${result.status} ${JSON.stringify(result.body)}`);
|
||||
|
||||
debug(`updateContact: contact of user updated to ${this.email}`);
|
||||
};
|
||||
|
||||
Acme2.prototype.registerUser = async function () {
|
||||
async function generateAccountKey() {
|
||||
const acmeAccountKey = safe.child_process.execSync('openssl genrsa 4096');
|
||||
if (!acmeAccountKey) throw new BoxError(BoxError.OPENSSL_ERROR, `Could not generate acme account key: ${safe.error.message}`);
|
||||
return acmeAccountKey;
|
||||
}
|
||||
|
||||
Acme2.prototype.ensureAccount = async function () {
|
||||
const payload = {
|
||||
termsOfServiceAgreed: true
|
||||
};
|
||||
|
||||
debug('registerUser: registering user');
|
||||
debug('ensureAccount: registering user');
|
||||
|
||||
this.accountKeyPem = await blobs.get(blobs.ACME_ACCOUNT_KEY);
|
||||
if (!this.accountKeyPem) {
|
||||
debug('ensureAccount: generating new account keys');
|
||||
this.accountKeyPem = await generateAccountKey();
|
||||
await blobs.set(blobs.ACME_ACCOUNT_KEY, this.accountKeyPem);
|
||||
}
|
||||
|
||||
let result = await this.sendSignedRequest(this.directory.newAccount, JSON.stringify(payload));
|
||||
if (result.status === 403 && result.body.type === 'urn:ietf:params:acme:error:unauthorized') {
|
||||
debug(`ensureAccount: key was revoked. ${result.status} ${JSON.stringify(result.body)}. generating new account key`);
|
||||
this.accountKeyPem = await generateAccountKey();
|
||||
await blobs.set(blobs.ACME_ACCOUNT_KEY, this.accountKeyPem);
|
||||
result = await this.sendSignedRequest(this.directory.newAccount, JSON.stringify(payload));
|
||||
}
|
||||
|
||||
const result = await this.sendSignedRequest(this.directory.newAccount, JSON.stringify(payload));
|
||||
// 200 if already exists. 201 for new accounts
|
||||
if (result.status !== 200 && result.status !== 201) return new BoxError(BoxError.EXTERNAL_ERROR, `Failed to register new account. Expecting 200 or 201, got ${result.status} ${JSON.stringify(result.body)}`);
|
||||
if (result.status !== 200 && result.status !== 201) throw new BoxError(BoxError.ACME_ERROR, `Failed to register new account. Expecting 200 or 201, got ${result.status} ${JSON.stringify(result.body)}`);
|
||||
|
||||
debug(`registerUser: user registered keyid: ${result.headers.location}`);
|
||||
debug(`ensureAccount: user registered keyid: ${result.headers.location}`);
|
||||
|
||||
this.keyId = result.headers.location;
|
||||
|
||||
@@ -165,15 +186,15 @@ Acme2.prototype.newOrder = async function (domain) {
|
||||
|
||||
const result = await this.sendSignedRequest(this.directory.newOrder, JSON.stringify(payload));
|
||||
if (result.status === 403) throw new BoxError(BoxError.ACCESS_DENIED, `Forbidden sending new order: ${result.body.detail}`);
|
||||
if (result.status !== 201) throw new BoxError(BoxError.EXTERNAL_ERROR, `Failed to send new order. Expecting 201, got ${result.statusCode} ${JSON.stringify(result.body)}`);
|
||||
if (result.status !== 201) throw new BoxError(BoxError.ACME_ERROR, `Failed to send new order. Expecting 201, got ${result.statusCode} ${JSON.stringify(result.body)}`);
|
||||
|
||||
debug('newOrder: created order %s %j', domain, result.body);
|
||||
|
||||
const order = result.body, orderUrl = result.headers.location;
|
||||
|
||||
if (!Array.isArray(order.authorizations)) throw new BoxError(BoxError.EXTERNAL_ERROR, 'invalid authorizations in order');
|
||||
if (typeof order.finalize !== 'string') throw new BoxError(BoxError.EXTERNAL_ERROR, 'invalid finalize in order');
|
||||
if (typeof orderUrl !== 'string') throw new BoxError(BoxError.EXTERNAL_ERROR, 'invalid order location in order header');
|
||||
if (!Array.isArray(order.authorizations)) throw new BoxError(BoxError.ACME_ERROR, 'invalid authorizations in order');
|
||||
if (typeof order.finalize !== 'string') throw new BoxError(BoxError.ACME_ERROR, 'invalid finalize in order');
|
||||
if (typeof orderUrl !== 'string') throw new BoxError(BoxError.ACME_ERROR, 'invalid order location in order header');
|
||||
|
||||
return { order, orderUrl };
|
||||
};
|
||||
@@ -189,14 +210,14 @@ Acme2.prototype.waitForOrder = async function (orderUrl) {
|
||||
const result = await this.postAsGet(orderUrl);
|
||||
if (result.status !== 200) {
|
||||
debug(`waitForOrder: invalid response code getting uri ${result.status}`);
|
||||
throw new BoxError(BoxError.EXTERNAL_ERROR, `Bad response code: ${result.status}`);
|
||||
throw new BoxError(BoxError.ACME_ERROR, `Bad response when waiting for order. code: ${result.status}`);
|
||||
}
|
||||
|
||||
debug('waitForOrder: status is "%s %j', result.body.status, result.body);
|
||||
|
||||
if (result.body.status === 'pending' || result.body.status === 'processing') throw new BoxError(BoxError.TRY_AGAIN, `Request is in ${result.body.status} state`);
|
||||
if (result.body.status === 'pending' || result.body.status === 'processing') throw new BoxError(BoxError.ACME_ERROR, `Request is in ${result.body.status} state`);
|
||||
else if (result.body.status === 'valid' && result.body.certificate) return result.body.certificate;
|
||||
else throw new BoxError(BoxError.EXTERNAL_ERROR, `Unexpected status or invalid response: ${JSON.stringify(result.body)}`);
|
||||
else throw new BoxError(BoxError.ACME_ERROR, `Unexpected status or invalid response when waiting for order: ${JSON.stringify(result.body)}`);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -228,7 +249,7 @@ Acme2.prototype.notifyChallengeReady = async function (challenge) {
|
||||
};
|
||||
|
||||
const result = await this.sendSignedRequest(challenge.url, JSON.stringify(payload));
|
||||
if (result.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Failed to notify challenge. Expecting 200, got ${result.statusCode} ${JSON.stringify(result.body)}`);
|
||||
if (result.status !== 200) throw new BoxError(BoxError.ACME_ERROR, `Failed to notify challenge. Expecting 200, got ${result.statusCode} ${JSON.stringify(result.body)}`);
|
||||
};
|
||||
|
||||
Acme2.prototype.waitForChallenge = async function (challenge) {
|
||||
@@ -242,14 +263,14 @@ Acme2.prototype.waitForChallenge = async function (challenge) {
|
||||
const result = await this.postAsGet(challenge.url);
|
||||
if (result.status !== 200) {
|
||||
debug(`waitForChallenge: invalid response code getting uri ${result.status}`);
|
||||
throw new BoxError(BoxError.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode);
|
||||
throw new BoxError(BoxError.ACME_ERROR, `Bad response code when waiting for challenge : ${result.status}`);
|
||||
}
|
||||
|
||||
debug(`waitForChallenge: status is "${result.body.status}" "${JSON.stringify(result.body)}"`);
|
||||
|
||||
if (result.body.status === 'pending') throw new BoxError(BoxError.TRY_AGAIN);
|
||||
if (result.body.status === 'pending') throw new BoxError(BoxError.ACME_ERROR, 'Challenge is in pending state');
|
||||
else if (result.body.status === 'valid') return;
|
||||
else throw new BoxError(BoxError.EXTERNAL_ERROR, `Unexpected status: ${result.body.status}`);
|
||||
else throw new BoxError(BoxError.ACME_ERROR, `Unexpected status when waiting for challenge: ${result.body.status}`);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -267,7 +288,7 @@ Acme2.prototype.signCertificate = async function (domain, finalizationUrl, csrDe
|
||||
|
||||
const result = await this.sendSignedRequest(finalizationUrl, JSON.stringify(payload));
|
||||
// 429 means we reached the cert limit for this domain
|
||||
if (result.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Failed to sign certificate. Expecting 200, got ${result.status} ${JSON.stringify(result.body)}`);
|
||||
if (result.status !== 200) throw new BoxError(BoxError.ACME_ERROR, `Failed to sign certificate. Expecting 200, got ${result.status} ${JSON.stringify(result.body)}`);
|
||||
};
|
||||
|
||||
Acme2.prototype.createKeyAndCsr = async function (hostname, keyFilePath, csrFilePath) {
|
||||
@@ -318,8 +339,8 @@ Acme2.prototype.downloadCertificate = async function (hostname, certUrl, certFil
|
||||
debug('downloadCertificate: downloading certificate');
|
||||
|
||||
const result = await this.postAsGet(certUrl);
|
||||
if (result.statusCode === 202) throw new BoxError(BoxError.TRY_AGAIN, 'Retry downloading certificate');
|
||||
if (result.statusCode !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Failed to get cert. Expecting 200, got ${result.statusCode} ${JSON.stringify(result.body)}`);
|
||||
if (result.statusCode === 202) throw new BoxError(BoxError.ACME_ERROR, 'Retry downloading certificate');
|
||||
if (result.statusCode !== 200) throw new BoxError(BoxError.ACME_ERROR, `Failed to get cert. Expecting 200, got ${result.statusCode} ${JSON.stringify(result.body)}`);
|
||||
|
||||
const fullChainPem = result.body; // buffer
|
||||
|
||||
@@ -337,7 +358,7 @@ Acme2.prototype.prepareHttpChallenge = async function (hostname, domain, authori
|
||||
|
||||
debug('prepareHttpChallenge: challenges: %j', authorization);
|
||||
let httpChallenges = authorization.challenges.filter(function(x) { return x.type === 'http-01'; });
|
||||
if (httpChallenges.length === 0) throw new BoxError(BoxError.EXTERNAL_ERROR, 'no http challenges');
|
||||
if (httpChallenges.length === 0) throw new BoxError(BoxError.ACME_ERROR, 'no http challenges');
|
||||
let challenge = httpChallenges[0];
|
||||
|
||||
debug('prepareHttpChallenge: preparing for challenge %j', challenge);
|
||||
@@ -386,7 +407,7 @@ Acme2.prototype.prepareDnsChallenge = async function (hostname, domain, authoriz
|
||||
|
||||
debug('prepareDnsChallenge: challenges: %j', authorization);
|
||||
const dnsChallenges = authorization.challenges.filter(function(x) { return x.type === 'dns-01'; });
|
||||
if (dnsChallenges.length === 0) throw new BoxError(BoxError.EXTERNAL_ERROR, 'no dns challenges');
|
||||
if (dnsChallenges.length === 0) throw new BoxError(BoxError.ACME_ERROR, 'no dns challenges');
|
||||
const challenge = dnsChallenges[0];
|
||||
|
||||
const keyAuthorization = this.getKeyAuthorization(challenge.token);
|
||||
@@ -431,7 +452,7 @@ Acme2.prototype.prepareChallenge = async function (hostname, domain, authorizati
|
||||
debug(`prepareChallenge: http: ${this.performHttpAuthorization}`);
|
||||
|
||||
const response = await this.postAsGet(authorizationUrl);
|
||||
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Invalid response code getting authorization : ${response.status}`);
|
||||
if (response.status !== 200) throw new BoxError(BoxError.ACME_ERROR, `Invalid response code getting authorization : ${response.status}`);
|
||||
|
||||
const authorization = response.body;
|
||||
|
||||
@@ -464,7 +485,7 @@ Acme2.prototype.acmeFlow = async function (hostname, domain, paths) {
|
||||
|
||||
const { certFilePath, keyFilePath, csrFilePath, acmeChallengesDir } = paths;
|
||||
|
||||
await this.registerUser();
|
||||
await this.ensureAccount();
|
||||
const { order, orderUrl } = await this.newOrder(hostname);
|
||||
|
||||
for (let i = 0; i < order.authorizations.length; i++) {
|
||||
@@ -491,11 +512,11 @@ Acme2.prototype.loadDirectory = async function () {
|
||||
await promiseRetry({ times: 3, interval: 20000 }, async () => {
|
||||
const response = await superagent.get(this.caDirectory).timeout(30000).ok(() => true);
|
||||
|
||||
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Invalid response code when fetching directory : ${response.status}`);
|
||||
if (response.status !== 200) throw new BoxError(BoxError.ACME_ERROR, `Invalid response code when fetching directory : ${response.status}`);
|
||||
|
||||
if (typeof response.body.newNonce !== 'string' ||
|
||||
typeof response.body.newOrder !== 'string' ||
|
||||
typeof response.body.newAccount !== 'string') throw new BoxError(BoxError.EXTERNAL_ERROR, `Invalid response body : ${response.body}`);
|
||||
typeof response.body.newAccount !== 'string') throw new BoxError(BoxError.ACME_ERROR, `Invalid response body : ${response.body}`);
|
||||
|
||||
this.directory = response.body;
|
||||
});
|
||||
|
||||
+58
-38
@@ -141,7 +141,6 @@ exports = module.exports = {
|
||||
const appstore = require('./appstore.js'),
|
||||
appTaskManager = require('./apptaskmanager.js'),
|
||||
assert = require('assert'),
|
||||
AuditSource = require('./auditsource.js'),
|
||||
backups = require('./backups.js'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
constants = require('./constants.js'),
|
||||
@@ -1036,7 +1035,13 @@ function mailboxNameForLocation(location, manifest) {
|
||||
return 'noreply.app';
|
||||
}
|
||||
|
||||
async function onTaskFinished(appId, installationState, taskId, error) {
|
||||
async function onTaskFinished(error, appId, installationState, taskId, auditSource) {
|
||||
assert(!error || typeof error === 'object');
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof installationState, 'string');
|
||||
assert.strictEqual(typeof taskId, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
|
||||
const success = !error;
|
||||
const errorMessage = error?.message || null;
|
||||
|
||||
@@ -1046,25 +1051,26 @@ async function onTaskFinished(appId, installationState, taskId, error) {
|
||||
|
||||
switch (installationState) {
|
||||
case exports.ISTATE_PENDING_DATA_DIR_MIGRATION:
|
||||
if (success) await safe(services.rebuildService('sftp', AuditSource.APPTASK), { debug });
|
||||
if (success) await safe(services.rebuildService('sftp', auditSource), { debug });
|
||||
break;
|
||||
case exports.ISTATE_PENDING_UPDATE: {
|
||||
const fromManifest = success ? task.args[1].updateConfig.manifest : app.manifest;
|
||||
const toManifest = success ? app.manifest : task.args[1].updateConfig.manifest;
|
||||
|
||||
await eventlog.add(eventlog.ACTION_APP_UPDATE_FINISH, AuditSource.APPTASK, { app, toManifest, fromManifest, success, errorMessage });
|
||||
await eventlog.add(eventlog.ACTION_APP_UPDATE_FINISH, auditSource, { app, toManifest, fromManifest, success, errorMessage });
|
||||
break;
|
||||
}
|
||||
case exports.ISTATE_PENDING_BACKUP:
|
||||
await eventlog.add(eventlog.ACTION_APP_BACKUP_FINISH, AuditSource.APPTASK, { app, success, errorMessage, backupId: task.result });
|
||||
await eventlog.add(eventlog.ACTION_APP_BACKUP_FINISH, auditSource, { app, success, errorMessage, backupId: task.result });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async function scheduleTask(appId, installationState, taskId) {
|
||||
async function scheduleTask(appId, installationState, taskId, auditSource) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof installationState, 'string');
|
||||
assert.strictEqual(typeof taskId, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
|
||||
const backupConfig = await settings.getBackupConfig();
|
||||
|
||||
@@ -1093,14 +1099,15 @@ async function scheduleTask(appId, installationState, taskId) {
|
||||
await safe(update(appId, { taskId: null }), { debug });
|
||||
}
|
||||
|
||||
await safe(onTaskFinished(appId, installationState, taskId, error), { debug }); // ignore error
|
||||
await safe(onTaskFinished(error, appId, installationState, taskId, auditSource), { debug }); // ignore error
|
||||
});
|
||||
}
|
||||
|
||||
async function addTask(appId, installationState, task) {
|
||||
async function addTask(appId, installationState, task, auditSource) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof installationState, 'string');
|
||||
assert.strictEqual(typeof task, 'object'); // { args, values }
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
|
||||
const { args, values } = task;
|
||||
// TODO: match the SQL logic to match checkAppState. this means checking the error.installationState and installationState. Unfortunately, former is JSON right now
|
||||
@@ -1114,7 +1121,7 @@ async function addTask(appId, installationState, task) {
|
||||
if (updateError && updateError.reason === BoxError.NOT_FOUND) throw new BoxError(BoxError.BAD_STATE, 'Another task is scheduled for this app'); // could be because app went away OR a taskId exists
|
||||
if (updateError) throw updateError;
|
||||
|
||||
if (scheduleNow) await safe(scheduleTask(appId, installationState, taskId), { debug }); // ignore error
|
||||
if (scheduleNow) await safe(scheduleTask(appId, installationState, taskId, auditSource), { debug }); // ignore error
|
||||
|
||||
return taskId;
|
||||
}
|
||||
@@ -1162,6 +1169,11 @@ async function validateLocations(locations) {
|
||||
return domainObjectMap;
|
||||
}
|
||||
|
||||
async function getCount() {
|
||||
const result = await database.query('SELECT COUNT(*) AS total FROM apps');
|
||||
return result[0].total;
|
||||
}
|
||||
|
||||
async function install(data, auditSource) {
|
||||
assert(data && typeof data === 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
@@ -1237,6 +1249,8 @@ async function install(data, auditSource) {
|
||||
|
||||
const domainObjectMap = await validateLocations(locations);
|
||||
|
||||
if (settings.isDemo() && (await getCount() >= constants.DEMO_APP_LIMIT)) throw new BoxError(BoxError.BAD_STATE, 'Too many installed apps, please uninstall a few and try again');
|
||||
|
||||
const appId = uuid.v4();
|
||||
debug('Will install app with id : ' + appId);
|
||||
|
||||
@@ -1272,7 +1286,7 @@ async function install(data, auditSource) {
|
||||
requiredState: app.installationState
|
||||
};
|
||||
|
||||
const taskId = await addTask(appId, app.installationState, task);
|
||||
const taskId = await addTask(appId, app.installationState, task, auditSource);
|
||||
|
||||
const newApp = _.extend({}, _.omit(app, 'icon'), { appStoreId, manifest, location, domain, portBindings });
|
||||
newApp.fqdn = dns.fqdn(newApp.location, domainObjectMap[newApp.domain]);
|
||||
@@ -1380,7 +1394,7 @@ async function setMemoryLimit(app, memoryLimit, auditSource) {
|
||||
args: {},
|
||||
values: { memoryLimit }
|
||||
};
|
||||
const taskId = await addTask(appId, exports.ISTATE_PENDING_RESIZE, task);
|
||||
const taskId = await addTask(appId, exports.ISTATE_PENDING_RESIZE, task, auditSource);
|
||||
|
||||
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, memoryLimit, taskId });
|
||||
|
||||
@@ -1403,7 +1417,7 @@ async function setCpuShares(app, cpuShares, auditSource) {
|
||||
args: {},
|
||||
values: { cpuShares }
|
||||
};
|
||||
const taskId = await safe(addTask(appId, exports.ISTATE_PENDING_RESIZE, task));
|
||||
const taskId = await safe(addTask(appId, exports.ISTATE_PENDING_RESIZE, task, auditSource));
|
||||
|
||||
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, cpuShares, taskId });
|
||||
|
||||
@@ -1423,7 +1437,7 @@ async function setMounts(app, mounts, auditSource) {
|
||||
args: {},
|
||||
values: { mounts }
|
||||
};
|
||||
const [taskError, taskId] = await safe(addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task));
|
||||
const [taskError, taskId] = await safe(addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task, auditSource));
|
||||
if (taskError && taskError.reason === BoxError.ALREADY_EXISTS) throw new BoxError(BoxError.CONFLICT, 'Duplicate mount points');
|
||||
if (taskError) throw taskError;
|
||||
|
||||
@@ -1448,7 +1462,7 @@ async function setEnvironment(app, env, auditSource) {
|
||||
args: {},
|
||||
values: { env }
|
||||
};
|
||||
const taskId = await addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task);
|
||||
const taskId = await addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task, auditSource);
|
||||
|
||||
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, env, taskId });
|
||||
|
||||
@@ -1471,7 +1485,7 @@ async function setDebugMode(app, debugMode, auditSource) {
|
||||
args: {},
|
||||
values: { debugMode }
|
||||
};
|
||||
const taskId = await addTask(appId, exports.ISTATE_PENDING_DEBUG, task);
|
||||
const taskId = await addTask(appId, exports.ISTATE_PENDING_DEBUG, task, auditSource);
|
||||
|
||||
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, debugMode, taskId });
|
||||
|
||||
@@ -1513,7 +1527,7 @@ async function setMailbox(app, data, auditSource) {
|
||||
args: {},
|
||||
values: { enableMailbox, mailboxName, mailboxDomain }
|
||||
};
|
||||
const taskId = await addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task);
|
||||
const taskId = await addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task, auditSource);
|
||||
|
||||
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, mailboxName, mailboxDomain, taskId });
|
||||
|
||||
@@ -1548,7 +1562,7 @@ async function setInbox(app, data, auditSource) {
|
||||
args: {},
|
||||
values: { enableInbox, inboxName, inboxDomain }
|
||||
};
|
||||
const taskId = await addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task);
|
||||
const taskId = await addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task, auditSource);
|
||||
|
||||
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, enableInbox, inboxName, inboxDomain, taskId });
|
||||
|
||||
@@ -1668,7 +1682,7 @@ async function setLocation(app, data, auditSource) {
|
||||
},
|
||||
values
|
||||
};
|
||||
let [taskError, taskId] = await safe(addTask(appId, exports.ISTATE_PENDING_LOCATION_CHANGE, task));
|
||||
let [taskError, taskId] = await safe(addTask(appId, exports.ISTATE_PENDING_LOCATION_CHANGE, task, auditSource));
|
||||
if (taskError && taskError.reason === BoxError.ALREADY_EXISTS) taskError = getDuplicateErrorDetails(error.message, locations, domainObjectMap, data.portBindings);
|
||||
if (taskError) throw taskError;
|
||||
|
||||
@@ -1697,7 +1711,7 @@ async function setDataDir(app, dataDir, auditSource) {
|
||||
args: { newDataDir: dataDir },
|
||||
values: {}
|
||||
};
|
||||
const taskId = await addTask(appId, exports.ISTATE_PENDING_DATA_DIR_MIGRATION, task);
|
||||
const taskId = await addTask(appId, exports.ISTATE_PENDING_DATA_DIR_MIGRATION, task, auditSource);
|
||||
|
||||
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, dataDir, taskId });
|
||||
|
||||
@@ -1771,7 +1785,7 @@ async function updateApp(app, data, auditSource) {
|
||||
args: { updateConfig },
|
||||
values
|
||||
};
|
||||
const taskId = await addTask(appId, exports.ISTATE_PENDING_UPDATE, task);
|
||||
const taskId = await addTask(appId, exports.ISTATE_PENDING_UPDATE, task, auditSource);
|
||||
|
||||
await eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId, app, skipBackup, toManifest: manifest, fromManifest: app.manifest, force: data.force, taskId });
|
||||
|
||||
@@ -1890,7 +1904,7 @@ async function repair(app, data, auditSource) {
|
||||
}
|
||||
}
|
||||
|
||||
const taskId = await addTask(appId, errorState, task);
|
||||
const taskId = await addTask(appId, errorState, task, auditSource);
|
||||
|
||||
await eventlog.add(eventlog.ACTION_APP_REPAIR, auditSource, { app, taskId });
|
||||
|
||||
@@ -1937,7 +1951,7 @@ async function restore(app, backupId, auditSource) {
|
||||
values
|
||||
};
|
||||
|
||||
const taskId = await addTask(appId, exports.ISTATE_PENDING_RESTORE, task);
|
||||
const taskId = await addTask(appId, exports.ISTATE_PENDING_RESTORE, task, auditSource);
|
||||
|
||||
await eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { app: app, backupId: backupInfo.id, fromManifest: app.manifest, toManifest: backupInfo.manifest, taskId });
|
||||
|
||||
@@ -2003,7 +2017,7 @@ async function importApp(app, data, auditSource) {
|
||||
},
|
||||
values: {}
|
||||
};
|
||||
const taskId = await addTask(appId, exports.ISTATE_PENDING_IMPORT, task);
|
||||
const taskId = await addTask(appId, exports.ISTATE_PENDING_IMPORT, task, auditSource);
|
||||
|
||||
await eventlog.add(eventlog.ACTION_APP_IMPORT, auditSource, { app: app, backupId, fromManifest: app.manifest, toManifest: app.manifest, taskId });
|
||||
|
||||
@@ -2025,7 +2039,7 @@ async function exportApp(app, data, auditSource) {
|
||||
values: {}
|
||||
};
|
||||
|
||||
const taskId = await addTask(appId, exports.ISTATE_PENDING_BACKUP, task);
|
||||
const taskId = await addTask(appId, exports.ISTATE_PENDING_BACKUP, task, auditSource);
|
||||
return { taskId };
|
||||
}
|
||||
|
||||
@@ -2117,7 +2131,7 @@ async function clone(app, data, user, auditSource) {
|
||||
values: {},
|
||||
requiredState: exports.ISTATE_PENDING_CLONE
|
||||
};
|
||||
const taskId = await addTask(newAppId, exports.ISTATE_PENDING_CLONE, task);
|
||||
const taskId = await addTask(newAppId, exports.ISTATE_PENDING_CLONE, task, auditSource);
|
||||
|
||||
const newApp = _.extend({}, _.omit(obj, 'icon'), { appStoreId, manifest, location, domain, portBindings });
|
||||
newApp.fqdn = dns.fqdn(newApp.location, domainObjectMap[newApp.domain]);
|
||||
@@ -2144,7 +2158,7 @@ async function uninstall(app, auditSource) {
|
||||
values: {},
|
||||
requiredState: null // can run in any state, as long as no task is active
|
||||
};
|
||||
const taskId = await addTask(appId, exports.ISTATE_PENDING_UNINSTALL, task);
|
||||
const taskId = await addTask(appId, exports.ISTATE_PENDING_UNINSTALL, task, auditSource);
|
||||
await eventlog.add(eventlog.ACTION_APP_UNINSTALL, auditSource, { appId, app, taskId });
|
||||
|
||||
return { taskId };
|
||||
@@ -2162,7 +2176,7 @@ async function start(app, auditSource) {
|
||||
args: {},
|
||||
values: { runState: exports.RSTATE_RUNNING }
|
||||
};
|
||||
const taskId = await addTask(appId, exports.ISTATE_PENDING_START, task);
|
||||
const taskId = await addTask(appId, exports.ISTATE_PENDING_START, task, auditSource);
|
||||
await eventlog.add(eventlog.ACTION_APP_START, auditSource, { appId, app, taskId });
|
||||
return { taskId };
|
||||
}
|
||||
@@ -2179,7 +2193,7 @@ async function stop(app, auditSource) {
|
||||
args: {},
|
||||
values: { runState: exports.RSTATE_STOPPED }
|
||||
};
|
||||
const taskId = await addTask(appId, exports.ISTATE_PENDING_STOP, task);
|
||||
const taskId = await addTask(appId, exports.ISTATE_PENDING_STOP, task, auditSource);
|
||||
|
||||
await eventlog.add(eventlog.ACTION_APP_STOP, auditSource, { appId, app, taskId });
|
||||
|
||||
@@ -2198,7 +2212,7 @@ async function restart(app, auditSource) {
|
||||
args: {},
|
||||
values: { runState: exports.RSTATE_RUNNING }
|
||||
};
|
||||
const taskId = await addTask(appId, exports.ISTATE_PENDING_RESTART, task);
|
||||
const taskId = await addTask(appId, exports.ISTATE_PENDING_RESTART, task, auditSource);
|
||||
|
||||
await eventlog.add(eventlog.ACTION_APP_RESTART, auditSource, { appId, app, taskId });
|
||||
|
||||
@@ -2333,7 +2347,7 @@ async function backup(app, auditSource) {
|
||||
args: {},
|
||||
values: {}
|
||||
};
|
||||
const taskId = await addTask(appId, exports.ISTATE_PENDING_BACKUP, task);
|
||||
const taskId = await addTask(appId, exports.ISTATE_PENDING_BACKUP, task, auditSource);
|
||||
await eventlog.add(eventlog.ACTION_APP_BACKUP, auditSource, { app, appId, taskId });
|
||||
|
||||
return { taskId };
|
||||
@@ -2347,8 +2361,9 @@ async function listBackups(app, page, perPage) {
|
||||
return await backups.getByIdentifierAndStatePaged(app.id, backups.BACKUP_STATE_NORMAL, page, perPage);
|
||||
}
|
||||
|
||||
async function restoreInstalledApps(options) {
|
||||
async function restoreInstalledApps(options, auditSource) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
|
||||
let apps = await list();
|
||||
apps = apps.filter(app => app.installationState !== exports.ISTATE_ERROR); // remove errored apps. let them be 'repaired' by hand
|
||||
@@ -2376,13 +2391,15 @@ async function restoreInstalledApps(options) {
|
||||
|
||||
debug(`restoreInstalledApps: marking ${app.fqdn} for restore using restore config ${JSON.stringify(restoreConfig)}`);
|
||||
|
||||
const [addTaskError, taskId] = await safe(addTask(app.id, installationState, task));
|
||||
const [addTaskError, taskId] = await safe(addTask(app.id, installationState, task, auditSource));
|
||||
if (addTaskError) debug(`restoreInstalledApps: error marking ${app.fqdn} for restore: ${JSON.stringify(addTaskError)}`);
|
||||
else debug(`restoreInstalledApps: marked ${app.id} for restore with taskId ${taskId}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function configureInstalledApps() {
|
||||
async function configureInstalledApps(auditSource) {
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
|
||||
let apps = await list();
|
||||
apps = apps.filter(app => app.installationState !== exports.ISTATE_ERROR); // remove errored apps. let them be 'repaired' by hand
|
||||
apps = apps.filter(app => app.installationState !== exports.ISTATE_PENDING_CONFIGURE); // safeguard against tasks being created non-stop if we crash on startup
|
||||
@@ -2397,14 +2414,15 @@ async function configureInstalledApps() {
|
||||
requireNullTaskId: false // ignore existing stale taskId
|
||||
};
|
||||
|
||||
const [addTaskError, taskId] = await safe(addTask(app.id, exports.ISTATE_PENDING_CONFIGURE, task));
|
||||
const [addTaskError, taskId] = await safe(addTask(app.id, exports.ISTATE_PENDING_CONFIGURE, task, auditSource));
|
||||
if (addTaskError) debug(`configureInstalledApps: error marking ${app.fqdn} for configure: ${JSON.stringify(addTaskError)}`);
|
||||
else debug(`configureInstalledApps: marked ${app.id} for re-configure with taskId ${taskId}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function restartAppsUsingAddons(changedAddons) {
|
||||
async function restartAppsUsingAddons(changedAddons, auditSource) {
|
||||
assert(Array.isArray(changedAddons));
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
|
||||
let apps = await list();
|
||||
apps = apps.filter(app => app.manifest.addons && _.intersection(Object.keys(app.manifest.addons), changedAddons).length !== 0);
|
||||
@@ -2424,14 +2442,16 @@ async function restartAppsUsingAddons(changedAddons) {
|
||||
const [stopError] = await safe(docker.stopContainers(app.id));
|
||||
if (stopError) debug(`restartAppsUsingAddons: error stopping ${app.fqdn}`, stopError);
|
||||
|
||||
const [addTaskError, taskId] = await safe(addTask(app.id, exports.ISTATE_PENDING_RESTART, task));
|
||||
const [addTaskError, taskId] = await safe(addTask(app.id, exports.ISTATE_PENDING_RESTART, task, auditSource));
|
||||
if (addTaskError) debug(`restartAppsUsingAddons: error marking ${app.fqdn} for restart: ${JSON.stringify(addTaskError)}`);
|
||||
else debug(`restartAppsUsingAddons: marked ${app.id} for restart with taskId ${taskId}`);
|
||||
}
|
||||
}
|
||||
|
||||
// auto-restart app tasks after a crash
|
||||
async function schedulePendingTasks() {
|
||||
async function schedulePendingTasks(auditSource) {
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
|
||||
debug('schedulePendingTasks: scheduling app tasks');
|
||||
|
||||
const result = await list();
|
||||
@@ -2441,7 +2461,7 @@ async function schedulePendingTasks() {
|
||||
|
||||
debug(`schedulePendingTasks: schedule task for ${app.fqdn} ${app.id}: state=${app.installationState},taskId=${app.taskId}`);
|
||||
|
||||
await safe(scheduleTask(app.id, app.installationState, app.taskId), { debug }); // ignore error
|
||||
await safe(scheduleTask(app.id, app.installationState, app.taskId, auditSource), { debug }); // ignore error
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -107,7 +107,7 @@ async function registerUser(email, password) {
|
||||
|
||||
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message);
|
||||
|
||||
if (response.status === 409) throw new BoxError(BoxError.ALREADY_EXISTS, error.message);
|
||||
if (response.status === 409) throw new BoxError(BoxError.ALREADY_EXISTS, 'account already exists');
|
||||
if (response.status !== 201) throw new BoxError(BoxError.EXTERNAL_ERROR, `register status code: ${response.status}`);
|
||||
}
|
||||
|
||||
|
||||
+1
-2
@@ -565,7 +565,6 @@ async function update(app, args, progressCallback) {
|
||||
// app does not want these addons anymore
|
||||
// FIXME: this does not handle option changes (like multipleDatabases)
|
||||
const unusedAddons = _.omit(app.manifest.addons, Object.keys(updateConfig.manifest.addons));
|
||||
const httpPathsChanged = app.manifest.httpPaths !== updateConfig.manifest.httpPaths;
|
||||
const httpPortChanged = app.manifest.httpPort !== updateConfig.manifest.httpPort;
|
||||
const proxyAuthChanged = !_.isEqual(safe.query(app.manifest, 'addons.proxyAuth'), safe.query(updateConfig.manifest, 'addons.proxyAuth'));
|
||||
|
||||
@@ -630,7 +629,7 @@ async function update(app, args, progressCallback) {
|
||||
await startApp(app);
|
||||
|
||||
await progressCallback({ percent: 90, message: 'Configuring reverse proxy' });
|
||||
if (httpPathsChanged || proxyAuthChanged || httpPortChanged) {
|
||||
if (proxyAuthChanged || httpPortChanged) {
|
||||
await reverseProxy.configureApp(app, AuditSource.APPTASK);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,5 +19,6 @@ AuditSource.HEALTH_MONITOR = new AuditSource('healthmonitor');
|
||||
AuditSource.EXTERNAL_LDAP_TASK = new AuditSource('externalldap');
|
||||
AuditSource.EXTERNAL_LDAP_AUTO_CREATE = new AuditSource('externalldap');
|
||||
AuditSource.APPTASK = new AuditSource('apptask');
|
||||
AuditSource.PLATFORM = new AuditSource('platform');
|
||||
|
||||
exports = module.exports = AuditSource;
|
||||
|
||||
+27
-1
@@ -152,6 +152,29 @@ async function cleanupAppBackups(backupConfig, referencedAppBackupIds, progressC
|
||||
return removedAppBackupIds;
|
||||
}
|
||||
|
||||
async function cleanupMailBackups(backupConfig, referencedAppBackupIds, progressCallback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert(Array.isArray(referencedAppBackupIds));
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
|
||||
let removedMailBackupIds = [];
|
||||
|
||||
const mailBackups = await backups.getByTypePaged(backups.BACKUP_TYPE_MAIL, 1, 1000);
|
||||
|
||||
applyBackupRetentionPolicy(mailBackups, _.extend({ keepLatest: true }, backupConfig.retentionPolicy), referencedAppBackupIds);
|
||||
|
||||
for (const mailBackup of mailBackups) {
|
||||
if (mailBackup.keepReason) continue;
|
||||
await progressCallback({ message: `Removing mail backup ${mailBackup.id}`});
|
||||
removedMailBackupIds.push(mailBackup.id);
|
||||
await cleanupBackup(backupConfig, mailBackup, progressCallback); // never errors
|
||||
}
|
||||
|
||||
debug('cleanupMailBackups: done');
|
||||
|
||||
return removedMailBackupIds;
|
||||
}
|
||||
|
||||
async function cleanupBoxBackups(backupConfig, progressCallback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
@@ -267,6 +290,9 @@ async function run(progressCallback) {
|
||||
await progressCallback({ percent: 10, message: 'Cleaning box backups' });
|
||||
const { removedBoxBackupIds, referencedAppBackupIds } = await cleanupBoxBackups(backupConfig, progressCallback);
|
||||
|
||||
await progressCallback({ percent: 20, message: 'Cleaning mail backups' });
|
||||
const removedMailBackupIds = await cleanupMailBackups(backupConfig, referencedAppBackupIds, progressCallback);
|
||||
|
||||
await progressCallback({ percent: 40, message: 'Cleaning app backups' });
|
||||
const removedAppBackupIds = await cleanupAppBackups(backupConfig, referencedAppBackupIds, progressCallback);
|
||||
|
||||
@@ -276,5 +302,5 @@ async function run(progressCallback) {
|
||||
await progressCallback({ percent: 90, message: 'Cleaning snapshots' });
|
||||
await cleanupSnapshots(backupConfig);
|
||||
|
||||
return { removedBoxBackupIds, removedAppBackupIds, missingBackupIds };
|
||||
return { removedBoxBackupIds, removedMailBackupIds, removedAppBackupIds, missingBackupIds };
|
||||
}
|
||||
|
||||
+3
-2
@@ -193,7 +193,7 @@ async function startBackupTask(auditSource) {
|
||||
|
||||
const backupConfig = await settings.getBackupConfig();
|
||||
|
||||
const memoryLimit = 'memoryLimit' in backupConfig ? Math.max(backupConfig.memoryLimit/1024/1024, 400) : 400;
|
||||
const memoryLimit = 'memoryLimit' in backupConfig ? Math.max(backupConfig.memoryLimit/1024/1024, 800) : 800;
|
||||
|
||||
const taskId = await tasks.add(tasks.TASK_BACKUP, [ { /* options */ } ]);
|
||||
|
||||
@@ -268,11 +268,12 @@ async function startCleanupTask(auditSource) {
|
||||
|
||||
const taskId = await tasks.add(tasks.TASK_CLEAN_BACKUPS, []);
|
||||
|
||||
tasks.startTask(taskId, {}, (error, result) => { // result is { removedBoxBackupIds, removedAppBackupIds, missingBackupIds }
|
||||
tasks.startTask(taskId, {}, (error, result) => { // result is { removedBoxBackupIds, removedAppBackupIds, removedMailBackupIds, missingBackupIds }
|
||||
eventlog.add(eventlog.ACTION_BACKUP_CLEANUP_FINISH, auditSource, {
|
||||
taskId,
|
||||
errorMessage: error ? error.message : null,
|
||||
removedBoxBackupIds: result ? result.removedBoxBackupIds : [],
|
||||
removedMailBackupIds: result ? result.removedMailBackupIds : [],
|
||||
removedAppBackupIds: result ? result.removedAppBackupIds : [],
|
||||
missingBackupIds: result ? result.missingBackupIds : []
|
||||
});
|
||||
|
||||
+6
-6
@@ -1017,10 +1017,9 @@ async function downloadMail(restoreConfig, progressCallback) {
|
||||
const dataLayout = new DataLayout(mailDataDir, []);
|
||||
|
||||
const startTime = new Date();
|
||||
const backupConfig = restoreConfig.backupConfig || await settings.getBackupConfig();
|
||||
|
||||
const downloadAsync = util.promisify(download);
|
||||
await downloadAsync(backupConfig, restoreConfig.backupId, restoreConfig.backupFormat, dataLayout, progressCallback);
|
||||
await downloadAsync(restoreConfig.backupConfig, restoreConfig.backupId, restoreConfig.backupFormat, dataLayout, progressCallback);
|
||||
debug('downloadMail: time: %s', (new Date() - startTime)/1000);
|
||||
}
|
||||
|
||||
@@ -1037,13 +1036,14 @@ async function fullBackup(options, progressCallback) {
|
||||
let step = 100/(allApps.length+3);
|
||||
|
||||
const appBackupIds = [];
|
||||
for (const app of allApps) {
|
||||
progressCallback({ percent: percent, message: `Backing up ${app.fqdn}` });
|
||||
for (let i = 0; i < allApps.length; i++) {
|
||||
const app = allApps[i];
|
||||
progressCallback({ percent: percent, message: `Backing up ${app.fqdn} (${i+1}/${allApps.length})` });
|
||||
percent += step;
|
||||
|
||||
if (!app.enableBackup) {
|
||||
debug(`fullBackup: skipped backup ${app.fqdn}`);
|
||||
return; // nothing to backup
|
||||
debug(`fullBackup: skipped backup ${app.fqdn} (${i+1}/${allApps.length})`);
|
||||
continue; // nothing to backup
|
||||
}
|
||||
|
||||
const startTime = new Date();
|
||||
|
||||
+1
-58
@@ -7,11 +7,8 @@ exports = module.exports = {
|
||||
set,
|
||||
del,
|
||||
|
||||
initSecrets,
|
||||
|
||||
ACME_ACCOUNT_KEY: 'acme_account_key',
|
||||
ADDON_TURN_SECRET: 'addon_turn_secret',
|
||||
DHPARAMS: 'dhparams',
|
||||
SFTP_PUBLIC_KEY: 'sftp_public_key',
|
||||
SFTP_PRIVATE_KEY: 'sftp_private_key',
|
||||
|
||||
@@ -21,13 +18,7 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
const assert = require('assert'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
constants = require('./constants.js'),
|
||||
crypto = require('crypto'),
|
||||
database = require('./database.js'),
|
||||
debug = require('debug')('box:blobs'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance');
|
||||
database = require('./database.js');
|
||||
|
||||
const BLOBS_FIELDS = [ 'id', 'value' ].join(',');
|
||||
|
||||
@@ -53,51 +44,3 @@ async function del(id) {
|
||||
async function clear() {
|
||||
await database.query('DELETE FROM blobs');
|
||||
}
|
||||
|
||||
async function initSecrets() {
|
||||
let acmeAccountKey = await get(exports.ACME_ACCOUNT_KEY);
|
||||
if (!acmeAccountKey) {
|
||||
acmeAccountKey = safe.child_process.execSync('openssl genrsa 4096');
|
||||
if (!acmeAccountKey) throw new BoxError(BoxError.OPENSSL_ERROR, `Could not generate acme account key: ${safe.error.message}`);
|
||||
await set(exports.ACME_ACCOUNT_KEY, acmeAccountKey);
|
||||
}
|
||||
|
||||
let turnSecret = await get(exports.ADDON_TURN_SECRET);
|
||||
if (!turnSecret) {
|
||||
turnSecret = 'a' + crypto.randomBytes(15).toString('hex'); // prefix with a to ensure string starts with a letter
|
||||
await set(exports.ADDON_TURN_SECRET, Buffer.from(turnSecret));
|
||||
}
|
||||
|
||||
// TODO maybe skip this in tests if possible again
|
||||
let dhparams = await get(exports.DHPARAMS);
|
||||
if (!dhparams) {
|
||||
debug('initSecrets: generating dhparams.pem. this takes forever');
|
||||
if (constants.TEST) dhparams = safe.fs.readFileSync('/tmp/dhparams.pem');
|
||||
if (!dhparams) dhparams = safe.child_process.execSync('openssl dhparam 2048');
|
||||
if (!dhparams) throw new BoxError(BoxError.OPENSSL_ERROR, safe.error);
|
||||
if (constants.TEST) safe.fs.writeFileSync('/tmp/dhparams.pem', dhparams);
|
||||
if (!safe.fs.writeFileSync(paths.DHPARAMS_FILE, dhparams)) throw new BoxError(BoxError.FS_ERROR, `Could not save dhparams.pem: ${safe.error.message}`);
|
||||
await set(exports.DHPARAMS, dhparams);
|
||||
} else if (!safe.fs.existsSync(paths.DHPARAMS_FILE)) {
|
||||
if (!safe.fs.writeFileSync(paths.DHPARAMS_FILE, dhparams)) throw new BoxError(BoxError.FS_ERROR, `Could not save dhparams.pem: ${safe.error.message}`);
|
||||
}
|
||||
|
||||
let sftpPrivateKey = await get(exports.SFTP_PRIVATE_KEY);
|
||||
let sftpPublicKey = await get(exports.SFTP_PUBLIC_KEY);
|
||||
|
||||
if (!sftpPrivateKey || !sftpPublicKey) {
|
||||
debug('initSecrets: generate sftp keys');
|
||||
if (constants.TEST) {
|
||||
safe.fs.unlinkSync(paths.SFTP_PUBLIC_KEY_FILE);
|
||||
safe.fs.unlinkSync(paths.SFTP_PRIVATE_KEY_FILE);
|
||||
}
|
||||
if (!safe.child_process.execSync(`ssh-keygen -m PEM -t rsa -f "${paths.SFTP_KEYS_DIR}/ssh_host_rsa_key" -q -N ""`)) throw new BoxError(BoxError.OPENSSL_ERROR, `Could not generate sftp ssh keys: ${safe.error.message}`);
|
||||
sftpPublicKey = safe.fs.readFileSync(paths.SFTP_PUBLIC_KEY_FILE);
|
||||
await set(exports.SFTP_PUBLIC_KEY, sftpPublicKey);
|
||||
sftpPrivateKey = safe.fs.readFileSync(paths.SFTP_PRIVATE_KEY_FILE);
|
||||
await set(exports.SFTP_PRIVATE_KEY, sftpPrivateKey);
|
||||
} else if (!safe.fs.existsSync(paths.SFTP_PUBLIC_KEY_FILE) || !safe.fs.existsSync(paths.SFTP_PRIVATE_KEY_FILE)) {
|
||||
if (!safe.fs.writeFileSync(paths.SFTP_PUBLIC_KEY_FILE, sftpPublicKey)) throw new BoxError(BoxError.FS_ERROR, `Could not save sftp public key: ${safe.error.message}`);
|
||||
if (!safe.fs.writeFileSync(paths.SFTP_PRIVATE_KEY_FILE, sftpPrivateKey)) throw new BoxError(BoxError.FS_ERROR, `Could not save sftp private key: ${safe.error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ function BoxError(reason, errorOrMessage, override) {
|
||||
}
|
||||
util.inherits(BoxError, Error);
|
||||
BoxError.ACCESS_DENIED = 'Access Denied';
|
||||
BoxError.ACME_ERROR = 'Acme Error';
|
||||
BoxError.ADDONS_ERROR = 'Addons Error';
|
||||
BoxError.ALREADY_EXISTS = 'Already Exists';
|
||||
BoxError.BAD_FIELD = 'Bad Field';
|
||||
@@ -91,6 +92,7 @@ BoxError.toHttpError = function (error) {
|
||||
case BoxError.INVALID_CREDENTIALS:
|
||||
return new HttpError(412, error);
|
||||
case BoxError.EXTERNAL_ERROR:
|
||||
case BoxError.ACME_ERROR:
|
||||
case BoxError.NETWORK_ERROR:
|
||||
case BoxError.FS_ERROR:
|
||||
case BoxError.MOUNT_ERROR:
|
||||
|
||||
+1
-21
@@ -10,7 +10,6 @@ exports = module.exports = {
|
||||
isRebootRequired,
|
||||
|
||||
onActivated,
|
||||
onRestored,
|
||||
|
||||
setupDnsAndCert,
|
||||
|
||||
@@ -28,7 +27,6 @@ const apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
AuditSource = require('./auditsource.js'),
|
||||
backups = require('./backups.js'),
|
||||
backuptask = require('./backuptask.js'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
branding = require('./branding.js'),
|
||||
constants = require('./constants.js'),
|
||||
@@ -58,7 +56,7 @@ const apps = require('./apps.js'),
|
||||
const REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh');
|
||||
|
||||
async function initialize() {
|
||||
safe(runStartupTasks(), { debug });
|
||||
safe(runStartupTasks(), { debug }); // background
|
||||
|
||||
await notifyUpdate();
|
||||
}
|
||||
@@ -85,24 +83,6 @@ async function onActivated(options) {
|
||||
await reverseProxy.writeDefaultConfig({ activated :true });
|
||||
}
|
||||
|
||||
async function onRestored(options) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
|
||||
debug('onRestored: downloading mail');
|
||||
|
||||
const [error, results] = await safe(backups.getByIdentifierAndStatePaged(backups.BACKUP_IDENTIFIER_MAIL, backups.BACKUP_STATE_NORMAL, 1, 1));
|
||||
if (error || results.length == 0) {
|
||||
debug('startMail: could not find backup to restore', error, results);
|
||||
} else {
|
||||
debug(`startMail: downloading backup ${results[0].id}`);
|
||||
const restoreConfig = { backupId: results[0].id, backupFormat: results[0].format };
|
||||
// have to wait for download before starting mail container because we download as non-root user
|
||||
await backuptask.downloadMail(restoreConfig, (progress) => debug(`startMail: ${progress.message}`));
|
||||
}
|
||||
|
||||
await onActivated(options);
|
||||
}
|
||||
|
||||
async function notifyUpdate() {
|
||||
const version = safe.fs.readFileSync(paths.VERSION_FILE, 'utf8');
|
||||
if (version === constants.VERSION) return;
|
||||
|
||||
+2
-1
@@ -46,6 +46,7 @@ exports = module.exports = {
|
||||
'io.github.sickchill.cloudronapp',
|
||||
'to.couchpota.cloudronapp'
|
||||
],
|
||||
DEMO_APP_LIMIT: 20,
|
||||
|
||||
AUTOUPDATE_PATTERN_NEVER: 'never',
|
||||
|
||||
@@ -63,6 +64,6 @@ exports = module.exports = {
|
||||
|
||||
FOOTER: '© %YEAR% [Cloudron](https://cloudron.io) [Forum <i class="fa fa-comments"></i>](https://forum.cloudron.io)',
|
||||
|
||||
VERSION: process.env.BOX_ENV === 'cloudron' ? fs.readFileSync(path.join(__dirname, '../VERSION'), 'utf8').trim() : '6.3.0-test'
|
||||
VERSION: process.env.BOX_ENV === 'cloudron' ? fs.readFileSync(path.join(__dirname, '../VERSION'), 'utf8').trim() : '7.0.0-test'
|
||||
};
|
||||
|
||||
|
||||
+2
-3
@@ -15,7 +15,6 @@ exports = module.exports = {
|
||||
const assert = require('assert'),
|
||||
async = require('async'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
child_process = require('child_process'),
|
||||
constants = require('./constants.js'),
|
||||
debug = require('debug')('box:database'),
|
||||
mysql = require('mysql'),
|
||||
@@ -89,7 +88,7 @@ async function query() {
|
||||
let args = Array.prototype.slice.call(arguments);
|
||||
|
||||
args.push(function queryCallback(error, result) {
|
||||
if (error) return reject(new BoxError(BoxError.DATABASE_ERROR, error, { code: error.code, sqlMessage: error.sqlMessage }));
|
||||
if (error) return reject(new BoxError(BoxError.DATABASE_ERROR, error, { code: error.code, sqlMessage: error.sqlMessage || null }));
|
||||
|
||||
resolve(result);
|
||||
});
|
||||
@@ -107,7 +106,7 @@ async function transaction(queries) {
|
||||
|
||||
const releaseConnection = (error) => {
|
||||
connection.release();
|
||||
reject(new BoxError(BoxError.DATABASE_ERROR, error, { code: error.code, sqlMessage: error.sqlMessage }));
|
||||
reject(new BoxError(BoxError.DATABASE_ERROR, error, { code: error.code, sqlMessage: error.sqlMessage || null }));
|
||||
};
|
||||
|
||||
connection.beginTransaction(function (error) {
|
||||
|
||||
+6
-6
@@ -254,9 +254,8 @@ async function search(identifier) {
|
||||
return users;
|
||||
}
|
||||
|
||||
async function maybeCreateUser(identifier, password) {
|
||||
async function maybeCreateUser(identifier) {
|
||||
assert.strictEqual(typeof identifier, 'string');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
|
||||
const externalLdapConfig = await settings.getExternalLdapConfig();
|
||||
if (externalLdapConfig.provider === 'noop') throw new BoxError(BoxError.BAD_STATE, 'not enabled');
|
||||
@@ -269,13 +268,14 @@ async function maybeCreateUser(identifier, password) {
|
||||
const user = translateUser(externalLdapConfig, ldapUsers[0]);
|
||||
if (!validUserRequirements(user)) throw new BoxError(BoxError.BAD_FIELD);
|
||||
|
||||
const [error] = await safe(users.add(user.email, { username: user.username, password: null, displayName: user.displayName, source: 'ldap' }, AuditSource.EXTERNAL_LDAP_AUTO_CREATE));
|
||||
const [error, userId] = await safe(users.add(user.email, { username: user.username, password: null, displayName: user.displayName, source: 'ldap' }, AuditSource.EXTERNAL_LDAP_AUTO_CREATE));
|
||||
if (error) {
|
||||
debug(`maybeCreateUser: failed to auto create user ${user.username}`, error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return user;
|
||||
// fetch the full record
|
||||
return await users.get(userId);
|
||||
}
|
||||
|
||||
async function verifyPassword(user, password) {
|
||||
@@ -291,7 +291,7 @@ async function verifyPassword(user, password) {
|
||||
|
||||
const client = await getClient(externalLdapConfig, { bind: false });
|
||||
|
||||
const [error] = await safe(util.promisify(client.bind)(ldapUsers[0].dn, password));
|
||||
const [error] = await safe(util.promisify(client.bind.bind(client))(ldapUsers[0].dn, password));
|
||||
client.unbind();
|
||||
if (error instanceof ldap.InvalidCredentialsError) throw new BoxError(BoxError.INVALID_CREDENTIALS);
|
||||
if (error) throw new BoxError(BoxError.EXTERNAL_ERROR, error);
|
||||
@@ -337,7 +337,7 @@ async function syncUsers(externalLdapConfig, progressCallback) {
|
||||
debug(`syncUsers: [adding user] username=${ldapUser.username} email=${ldapUser.email} displayName=${ldapUser.displayName}`);
|
||||
|
||||
const [userAddError] = await safe(users.add(ldapUser.email, { username: ldapUser.username, password: null, displayName: ldapUser.displayName, source: 'ldap' }, AuditSource.EXTERNAL_LDAP_TASK));
|
||||
if (userAddError) debug('syncUsers: Failed to create user', ldapUser, userAddError.message);
|
||||
if (userAddError) debug('syncUsers: Failed to create user', ldapUser, userAddError);
|
||||
} else if (user.source !== 'ldap') {
|
||||
debug(`syncUsers: [mapping user] username=${ldapUser.username} email=${ldapUser.email} displayName=${ldapUser.displayName}`);
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ exports = module.exports = {
|
||||
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:4.1.1@sha256:86e4e2f4fd43809efca7c9cb1def4d7608cf36cb9ea27052f9b64da4481db43a' },
|
||||
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:4.0.2@sha256:9df297ccc3370f38c54f8d614e214e082b363777cd1c6c9522e29663cc8f5362' },
|
||||
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:3.0.4@sha256:5c60de75d078ae609da5565f32dcd91030f45907e945756cc976ff207b8c6199' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:3.4.0@sha256:5f9795cad3634c177f789019c12f8d53a6481de3cc627fb5a4866ce085006507' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:3.5.0@sha256:e05d328ea1afa94e31e2eae9b035ff2edde8b90cae902ca49e06053b5bdb5fde' },
|
||||
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:3.0.1@sha256:bed9f6b5d06fe2c5289e895e806cfa5b74ad62993d705be55d4554a67d128029' },
|
||||
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:3.4.1@sha256:13e066fcd52230f23244c16fdd2f7aa447a91e98ff703269f48b1afe3b393e31' }
|
||||
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:3.4.2@sha256:810306478c3dac7caa7497e5f6381cc7ce2f68aafda849a4945d39a67cc04bc1' }
|
||||
}
|
||||
};
|
||||
|
||||
+27
-87
@@ -278,40 +278,6 @@ async function mailboxSearch(req, res, next) {
|
||||
} else {
|
||||
res.end();
|
||||
}
|
||||
} else if (req.dn.rdns[0].attrs.domain) { // legacy ldap mailbox search for old sogo
|
||||
const domain = req.dn.rdns[0].attrs.domain.value.toLowerCase();
|
||||
|
||||
let [error, mailboxes] = await safe(mail.listMailboxes(domain, 1, 1000));
|
||||
if (error) return next(new ldap.OperationsError(error.toString()));
|
||||
|
||||
mailboxes = mailboxes.filter(m => m.active);
|
||||
|
||||
let results = [];
|
||||
|
||||
// send mailbox objects
|
||||
mailboxes.forEach(function (mailbox) {
|
||||
var dn = ldap.parseDN(`cn=${mailbox.name}@${domain},domain=${domain},ou=mailboxes,dc=cloudron`);
|
||||
|
||||
var obj = {
|
||||
dn: dn.toString(),
|
||||
attributes: {
|
||||
objectclass: ['mailbox'],
|
||||
objectcategory: 'mailbox',
|
||||
cn: `${mailbox.name}@${domain}`,
|
||||
uid: `${mailbox.name}@${domain}`,
|
||||
mail: `${mailbox.name}@${domain}`
|
||||
}
|
||||
};
|
||||
|
||||
// ensure all filter values are also lowercase
|
||||
var lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null);
|
||||
if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString()));
|
||||
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) {
|
||||
results.push(obj);
|
||||
}
|
||||
});
|
||||
|
||||
finalSend(results, req, res, next);
|
||||
} else { // new sogo
|
||||
let [error, mailboxes] = await safe(mail.listAllMailboxes(1, 1000));
|
||||
if (error) return next(new ldap.OperationsError(error.toString()));
|
||||
@@ -500,36 +466,6 @@ async function verifyMailboxPassword(mailbox, password) {
|
||||
return verifiedUser;
|
||||
}
|
||||
|
||||
async function authenticateUserMailbox(req, res, next) {
|
||||
debug('user mailbox auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
|
||||
|
||||
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
const email = req.dn.rdns[0].attrs.cn.value.toLowerCase();
|
||||
const parts = email.split('@');
|
||||
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
const [error, domain] = await safe(mail.getDomain(parts[1]));
|
||||
if (error) return next(new ldap.OperationsError(error.message));
|
||||
if (!domain) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
if (!domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
const [getMailboxError, mailbox] = await safe(mail.getMailbox(parts[0], parts[1]));
|
||||
if (getMailboxError) return next(new ldap.OperationsError(getMailboxError.message));
|
||||
if (!mailbox) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
if (!mailbox.active) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
const [verifyError, result] = await safe(verifyMailboxPassword(mailbox, req.credentials || ''));
|
||||
if (verifyError && verifyError.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
if (verifyError && verifyError.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
|
||||
if (verifyError) return next(new ldap.OperationsError(verifyError.message));
|
||||
|
||||
eventlog.upsertLoginEvent(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) });
|
||||
|
||||
res.end();
|
||||
}
|
||||
|
||||
async function authenticateSftp(req, res, next) {
|
||||
debug('sftp auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
|
||||
|
||||
@@ -607,46 +543,43 @@ async function verifyAppMailboxPassword(serviceId, username, password) {
|
||||
if (!result.some(r => r.name.endsWith(`${pattern}_USERNAME`) && r.value === username)) throw new BoxError(BoxError.INVALID_CREDENTIALS);
|
||||
}
|
||||
|
||||
async function authenticateMail(req, res, next) {
|
||||
debug('authenticateMail: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
|
||||
async function authenticateService(serviceId, dn, req, res, next) {
|
||||
debug(`authenticateService: ${req.dn.toString()} (from ${req.connection.ldap.id})`);
|
||||
|
||||
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
if (!dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(dn.toString()));
|
||||
|
||||
const email = req.dn.rdns[0].attrs.cn.value.toLowerCase();
|
||||
const email = dn.rdns[0].attrs.cn.value.toLowerCase();
|
||||
const parts = email.split('@');
|
||||
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(dn.toString()));
|
||||
|
||||
const knownServices = [ 'msa', 'imap', 'pop3', 'sieve' ];
|
||||
const serviceId = req.dn.rdns[1].attrs.ou.value.toLowerCase();
|
||||
const knownServices = [ 'msa', 'imap', 'pop3', 'sieve', 'sogo' ];
|
||||
if (!knownServices.includes(serviceId)) return next(new ldap.OperationsError('Invalid DN. Unknown service'));
|
||||
|
||||
const [error, domain] = await safe(mail.getDomain(parts[1]));
|
||||
if (error) return next(new ldap.OperationsError(error.message));
|
||||
if (!domain) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
if (!domain) return next(new ldap.NoSuchObjectError(dn.toString()));
|
||||
|
||||
const serviceNeedsMailbox = serviceId === 'imap' || serviceId === 'sieve' || serviceId === 'pop3';
|
||||
if (serviceNeedsMailbox && !domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
const serviceNeedsMailbox = serviceId === 'imap' || serviceId === 'sieve' || serviceId === 'pop3' || serviceId === 'sogo';
|
||||
if (serviceNeedsMailbox && !domain.enabled) return next(new ldap.NoSuchObjectError(dn.toString()));
|
||||
|
||||
const [getMailboxError, mailbox] = await safe(mail.getMailbox(parts[0], parts[1]));
|
||||
if (getMailboxError) return next(new ldap.OperationsError(getMailboxError.message));
|
||||
if (serviceId === 'pop3' && !mailbox.enablePop3) return next(new ldap.OperationsError('POP3 is not enabled'));
|
||||
|
||||
const [appPasswordError] = await safe(verifyAppMailboxPassword(serviceId, email, req.credentials || ''));
|
||||
if (!appPasswordError) { // validated as app
|
||||
if (serviceNeedsMailbox && (!mailbox || !mailbox.active)) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
return res.end();
|
||||
if (serviceNeedsMailbox) {
|
||||
if (!mailbox || !mailbox.active) return next(new ldap.NoSuchObjectError(dn.toString()));
|
||||
if (serviceId === 'pop3' && !mailbox.enablePop3) return next(new ldap.OperationsError('POP3 is not enabled'));
|
||||
}
|
||||
|
||||
if (appPasswordError && appPasswordError.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
|
||||
if (appPasswordError && appPasswordError.reason !== BoxError.NOT_FOUND) return next(new ldap.OperationsError(appPasswordError.message));
|
||||
const [appPasswordError] = await safe(verifyAppMailboxPassword(serviceId, email, req.credentials || ''));
|
||||
if (!appPasswordError) return res.end(); // validated as app
|
||||
|
||||
// user password check requires an active mailbox
|
||||
if (!mailbox) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
if (!mailbox.active) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
if (appPasswordError.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(dn.toString()));
|
||||
if (appPasswordError.reason !== BoxError.NOT_FOUND) return next(new ldap.OperationsError(appPasswordError.message));
|
||||
|
||||
if (!mailbox || !mailbox.active) return next(new ldap.NoSuchObjectError(dn.toString())); // user auth requires active mailbox
|
||||
const [verifyError, result] = await safe(verifyMailboxPassword(mailbox, req.credentials || ''));
|
||||
if (verifyError && verifyError.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
if (verifyError && verifyError.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
|
||||
if (verifyError && verifyError.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(dn.toString()));
|
||||
if (verifyError && verifyError.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(dn.toString()));
|
||||
if (verifyError) return next(new ldap.OperationsError(verifyError.message));
|
||||
|
||||
eventlog.upsertLoginEvent(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) });
|
||||
@@ -654,6 +587,11 @@ async function authenticateMail(req, res, next) {
|
||||
res.end();
|
||||
}
|
||||
|
||||
async function authenticateMail(req, res, next) {
|
||||
if (!req.dn.rdns[1].attrs.ou) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
await authenticateService(req.dn.rdns[1].attrs.ou.value.toLowerCase(), req.dn, req, res, next);
|
||||
}
|
||||
|
||||
async function start() {
|
||||
const logger = {
|
||||
trace: NOOP,
|
||||
@@ -676,7 +614,9 @@ async function start() {
|
||||
|
||||
// http://www.ietf.org/proceedings/43/I-D/draft-srivastava-ldap-mail-00.txt
|
||||
gServer.search('ou=mailboxes,dc=cloudron', mailboxSearch); // haraka (address translation), dovecot (LMTP), sogo (mailbox search)
|
||||
gServer.bind('ou=mailboxes,dc=cloudron', authenticateUserMailbox); // apps like sogo can use domain=${domain} to authenticate a mailbox
|
||||
gServer.bind('ou=mailboxes,dc=cloudron', async function (req, res, next) { // used for sogo only. this route happens only at sogo login time. after that it will use imap ldap route
|
||||
await authenticateService('sogo', req.dn, req, res, next);
|
||||
});
|
||||
gServer.search('ou=mailaliases,dc=cloudron', mailAliasSearch); // haraka
|
||||
gServer.search('ou=mailinglists,dc=cloudron', mailingListSearch); // haraka
|
||||
|
||||
|
||||
+3
-2
@@ -106,6 +106,7 @@ const assert = require('assert'),
|
||||
const DNS_OPTIONS = { timeout: 5000 };
|
||||
const REMOVE_MAILBOX_CMD = path.join(__dirname, 'scripts/rmmailbox.sh');
|
||||
|
||||
// if you add a field here, listMailboxes has to be updated
|
||||
const MAILBOX_FIELDS = [ 'name', 'type', 'ownerId', 'ownerType', 'aliasName', 'aliasDomain', 'creationTime', 'membersJson', 'membersOnly', 'domain', 'active', 'enablePop3' ].join(',');
|
||||
const MAILDB_FIELDS = [ 'domain', 'enabled', 'mailFromValidation', 'catchAllJson', 'relayJson', 'dkimKeyJson', 'dkimSelector', 'bannerJson' ].join(',');
|
||||
|
||||
@@ -1080,7 +1081,7 @@ async function listMailboxes(domain, search, page, perPage) {
|
||||
const escapedSearch = mysql.escape('%' + search + '%'); // this also quotes the string
|
||||
const searchQuery = search ? ` HAVING (name LIKE ${escapedSearch} OR aliasNames LIKE ${escapedSearch} OR aliasDomains LIKE ${escapedSearch})` : ''; // having instead of where because of aggregated columns use
|
||||
|
||||
const query = 'SELECT m1.name AS name, m1.domain AS domain, m1.ownerId AS ownerId, m1.ownerType as ownerType, m1.active as active, JSON_ARRAYAGG(m2.name) AS aliasNames, JSON_ARRAYAGG(m2.domain) AS aliasDomains '
|
||||
const query = 'SELECT m1.name AS name, m1.domain AS domain, m1.ownerId AS ownerId, m1.ownerType as ownerType, m1.active as active, JSON_ARRAYAGG(m2.name) AS aliasNames, JSON_ARRAYAGG(m2.domain) AS aliasDomains, m1.enablePop3 AS enablePop3 '
|
||||
+ ` FROM (SELECT * FROM mailboxes WHERE type='${exports.TYPE_MAILBOX}') AS m1`
|
||||
+ ` LEFT JOIN (SELECT * FROM mailboxes WHERE type='${exports.TYPE_ALIAS}') AS m2`
|
||||
+ ' ON m1.name=m2.aliasName AND m1.domain=m2.aliasDomain AND m1.ownerId=m2.ownerId'
|
||||
@@ -1101,7 +1102,7 @@ async function listAllMailboxes(page, perPage) {
|
||||
assert.strictEqual(typeof page, 'number');
|
||||
assert.strictEqual(typeof perPage, 'number');
|
||||
|
||||
const query = 'SELECT m1.name AS name, m1.domain AS domain, m1.ownerId AS ownerId, m1.ownerType as ownerType, m1.active as active, JSON_ARRAYAGG(m2.name) AS aliasNames, JSON_ARRAYAGG(m2.domain) AS aliasDomains '
|
||||
const query = 'SELECT m1.name AS name, m1.domain AS domain, m1.ownerId AS ownerId, m1.ownerType as ownerType, m1.active as active, JSON_ARRAYAGG(m2.name) AS aliasNames, JSON_ARRAYAGG(m2.domain) AS aliasDomains, m1.enablePop3 AS enablePop3 '
|
||||
+ ` FROM (SELECT * FROM mailboxes WHERE type='${exports.TYPE_MAILBOX}') AS m1`
|
||||
+ ` LEFT JOIN (SELECT * FROM mailboxes WHERE type='${exports.TYPE_ALIAS}') AS m2`
|
||||
+ ' ON m1.name=m2.aliasName AND m1.domain=m2.aliasDomain AND m1.ownerId=m2.ownerId'
|
||||
|
||||
+9
-6
@@ -61,7 +61,7 @@ async function sendMail(mailOptions) {
|
||||
}
|
||||
}));
|
||||
|
||||
const transportSendMail = util.promisify(transport.sendMail).bind(transport);
|
||||
const transportSendMail = util.promisify(transport.sendMail.bind(transport));
|
||||
const [error] = await safe(transportSendMail(mailOptions));
|
||||
if (error) throw new BoxError(BoxError.EXTERNAL_ERROR, error);
|
||||
debug(`Email "${mailOptions.subject}" sent to ${mailOptions.to}`);
|
||||
@@ -89,9 +89,10 @@ function render(templateFile, params, translationAssets) {
|
||||
return content;
|
||||
}
|
||||
|
||||
async function sendInvite(user, invitor, inviteLink) {
|
||||
async function sendInvite(user, invitor, email, inviteLink) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof invitor, 'object');
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
assert.strictEqual(typeof inviteLink, 'string');
|
||||
|
||||
const mailConfig = await getMailConfig();
|
||||
@@ -108,7 +109,7 @@ async function sendInvite(user, invitor, inviteLink) {
|
||||
|
||||
const mailOptions = {
|
||||
from: mailConfig.notificationFrom,
|
||||
to: user.fallbackEmail,
|
||||
to: email,
|
||||
subject: ejs.render(translation.translate('{{ welcomeEmail.subject }}', translationAssets.translations || {}, translationAssets.fallback || {}), { cloudron: mailConfig.cloudronName }),
|
||||
text: render('welcome_user-text.ejs', templateData, translationAssets),
|
||||
html: render('welcome_user-html.ejs', templateData, translationAssets)
|
||||
@@ -143,7 +144,7 @@ async function sendNewLoginLocation(user, loginLocation) {
|
||||
|
||||
const mailOptions = {
|
||||
from: mailConfig.notificationFrom,
|
||||
to: user.fallbackEmail,
|
||||
to: user.email,
|
||||
subject: ejs.render(translation.translate('{{ newLoginEmail.subject }}', translationAssets.translations || {}, translationAssets.fallback || {}), { cloudron: mailConfig.cloudronName }),
|
||||
text: render('new_login_location-text.ejs', templateData, translationAssets),
|
||||
html: render('new_login_location-html.ejs', templateData, translationAssets)
|
||||
@@ -152,8 +153,10 @@ async function sendNewLoginLocation(user, loginLocation) {
|
||||
await sendMail(mailOptions);
|
||||
}
|
||||
|
||||
async function passwordReset(user, resetLink) {
|
||||
async function passwordReset(user, email, resetLink) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
assert.strictEqual(typeof resetLink, 'string');
|
||||
|
||||
const mailConfig = await getMailConfig();
|
||||
const translationAssets = await translation.getTranslations();
|
||||
@@ -167,7 +170,7 @@ async function passwordReset(user, resetLink) {
|
||||
|
||||
const mailOptions = {
|
||||
from: mailConfig.notificationFrom,
|
||||
to: user.fallbackEmail,
|
||||
to: email,
|
||||
subject: ejs.render(translation.translate('{{ passwordResetEmail.subject }}', translationAssets.translations || {}, translationAssets.fallback || {}), { cloudron: mailConfig.cloudronName }),
|
||||
text: render('password_reset-text.ejs', templateData, translationAssets),
|
||||
html: render('password_reset-html.ejs', templateData, translationAssets)
|
||||
|
||||
+1
-1
@@ -150,7 +150,7 @@ async function getStatus(mountType, hostPath) {
|
||||
let start = -1, end = -1; // start and end of error message block
|
||||
for (let idx = lines.length - 1; idx >= 0; idx--) { // reverse
|
||||
const line = lines[idx];
|
||||
const match = line['SYSLOG_IDENTIFIER'] === 'mount' || line['_EXE'].includes('mount') || line['_COMM'].includes('mount');
|
||||
const match = line['SYSLOG_IDENTIFIER'] === 'mount' || (line['_EXE'] && line['_EXE'].includes('mount')) || (line['_COMM'] && line['_COMM'].includes('mount'));
|
||||
if (match) {
|
||||
if (end === -1) end = idx;
|
||||
start = idx;
|
||||
|
||||
+4
-8
@@ -87,7 +87,10 @@ server {
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256;
|
||||
ssl_prefer_server_ciphers off;
|
||||
|
||||
<% if (endpoint !== 'ip' && endpoint !== 'setup') { -%>
|
||||
# dhparams is generated only after dns setup
|
||||
ssl_dhparam /home/yellowtent/platformdata/dhparams.pem;
|
||||
<% } -%>
|
||||
add_header Strict-Transport-Security "max-age=63072000";
|
||||
|
||||
<% if ( ocsp ) { -%>
|
||||
@@ -214,7 +217,7 @@ server {
|
||||
client_max_body_size 1m;
|
||||
}
|
||||
|
||||
location ~ ^/api/v1/(developer|session)/login$ {
|
||||
location ~ ^/api/v1/cloudron/login$ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
client_max_body_size 1m;
|
||||
limit_req zone=admin_login burst=5;
|
||||
@@ -301,13 +304,6 @@ server {
|
||||
}
|
||||
<% } %>
|
||||
|
||||
<% Object.keys(httpPaths).forEach(function (path) { -%>
|
||||
location "<%= path %>" {
|
||||
# the trailing / will replace part of the original URI matched by the location.
|
||||
proxy_pass http://<%= ip %>:<%= httpPaths[path] %>/;
|
||||
}
|
||||
<% }); %>
|
||||
|
||||
<% } else if ( endpoint === 'redirect' ) { %>
|
||||
location / {
|
||||
# redirect everything to the app. this is temporary because there is no way
|
||||
|
||||
@@ -246,9 +246,6 @@ async function onEvent(id, action, source, data) {
|
||||
assert.strictEqual(typeof source, 'object');
|
||||
assert.strictEqual(typeof data, 'object');
|
||||
|
||||
// external ldap syncer does not generate notifications - FIXME username might be an issue here
|
||||
if (source.username === AuditSource.EXTERNAL_LDAP_TASK.username) return;
|
||||
|
||||
switch (action) {
|
||||
case eventlog.ACTION_APP_OOM:
|
||||
return await oomEvent(id, data.containerId, data.app, data.addonName, data.event);
|
||||
|
||||
+27
-14
@@ -10,7 +10,10 @@ exports = module.exports = {
|
||||
|
||||
const apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
AuditSource = require('./auditsource.js'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
debug = require('debug')('box:platform'),
|
||||
delay = require('delay'),
|
||||
fs = require('fs'),
|
||||
infra = require('./infra_version.js'),
|
||||
locker = require('./locker.js'),
|
||||
@@ -37,9 +40,7 @@ async function start(options) {
|
||||
// short-circuit for the restart case
|
||||
if (_.isEqual(infra, existingInfra)) {
|
||||
debug('platform is uptodate at version %s', infra.version);
|
||||
|
||||
onPlatformReady(false /* !infraChanged */);
|
||||
|
||||
await onPlatformReady(false /* !infraChanged */);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -48,15 +49,27 @@ async function start(options) {
|
||||
const error = locker.lock(locker.OP_PLATFORM_START);
|
||||
if (error) throw error;
|
||||
|
||||
if (existingInfra.version !== infra.version) await removeAllContainers();
|
||||
if (existingInfra.version === 'none') await volumes.mountAll(); // when restoring, mount all volumes
|
||||
await markApps(existingInfra, options); // mark app state before we start addons. this gives the db import logic a chance to mark an app as errored
|
||||
await services.startServices(existingInfra);
|
||||
await fs.promises.writeFile(paths.INFRA_VERSION_FILE, JSON.stringify(infra, null, 4));
|
||||
for (let attempt = 0; attempt < 5; attempt++) {
|
||||
try {
|
||||
if (existingInfra.version !== infra.version) await removeAllContainers();
|
||||
if (existingInfra.version === 'none') await volumes.mountAll(); // when restoring, mount all volumes
|
||||
await markApps(existingInfra, options); // mark app state before we start addons. this gives the db import logic a chance to mark an app as errored
|
||||
await services.startServices(existingInfra);
|
||||
await fs.promises.writeFile(paths.INFRA_VERSION_FILE, JSON.stringify(infra, null, 4));
|
||||
break;
|
||||
} catch (error) {
|
||||
// for some reason, mysql arbitrary restarts making startup tasks fail. this makes the box update stuck
|
||||
// LOST is when existing connection breaks. REFUSED is when new connection cannot connect at all
|
||||
const retry = error.reason === BoxError.DATABASE_ERROR && (error.code === 'PROTOCOL_CONNECTION_LOST' || error.code === 'ECONNREFUSED');
|
||||
debug(`Failed to start services. retry=${retry} (attempt ${attempt}): ${error.message}`);
|
||||
if (!retry) break;
|
||||
await delay(10000);
|
||||
}
|
||||
}
|
||||
|
||||
locker.unlock(locker.OP_PLATFORM_START);
|
||||
|
||||
onPlatformReady(true /* infraChanged */); // background
|
||||
await onPlatformReady(true /* infraChanged */);
|
||||
}
|
||||
|
||||
async function stopAllTasks() {
|
||||
@@ -67,9 +80,9 @@ async function onPlatformReady(infraChanged) {
|
||||
debug(`onPlatformReady: platform is ready. infra changed: ${infraChanged}`);
|
||||
exports._isReady = true;
|
||||
|
||||
if (infraChanged) await pruneInfraImages();
|
||||
if (infraChanged) await safe(pruneInfraImages(), { debug }); // ignore error
|
||||
|
||||
await apps.schedulePendingTasks();
|
||||
await apps.schedulePendingTasks(AuditSource.PLATFORM);
|
||||
}
|
||||
|
||||
async function pruneInfraImages() {
|
||||
@@ -111,11 +124,11 @@ async function markApps(existingInfra, options) {
|
||||
|
||||
if (existingInfra.version === 'none') { // cloudron is being restored from backup
|
||||
debug('markApps: restoring installed apps');
|
||||
await apps.restoreInstalledApps(options);
|
||||
await apps.restoreInstalledApps(options, AuditSource.PLATFORM);
|
||||
} else if (existingInfra.version !== infra.version) {
|
||||
debug('markApps: reconfiguring installed apps');
|
||||
reverseProxy.removeAppConfigs(); // should we change the cert location, nginx will not start
|
||||
await apps.configureInstalledApps();
|
||||
await apps.configureInstalledApps(AuditSource.PLATFORM);
|
||||
} else {
|
||||
let changedAddons = [];
|
||||
if (infra.images.mysql.tag !== existingInfra.images.mysql.tag) changedAddons.push('mysql');
|
||||
@@ -126,7 +139,7 @@ async function markApps(existingInfra, options) {
|
||||
if (changedAddons.length) {
|
||||
// restart apps if docker image changes since the IP changes and any "persistent" connections fail
|
||||
debug(`markApps: changedAddons: ${JSON.stringify(changedAddons)}`);
|
||||
await apps.restartAppsUsingAddons(changedAddons);
|
||||
await apps.restartAppsUsingAddons(changedAddons, AuditSource.PLATFORM);
|
||||
} else {
|
||||
debug('markApps: apps are already uptodate');
|
||||
}
|
||||
|
||||
+21
-2
@@ -17,6 +17,7 @@ const assert = require('assert'),
|
||||
debug = require('debug')('box:provision'),
|
||||
domains = require('./domains.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
fs = require('fs'),
|
||||
mail = require('./mail.js'),
|
||||
mounts = require('./mounts.js'),
|
||||
reverseProxy = require('./reverseproxy.js'),
|
||||
@@ -24,6 +25,7 @@ const assert = require('assert'),
|
||||
semver = require('semver'),
|
||||
settings = require('./settings.js'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
paths = require('./paths.js'),
|
||||
users = require('./users.js'),
|
||||
tld = require('tldjs'),
|
||||
tokens = require('./tokens.js'),
|
||||
@@ -49,6 +51,14 @@ function setProgress(task, message, callback) {
|
||||
if (callback) callback();
|
||||
}
|
||||
|
||||
async function ensureDhparams() {
|
||||
if (fs.existsSync(paths.DHPARAMS_FILE)) return;
|
||||
debug('ensureDhparams: generating dhparams');
|
||||
const dhparams = safe.child_process.execSync('openssl dhparam -dsaparam 2048');
|
||||
if (!dhparams) throw new BoxError(BoxError.OPENSSL_ERROR, safe.error);
|
||||
if (!safe.fs.writeFileSync(paths.DHPARAMS_FILE, dhparams)) throw new BoxError(BoxError.FS_ERROR, `Could not save dhparams.pem: ${safe.error.message}`);
|
||||
}
|
||||
|
||||
async function unprovision() {
|
||||
// TODO: also cancel any existing configureWebadmin task
|
||||
await settings.setDashboardLocation('', '');
|
||||
@@ -62,6 +72,7 @@ async function setupTask(domain, auditSource) {
|
||||
|
||||
try {
|
||||
await cloudron.setupDnsAndCert(constants.DASHBOARD_LOCATION, domain, auditSource, (progress) => setProgress('setup', progress.message));
|
||||
await ensureDhparams();
|
||||
await cloudron.setDashboardDomain(domain, auditSource);
|
||||
setProgress('setup', 'Done'),
|
||||
await eventlog.add(eventlog.ACTION_PROVISION, auditSource, {});
|
||||
@@ -150,8 +161,16 @@ async function restoreTask(backupConfig, backupId, sysinfoConfig, options, audit
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
|
||||
try {
|
||||
setProgress('restore', 'Downloading backup');
|
||||
setProgress('restore', 'Downloading box backup');
|
||||
await backuptask.restore(backupConfig, backupId, (progress) => setProgress('restore', progress.message));
|
||||
|
||||
setProgress('restore', 'Downloading mail backup');
|
||||
const mailBackups = await backups.getByIdentifierAndStatePaged(backups.BACKUP_IDENTIFIER_MAIL, backups.BACKUP_STATE_NORMAL, 1, 1);
|
||||
if (mailBackups.length === 0) throw new BoxError(BoxError.NOT_FOUND, 'mail backup not found');
|
||||
const mailRestoreConfig = { backupConfig, backupId: mailBackups[0].id, backupFormat: mailBackups[0].format };
|
||||
await backuptask.downloadMail(mailRestoreConfig, (progress) => setProgress('restore', progress.message));
|
||||
|
||||
await ensureDhparams();
|
||||
await settings.setSysinfoConfig(sysinfoConfig);
|
||||
await reverseProxy.restoreFallbackCertificates();
|
||||
|
||||
@@ -161,7 +180,7 @@ async function restoreTask(backupConfig, backupId, sysinfoConfig, options, audit
|
||||
await settings.setBackupCredentials(backupConfig); // update just the credentials and not the policy and flags
|
||||
await eventlog.add(eventlog.ACTION_RESTORE, auditSource, { backupId });
|
||||
|
||||
setImmediate(() => safe(cloudron.onRestored(options), { debug }));
|
||||
setImmediate(() => safe(cloudron.onActivated(options), { debug }));
|
||||
} catch (error) {
|
||||
gProvisionStatus.restore.errorMessage = error ? error.message : '';
|
||||
}
|
||||
|
||||
+21
-2
@@ -24,6 +24,7 @@ const apps = require('./apps.js'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
settings = require('./settings.js'),
|
||||
speakeasy = require('speakeasy'),
|
||||
translation = require('./translation.js'),
|
||||
users = require('./users.js'),
|
||||
@@ -89,9 +90,10 @@ async function loginPage(req, res, next) {
|
||||
if (iconError || !iconBuffer) return next(new HttpError(500, 'Icon rendering error'));
|
||||
|
||||
const icon = 'data:image/png;base64,' + iconBuffer.toString('base64');
|
||||
const dashboardOrigin = settings.dashboardOrigin();
|
||||
|
||||
try {
|
||||
finalContent = ejs.render(translatedContent, { title, icon });
|
||||
finalContent = ejs.render(translatedContent, { title, icon, dashboardOrigin });
|
||||
} catch (e) {
|
||||
debug('Error rendering proxyauth-login.ejs', e);
|
||||
return next(new HttpError(500, 'Login template error'));
|
||||
@@ -209,7 +211,24 @@ function initializeAuthwallExpressSync() {
|
||||
|
||||
const json = middleware.json({ strict: true, limit: QUERY_LIMIT }); // application/json
|
||||
|
||||
if (process.env.BOX_ENV !== 'test') app.use(middleware.morgan('proxyauth :method :url :status :response-time ms - :res[content-length]', { immediate: false }));
|
||||
if (process.env.BOX_ENV !== 'test') {
|
||||
app.use(middleware.morgan(function (tokens, req, res) {
|
||||
return [
|
||||
'proxyauth',
|
||||
tokens.method(req, res),
|
||||
tokens.url(req, res),
|
||||
tokens.status(req, res),
|
||||
res.errorBody ? res.errorBody.status : '', // attached by connect-lastmile. can be missing when router errors like 404
|
||||
res.errorBody ? res.errorBody.message : '', // attached by connect-lastmile. can be missing when router errors like 404
|
||||
tokens['response-time'](req, res), 'ms', '-',
|
||||
tokens.res(req, res, 'content-length')
|
||||
].join(' ');
|
||||
}, {
|
||||
immediate: false,
|
||||
// only log failed requests by default
|
||||
skip: function (req, res) { return res.statusCode < 400; }
|
||||
}));
|
||||
}
|
||||
|
||||
const router = new express.Router();
|
||||
router.del = router.delete; // amend router.del for readability further on
|
||||
|
||||
+3
-10
@@ -51,8 +51,7 @@ const acme2 = require('./acme2.js'),
|
||||
shell = require('./shell.js'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
users = require('./users.js'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
util = require('util');
|
||||
|
||||
const NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/nginxconfig.ejs', { encoding: 'utf8' });
|
||||
const RESTART_SERVICE_CMD = path.join(__dirname, 'scripts/restartservice.sh');
|
||||
@@ -82,11 +81,6 @@ async function getAcmeApi(domainObject) {
|
||||
const [error, owner] = await safe(users.getOwner());
|
||||
apiOptions.email = (error || !owner) ? 'webmaster@cloudron.io' : owner.email; // can error if not activated yet
|
||||
|
||||
const accountKeyPem = await blobs.get(blobs.ACME_ACCOUNT_KEY);
|
||||
if (!accountKeyPem) throw new BoxError(BoxError.NOT_FOUND, 'acme account key not found');
|
||||
|
||||
apiOptions.accountKeyPem = accountKeyPem;
|
||||
|
||||
return { acmeApi, apiOptions };
|
||||
}
|
||||
|
||||
@@ -412,7 +406,7 @@ async function ensureCertificate(vhost, domain, auditSource) {
|
||||
debug(`ensureCertificate: ${vhost} cert does not exist`);
|
||||
}
|
||||
|
||||
debug('ensureCertificate: getting certificate for %s with options %j', vhost, _.omit(apiOptions, 'accountKeyPem'));
|
||||
debug('ensureCertificate: getting certificate for %s with options %j', vhost, apiOptions);
|
||||
|
||||
const acmePaths = getAcmeCertificatePathSync(vhost, domainObject);
|
||||
let [error] = await safe(acmeApi.getCertificate(vhost, domain, acmePaths, apiOptions));
|
||||
@@ -496,7 +490,7 @@ async function writeAppNginxConfig(app, fqdn, bundle) {
|
||||
hasIPv6: sysinfo.hasIPv6(),
|
||||
ip: app.containerIp,
|
||||
port: app.manifest.httpPort,
|
||||
endpoint: endpoint,
|
||||
endpoint,
|
||||
certFilePath: bundle.certFilePath,
|
||||
keyFilePath: bundle.keyFilePath,
|
||||
robotsTxtQuoted,
|
||||
@@ -507,7 +501,6 @@ async function writeAppNginxConfig(app, fqdn, bundle) {
|
||||
id: app.id,
|
||||
location: nginxLocation(safe.query(app.manifest, 'addons.proxyAuth.path') || '/')
|
||||
},
|
||||
httpPaths: app.manifest.httpPaths || {},
|
||||
ocsp: await isOcspEnabled(bundle.certFilePath)
|
||||
};
|
||||
const nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
|
||||
|
||||
@@ -31,9 +31,9 @@ async function passwordAuth(req, res, next) {
|
||||
|
||||
let [error, user] = await safe(verifyFunc(username, password, users.AP_WEBADMIN));
|
||||
if (error && error.reason === BoxError.NOT_FOUND) {
|
||||
[error, user] = await safe(externalLdap.maybeCreateUser(username.toLowerCase(), password));
|
||||
[error, user] = await safe(externalLdap.maybeCreateUser(username.toLowerCase()));
|
||||
if (error) return next(new HttpError(401, 'Unauthorized'));
|
||||
[error] = await safe(externalLdap.verifyPassword(user));
|
||||
[error] = await safe(externalLdap.verifyPassword(user, password));
|
||||
if (error) return next(new HttpError(401, 'Unauthorized'));
|
||||
}
|
||||
if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new HttpError(401, 'Unauthorized'));
|
||||
|
||||
@@ -81,10 +81,10 @@ async function logout(req, res) {
|
||||
async function passwordResetRequest(req, res, next) {
|
||||
if (!req.body.identifier || typeof req.body.identifier !== 'string') return next(new HttpError(401, 'A identifier must be non-empty string'));
|
||||
|
||||
const [error, result] = await safe(users.sendPasswordResetByIdentifier(req.body.identifier, AuditSource.fromRequest(req)));
|
||||
const [error] = await safe(users.sendPasswordResetByIdentifier(req.body.identifier, AuditSource.fromRequest(req)));
|
||||
if (error && error.reason !== BoxError.NOT_FOUND) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, { resetLink: result }));
|
||||
next(new HttpSuccess(202, {}));
|
||||
}
|
||||
|
||||
async function passwordReset(req, res, next) {
|
||||
|
||||
@@ -81,6 +81,8 @@ async function setup() {
|
||||
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
|
||||
const token = await tokens.add({ identifier: user.id, clientId: 'test-client-id', expires: Date.now() + (60 * 60 * 1000), name: 'fromtest' });
|
||||
user.token = token.accessToken;
|
||||
|
||||
await settings._set(settings.CLOUDRON_TOKEN_KEY, exports.appstoreToken); // appstore token
|
||||
}
|
||||
|
||||
async function cleanup() {
|
||||
|
||||
@@ -49,7 +49,7 @@ describe('Profile API', function () {
|
||||
expect(response.statusCode).to.equal(200);
|
||||
expect(response.body.username).to.equal(owner.username.toLowerCase());
|
||||
expect(response.body.email).to.equal(owner.email.toLowerCase());
|
||||
expect(response.body.fallbackEmail).to.equal(owner.email.toLowerCase());
|
||||
expect(response.body.fallbackEmail).to.equal('');
|
||||
expect(response.body.displayName).to.be.a('string');
|
||||
expect(response.body.password).to.not.be.ok();
|
||||
expect(response.body.salt).to.not.be.ok();
|
||||
|
||||
@@ -76,6 +76,15 @@ describe('Users API', function () {
|
||||
expect(response.statusCode).to.equal(400);
|
||||
});
|
||||
|
||||
it('cannot create user with non email fallbackEmail', async function () {
|
||||
const response = await superagent.post(`${serverUrl}/api/v1/users`)
|
||||
.query({ access_token: owner.token })
|
||||
.send({ username: user2.username, email: user2.email, fallbackEmail: 'notanemail' })
|
||||
.ok(() => true);
|
||||
|
||||
expect(response.statusCode).to.equal(400);
|
||||
});
|
||||
|
||||
it('create second user succeeds', async function () {
|
||||
const response = await superagent.post(`${serverUrl}/api/v1/users`)
|
||||
.query({ access_token: owner.token })
|
||||
@@ -176,11 +185,11 @@ describe('Users API', function () {
|
||||
it('sending succeeds', async function () {
|
||||
common.clearMailQueue();
|
||||
|
||||
const response = await superagent.post(`${serverUrl}/api/v1/users/${user.id}/send_invite`)
|
||||
const response = await superagent.post(`${serverUrl}/api/v1/users/${user.id}/send_invite_email`)
|
||||
.query({ access_token: owner.token })
|
||||
.send({});
|
||||
.send({ email: user.email });
|
||||
|
||||
expect(response.statusCode).to.equal(200);
|
||||
expect(response.statusCode).to.equal(202);
|
||||
await common.checkMails(1);
|
||||
});
|
||||
});
|
||||
@@ -299,6 +308,15 @@ describe('Users API', function () {
|
||||
expect(response.statusCode).to.equal(400);
|
||||
});
|
||||
|
||||
it('change fallbackEmail fails due to invalid email', async function () {
|
||||
const response = await superagent.post(`${serverUrl}/api/v1/users/${user2.id}`)
|
||||
.query({ access_token: owner.token })
|
||||
.send({ fallbackEmail: 'newemail@cloudron' })
|
||||
.ok(() => true);
|
||||
|
||||
expect(response.statusCode).to.equal(400);
|
||||
});
|
||||
|
||||
it('change user succeeds without email nor displayName', async function () {
|
||||
const response = await superagent.post(`${serverUrl}/api/v1/users/${user2.id}`)
|
||||
.query({ access_token: owner.token })
|
||||
|
||||
+56
-13
@@ -8,11 +8,16 @@ exports = module.exports = {
|
||||
del,
|
||||
setPassword,
|
||||
verifyPassword,
|
||||
sendInvite,
|
||||
setGroups,
|
||||
setGhost,
|
||||
makeOwner,
|
||||
|
||||
getPasswordResetLink,
|
||||
sendPasswordResetEmail,
|
||||
|
||||
getInviteLink,
|
||||
sendInviteEmail,
|
||||
|
||||
disableTwoFactorAuthentication,
|
||||
|
||||
load
|
||||
@@ -43,6 +48,7 @@ async function add(req, res, next) {
|
||||
|
||||
if (typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string'));
|
||||
if ('username' in req.body && typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be string'));
|
||||
if ('fallbackEmail' in req.body && typeof req.body.fallbackEmail !== 'string') return next(new HttpError(400, 'fallbackEmail must be string'));
|
||||
if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be string'));
|
||||
if ('password' in req.body && typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be string'));
|
||||
if ('role' in req.body) {
|
||||
@@ -54,8 +60,9 @@ async function add(req, res, next) {
|
||||
const email = req.body.email;
|
||||
const username = 'username' in req.body ? req.body.username : null;
|
||||
const displayName = req.body.displayName || '';
|
||||
const fallbackEmail = req.body.fallbackEmail || '';
|
||||
|
||||
const [error, id] = await safe(users.add(email, { username, password, displayName, invitor: req.user, role: req.body.role || users.ROLE_USER }, AuditSource.fromRequest(req)));
|
||||
const [error, id] = await safe(users.add(email, { username, password, displayName, fallbackEmail, invitor: req.user, role: req.body.role || users.ROLE_USER }, AuditSource.fromRequest(req)));
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(201, { id }));
|
||||
@@ -151,17 +158,6 @@ async function disableTwoFactorAuthentication(req, res, next) {
|
||||
next(new HttpSuccess(200, {}));
|
||||
}
|
||||
|
||||
async function sendInvite(req, res, next) {
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
if (users.compareRoles(req.user.role, req.resource.role) < 0) return next(new HttpError(403, `role '${req.resource.role}' is required but user has only '${req.user.role}'`));
|
||||
|
||||
const [error, inviteLink ] = await safe(users.sendInvite(req.resource, { invitor: req.user }, AuditSource.fromRequest(req)));
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(200, { inviteLink }));
|
||||
}
|
||||
|
||||
async function setGroups(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
@@ -214,3 +210,50 @@ async function makeOwner(req, res, next) {
|
||||
|
||||
next(new HttpSuccess(204));
|
||||
}
|
||||
|
||||
// This will always return a reset link, if none is set or expired a new one will be created
|
||||
async function getPasswordResetLink(req, res, next) {
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
if (users.compareRoles(req.user.role, req.resource.role) < 0) return next(new HttpError(403, `role '${req.resource.role}' is required but user has only '${req.user.role}'`));
|
||||
|
||||
let [error, passwordResetLink] = await safe(users.getPasswordResetLink(req.resource, AuditSource.fromRequest(req)));
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(200, { passwordResetLink }));
|
||||
}
|
||||
|
||||
async function sendPasswordResetEmail(req, res, next) {
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
if (!req.body.email || typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be a non-empty string'));
|
||||
if (users.compareRoles(req.user.role, req.resource.role) < 0) return next(new HttpError(403, `role '${req.resource.role}' is required but user has only '${req.user.role}'`));
|
||||
|
||||
let [error] = await safe(users.sendPasswordResetEmail(req.resource, req.body.email, AuditSource.fromRequest(req)));
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
}
|
||||
|
||||
async function getInviteLink(req, res, next) {
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
if (users.compareRoles(req.user.role, req.resource.role) < 0) return next(new HttpError(403, `role '${req.resource.role}' is required but user has only '${req.user.role}'`));
|
||||
|
||||
let [error, inviteLink] = await safe(users.getInviteLink(req.resource, AuditSource.fromRequest(req)));
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(200, { inviteLink }));
|
||||
}
|
||||
|
||||
async function sendInviteEmail(req, res, next) {
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
if (!req.body.email || typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be a non-empty string'));
|
||||
if (users.compareRoles(req.user.role, req.resource.role) < 0) return next(new HttpError(403, `role '${req.resource.role}' is required but user has only '${req.user.role}'`));
|
||||
|
||||
let [error] = await safe(users.sendInviteEmail(req.resource, req.body.email, AuditSource.fromRequest(req)));
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
}
|
||||
|
||||
@@ -56,8 +56,15 @@ function dumpMemoryInfo() {
|
||||
const mu = process.memoryUsage();
|
||||
const hs = v8.getHeapStatistics();
|
||||
|
||||
debug(`process: rss: ${mu.rss} heapTotal: ${mu.heapTotal} heapUsed: ${mu.heapUsed} external: ${mu.external}`);
|
||||
debug(`v8 heap : used ${hs.used_heap_size} total: ${hs.total_heap_size} max: ${hs.heap_size_limit}`);
|
||||
function h(bytes) { // human readable
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024)),
|
||||
sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
return (bytes / Math.pow(1024, i)).toFixed(2) * 1 + '' + sizes[i];
|
||||
}
|
||||
|
||||
debug(`process: rss: ${h(mu.rss)} heapUsed: ${h(mu.heapUsed)} heapTotal: ${h(mu.heapTotal)} external: ${h(mu.external)}`);
|
||||
debug(`v8 heap: used ${h(hs.used_heap_size)} total: ${h(hs.total_heap_size)} max: ${h(hs.heap_size_limit)}`);
|
||||
}
|
||||
|
||||
initialize(function (error) {
|
||||
|
||||
+4
-6
@@ -6,7 +6,6 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
const assert = require('assert'),
|
||||
blobs = require('./blobs.js'),
|
||||
cloudron = require('./cloudron.js'),
|
||||
constants = require('./constants.js'),
|
||||
database = require('./database.js'),
|
||||
@@ -103,9 +102,6 @@ function initializeExpressSync() {
|
||||
router.post('/api/v1/cloudron/password_reset', json, routes.cloudron.passwordReset);
|
||||
router.post('/api/v1/cloudron/setup_account', json, routes.cloudron.setupAccount);
|
||||
|
||||
// developer routes
|
||||
router.post('/api/v1/developer/login', json, password, routes.cloudron.login); // DEPRECATED we should use the regular /api/v1/cloudron/login
|
||||
|
||||
// cloudron routes
|
||||
router.get ('/api/v1/cloudron/update', token, authorizeAdmin, routes.cloudron.getUpdateInfo);
|
||||
router.post('/api/v1/cloudron/update', json, token, authorizeAdmin, routes.cloudron.update);
|
||||
@@ -179,8 +175,11 @@ function initializeExpressSync() {
|
||||
router.post('/api/v1/users/:userId/ghost', json, token, authorizeAdmin, routes.users.load, routes.users.setGhost);
|
||||
router.put ('/api/v1/users/:userId/groups', json, token, authorizeUserManager, routes.users.load, routes.users.setGroups);
|
||||
router.post('/api/v1/users/:userId/make_owner', json, token, authorizeOwner, routes.users.load, routes.users.makeOwner);
|
||||
router.post('/api/v1/users/:userId/send_invite', json, token, authorizeUserManager, routes.users.load, routes.users.sendInvite);
|
||||
router.post('/api/v1/users/:userId/twofactorauthentication_disable', json, token, authorizeUserManager, routes.users.load, routes.users.disableTwoFactorAuthentication);
|
||||
router.get ('/api/v1/users/:userId/password_reset_link', json, token, authorizeUserManager, routes.users.load, routes.users.getPasswordResetLink);
|
||||
router.post('/api/v1/users/:userId/send_password_reset_email', json, token, authorizeUserManager, routes.users.load, routes.users.sendPasswordResetEmail);
|
||||
router.get ('/api/v1/users/:userId/invite_link', json, token, authorizeUserManager, routes.users.load, routes.users.getInviteLink);
|
||||
router.post('/api/v1/users/:userId/send_invite_email', json, token, authorizeUserManager, routes.users.load, routes.users.sendInviteEmail);
|
||||
|
||||
// Group management
|
||||
router.get ('/api/v1/groups', token, authorizeUserManager, routes.groups.list);
|
||||
@@ -380,7 +379,6 @@ async function start() {
|
||||
|
||||
await database.initialize();
|
||||
await settings.initCache(); // pre-load very often used settings
|
||||
await blobs.initSecrets();
|
||||
await cloudron.initialize();
|
||||
await util.promisify(gHttpServer.listen.bind(gHttpServer))(constants.PORT, '127.0.0.1');
|
||||
await safe(eventlog.add(eventlog.ACTION_START, { userId: null, username: 'boot' }, { version: constants.VERSION })); // can fail if db down
|
||||
|
||||
+11
-5
@@ -919,8 +919,12 @@ async function startTurn(existingInfra) {
|
||||
const memory = system.getMemoryAllocation(memoryLimit);
|
||||
const realm = settings.dashboardFqdn();
|
||||
|
||||
const turnSecret = await blobs.get(blobs.ADDON_TURN_SECRET);
|
||||
if (!turnSecret) throw new BoxError(BoxError.ADDONS_ERROR, 'Turn secret is missing');
|
||||
let turnSecret = await blobs.get(blobs.ADDON_TURN_SECRET);
|
||||
if (!turnSecret) {
|
||||
debug('startTurn: generting turn secret');
|
||||
turnSecret = 'a' + crypto.randomBytes(15).toString('hex'); // prefix with a to ensure string starts with a letter
|
||||
await blobs.set(blobs.ADDON_TURN_SECRET, Buffer.from(turnSecret));
|
||||
}
|
||||
|
||||
const readOnly = !serviceConfig.recoveryMode ? '--read-only' : '';
|
||||
const cmd = serviceConfig.recoveryMode ? '/bin/bash -c \'echo "Debug mode. Sleeping" && sleep infinity\'' : '';
|
||||
@@ -1080,8 +1084,10 @@ async function setupRecvMail(app, options) {
|
||||
|
||||
const env = [
|
||||
{ name: `${envPrefix}MAIL_IMAP_SERVER`, value: 'mail' },
|
||||
{ name: `${envPrefix}MAIL_IMAP_PORT`, value: '9993' },
|
||||
{ name: `${envPrefix}MAIL_POP3_PORT`, value: '9995' },
|
||||
{ name: `${envPrefix}MAIL_IMAP_PORT`, value: '9393' },
|
||||
{ name: `${envPrefix}MAIL_IMAPS_PORT`, value: '9993' },
|
||||
{ name: `${envPrefix}MAIL_POP3_PORT`, value: '9595' },
|
||||
{ name: `${envPrefix}MAIL_POP3S_PORT`, value: '9995' },
|
||||
{ name: `${envPrefix}MAIL_IMAP_USERNAME`, value: app.inboxName + '@' + app.inboxDomain },
|
||||
{ name: `${envPrefix}MAIL_IMAP_PASSWORD`, value: password },
|
||||
{ name: `${envPrefix}MAIL_TO`, value: app.inboxName + '@' + app.inboxDomain },
|
||||
@@ -1400,7 +1406,7 @@ async function setupPostgreSql(app, options) {
|
||||
];
|
||||
|
||||
debug('Setting postgresql addon config to %j', env);
|
||||
addonConfigs.set(app.id, 'postgresql', env);
|
||||
await addonConfigs.set(app.id, 'postgresql', env);
|
||||
}
|
||||
|
||||
async function clearPostgreSql(app, options) {
|
||||
|
||||
+20
@@ -9,6 +9,7 @@ exports = module.exports = {
|
||||
|
||||
const apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
blobs = require('./blobs.js'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
debug = require('debug')('box:sftp'),
|
||||
docker = require('./docker.js'),
|
||||
@@ -22,6 +23,23 @@ const apps = require('./apps.js'),
|
||||
system = require('./system.js'),
|
||||
volumes = require('./volumes.js');
|
||||
|
||||
async function ensureKeys() {
|
||||
let sftpPrivateKey = await blobs.get(blobs.SFTP_PRIVATE_KEY);
|
||||
let sftpPublicKey = await blobs.get(blobs.SFTP_PUBLIC_KEY);
|
||||
|
||||
if (!sftpPrivateKey || !sftpPublicKey) {
|
||||
debug('ensureSecrets: generating new sftp keys');
|
||||
if (!safe.child_process.execSync(`ssh-keygen -m PEM -t rsa -f "${paths.SFTP_KEYS_DIR}/ssh_host_rsa_key" -q -N ""`)) throw new BoxError(BoxError.OPENSSL_ERROR, `Could not generate sftp ssh keys: ${safe.error.message}`);
|
||||
sftpPublicKey = safe.fs.readFileSync(paths.SFTP_PUBLIC_KEY_FILE);
|
||||
await blobs.set(blobs.SFTP_PUBLIC_KEY, sftpPublicKey);
|
||||
sftpPrivateKey = safe.fs.readFileSync(paths.SFTP_PRIVATE_KEY_FILE);
|
||||
await blobs.set(blobs.SFTP_PRIVATE_KEY, sftpPrivateKey);
|
||||
}
|
||||
|
||||
if (!safe.fs.writeFileSync(paths.SFTP_PUBLIC_KEY_FILE, sftpPublicKey)) throw new BoxError(BoxError.FS_ERROR, `Could not save sftp public key: ${safe.error.message}`);
|
||||
if (!safe.fs.writeFileSync(paths.SFTP_PRIVATE_KEY_FILE, sftpPrivateKey)) throw new BoxError(BoxError.FS_ERROR, `Could not save sftp private key: ${safe.error.message}`);
|
||||
}
|
||||
|
||||
async function start(existingInfra) {
|
||||
assert.strictEqual(typeof existingInfra, 'object');
|
||||
|
||||
@@ -34,6 +52,8 @@ async function start(existingInfra) {
|
||||
const memory = system.getMemoryAllocation(memoryLimit);
|
||||
const cloudronToken = hat(8 * 128);
|
||||
|
||||
await ensureKeys();
|
||||
|
||||
const resolvedAppDataDir = safe.fs.realpathSync(paths.APPS_DATA_DIR);
|
||||
if (!resolvedAppDataDir) throw new BoxError(BoxError.FS_ERROR, `Could not resolve apps data dir: ${safe.error.message}`);
|
||||
|
||||
|
||||
@@ -71,18 +71,14 @@ async function checkPreconditions(apiConfig, dataLayout, callback) {
|
||||
for (let localPath of dataLayout.localPaths()) {
|
||||
debug(`checkPreconditions: getting disk usage of ${localPath}`);
|
||||
let result = safe.child_process.execSync(`du -Dsb ${localPath}`, { encoding: 'utf8' });
|
||||
if (!result) return callback(new BoxError(BoxError.FS_ERROR, safe.error));
|
||||
if (!result) return callback(new BoxError(BoxError.FS_ERROR, `du error: ${safe.error.message}`));
|
||||
used += parseInt(result, 10);
|
||||
}
|
||||
|
||||
debug(`checkPreconditions: ${used} bytes`);
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = await df.file(getBackupPath(apiConfig));
|
||||
} catch(error) {
|
||||
return callback(new BoxError(BoxError.FS_ERROR, error));
|
||||
}
|
||||
const [error, result] = await safe(df.file(getBackupPath(apiConfig)));
|
||||
if (error) return callback(new BoxError(BoxError.FS_ERROR, `Error when checking for disk space: ${error.message}`));
|
||||
|
||||
// Check filesystem is mounted so we don't write into the actual folder on disk
|
||||
if (apiConfig.provider === PROVIDER_SSHFS || apiConfig.provider === PROVIDER_CIFS || apiConfig.provider === PROVIDER_NFS || apiConfig.provider === PROVIDER_EXT4) {
|
||||
|
||||
@@ -29,6 +29,7 @@ function sshInfo() {
|
||||
filePath = '/home/ubuntu/.ssh/authorized_keys';
|
||||
user = 'ubuntu';
|
||||
} else {
|
||||
filePath = '/root/.ssh/authorized_keys';
|
||||
user = 'root';
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
'use strict';
|
||||
|
||||
const apps = require('../apps.js'),
|
||||
AuditSource = require('../auditsource.js'),
|
||||
BoxError = require('../boxerror.js'),
|
||||
common = require('./common.js'),
|
||||
expect = require('expect.js'),
|
||||
@@ -284,7 +285,7 @@ describe('Apps', function () {
|
||||
});
|
||||
|
||||
it('can mark apps for reconfigure', async function () {
|
||||
await apps.configureInstalledApps();
|
||||
await apps.configureInstalledApps(AuditSource.PLATFORM);
|
||||
|
||||
const result = await apps.list();
|
||||
expect(result[0].installationState).to.be(apps.ISTATE_PENDING_CONFIGURE);
|
||||
@@ -309,7 +310,7 @@ describe('Apps', function () {
|
||||
});
|
||||
|
||||
it('can mark apps for reconfigure', async function () {
|
||||
await apps.restoreInstalledApps({});
|
||||
await apps.restoreInstalledApps({}, AuditSource.PLATFORM);
|
||||
|
||||
const result = await apps.list();
|
||||
expect(result[0].installationState).to.be(apps.ISTATE_PENDING_INSTALL);
|
||||
|
||||
+6
-3
@@ -63,7 +63,7 @@ const admin = {
|
||||
username: 'testadmin',
|
||||
password: 'secret123',
|
||||
email: 'admin@me.com',
|
||||
fallbackEmail: 'admin@me.com',
|
||||
fallbackEmail: 'admin@external.com',
|
||||
salt: 'morton',
|
||||
createdAt: 'sometime back',
|
||||
resetToken: '',
|
||||
@@ -80,7 +80,7 @@ const user = {
|
||||
username: 'user',
|
||||
password: '123secret',
|
||||
email: 'user@me.com',
|
||||
fallbackEmail: 'user@me.com',
|
||||
fallbackEmail: 'user@external.com',
|
||||
role: 'user',
|
||||
salt: 'morton',
|
||||
createdAt: 'sometime back',
|
||||
@@ -165,7 +165,6 @@ async function databaseSetup() {
|
||||
await settings._setApiServerOrigin(exports.mockApiServerOrigin);
|
||||
await settings.setDashboardLocation(exports.dashboardDomain, exports.dashboardFqdn);
|
||||
await settings.initCache();
|
||||
await blobs.initSecrets();
|
||||
}
|
||||
|
||||
async function domainSetup() {
|
||||
@@ -208,5 +207,9 @@ function clearMailQueue() {
|
||||
async function checkMails(number) {
|
||||
await delay(1000);
|
||||
expect(mailer._mailQueue.length).to.equal(number);
|
||||
const emails = mailer._mailQueue;
|
||||
clearMailQueue();
|
||||
|
||||
// return for further investigation
|
||||
return emails;
|
||||
}
|
||||
|
||||
@@ -535,16 +535,12 @@ describe('External LDAP', function () {
|
||||
});
|
||||
|
||||
it('succeeds for known user with correct password', async function () {
|
||||
const newUser = {
|
||||
gLdapUsers.push({
|
||||
username: 'autologinuser2',
|
||||
displayName: 'Auto Login2',
|
||||
email: 'auto2@login.com',
|
||||
password: LDAP_SHARED_PASSWORD
|
||||
};
|
||||
|
||||
gLdapUsers.push(newUser);
|
||||
|
||||
await users.add(newUser.email, newUser, auditSource);
|
||||
});
|
||||
|
||||
const response = await superagent.post(`${serverUrl}/api/v1/cloudron/login`)
|
||||
.send({ username: 'autologinuser2', password: LDAP_SHARED_PASSWORD })
|
||||
|
||||
+37
-5
@@ -83,6 +83,12 @@ describe('User', function () {
|
||||
expect(error.reason).to.equal(BoxError.BAD_FIELD);
|
||||
});
|
||||
|
||||
it('fails because fallbackEmail is not an email', async function () {
|
||||
const user = Object.assign({}, admin, { fallbackEmail: 'notanemail' });
|
||||
const [error] = await safe(users.add(user.email, user, auditSource));
|
||||
expect(error.reason).to.equal(BoxError.BAD_FIELD);
|
||||
});
|
||||
|
||||
it('can add user', async function () {
|
||||
const id = await users.add(admin.email, admin, auditSource);
|
||||
admin.id = id;
|
||||
@@ -403,7 +409,7 @@ describe('User', function () {
|
||||
const result = await users.get(admin.id);
|
||||
expect(result.id).to.equal(admin.id);
|
||||
expect(result.email).to.equal(admin.email.toLowerCase());
|
||||
expect(result.fallbackEmail).to.equal(admin.email.toLowerCase());
|
||||
expect(result.fallbackEmail).to.equal(admin.fallbackEmail.toLowerCase());
|
||||
expect(result.username).to.equal(admin.username.toLowerCase());
|
||||
expect(result.displayName).to.equal(admin.displayName);
|
||||
});
|
||||
@@ -487,11 +493,37 @@ describe('User', function () {
|
||||
describe('invite', function () {
|
||||
before(createOwner);
|
||||
|
||||
it('send invite', async function () {
|
||||
let user;
|
||||
|
||||
it('get link fails as alreayd been used', async function () {
|
||||
const [error] = await safe(users.getInviteLink(admin, auditSource));
|
||||
expect(error.reason).to.be(BoxError.BAD_STATE);
|
||||
});
|
||||
|
||||
it('can get link', async function () {
|
||||
const userId = await users.add('some@mail.com', { username: 'someoneinvited', displayName: 'some one', password: 'unsafe1234' }, auditSource);
|
||||
user = await users.get(userId);
|
||||
|
||||
const inviteLink = await users.getInviteLink(user, auditSource);
|
||||
expect(inviteLink).to.be.a('string');
|
||||
expect(inviteLink).to.contain(user.inviteToken);
|
||||
});
|
||||
|
||||
it('cannot send mail for already active user', async function () {
|
||||
const [error] = await safe(users.sendInviteEmail(admin, 'admin@mail.com', auditSource));
|
||||
expect(error.reason).to.be(BoxError.BAD_STATE);
|
||||
});
|
||||
|
||||
it('cannot send mail with empty receipient', async function () {
|
||||
const [error] = await safe(users.sendInviteEmail(user, '', auditSource));
|
||||
expect(error.reason).to.be(BoxError.BAD_FIELD);
|
||||
});
|
||||
|
||||
it('can send mail', async function () {
|
||||
await clearMailQueue();
|
||||
const inviteLink = await users.sendInvite(admin, {}, auditSource);
|
||||
expect(inviteLink).to.be.ok();
|
||||
await checkMails(1);
|
||||
await users.sendInviteEmail(user, 'custom@mail.com', auditSource);
|
||||
const emails = await checkMails(1);
|
||||
expect(emails[0].to).to.equal('custom@mail.com');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+3
-1
@@ -148,9 +148,11 @@ async function update(boxUpdateInfo, options, progressCallback) {
|
||||
progressCallback({ percent: 10, message: 'Backing up' });
|
||||
|
||||
await backuptask.fullBackup({ preserveSecs: 3*7*24*60*60 }, (progress) => progressCallback({ percent: 10+progress.percent*70/100, message: progress.message }));
|
||||
|
||||
await checkFreeDiskSpace(1024*1024*1024); // check again in case backup is in same disk
|
||||
}
|
||||
|
||||
debug('updating box %s', boxUpdateInfo.sourceTarballUrl);
|
||||
debug(`Updating box with ${boxUpdateInfo.sourceTarballUrl}`);
|
||||
|
||||
progressCallback({ percent: 70, message: 'Installing update' });
|
||||
|
||||
|
||||
+63
-16
@@ -29,14 +29,18 @@ exports = module.exports = {
|
||||
|
||||
del,
|
||||
|
||||
sendInvite,
|
||||
|
||||
setTwoFactorAuthenticationSecret,
|
||||
enableTwoFactorAuthentication,
|
||||
disableTwoFactorAuthentication,
|
||||
|
||||
sendPasswordResetByIdentifier,
|
||||
|
||||
getPasswordResetLink,
|
||||
sendPasswordResetEmail,
|
||||
|
||||
getInviteLink,
|
||||
sendInviteEmail,
|
||||
|
||||
notifyLoginLocation,
|
||||
|
||||
setupAccount,
|
||||
@@ -180,8 +184,10 @@ async function add(email, data, auditSource) {
|
||||
assert(data.username === null || typeof data.username === 'string');
|
||||
assert(data.password === null || typeof data.password === 'string');
|
||||
assert.strictEqual(typeof data.displayName, 'string');
|
||||
if ('fallbackEmail' in data) assert.strictEqual(typeof data.fallbackEmail, 'string');
|
||||
|
||||
let { username, password, displayName } = data;
|
||||
let fallbackEmail = data.fallbackEmail || '';
|
||||
const source = data.source || ''; // empty is local user
|
||||
const role = data.role || exports.ROLE_USER;
|
||||
|
||||
@@ -204,6 +210,12 @@ async function add(email, data, auditSource) {
|
||||
error = validateEmail(email);
|
||||
if (error) throw error;
|
||||
|
||||
fallbackEmail = fallbackEmail.toLowerCase();
|
||||
if (fallbackEmail) {
|
||||
let error = validateEmail(fallbackEmail);
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
error = validateDisplayName(displayName);
|
||||
if (error) throw error;
|
||||
|
||||
@@ -222,7 +234,7 @@ async function add(email, data, auditSource) {
|
||||
id: 'uid-' + uuid.v4(),
|
||||
username: username,
|
||||
email: email,
|
||||
fallbackEmail: email,
|
||||
fallbackEmail: fallbackEmail,
|
||||
password: Buffer.from(derivedKey, 'binary').toString('hex'),
|
||||
salt: salt.toString('hex'),
|
||||
resetToken: '',
|
||||
@@ -256,6 +268,8 @@ async function setGhost(user, password, expiresAt) {
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
assert.strictEqual(typeof expiresAt, 'number');
|
||||
|
||||
if (!user.username) throw new BoxError(BoxError.BAD_STATE, 'user has no username yet');
|
||||
|
||||
expiresAt = expiresAt || (Date.now() + DEFAULT_GHOST_LIFETIME);
|
||||
|
||||
debug(`setGhost: ${user.username} expiresAt ${expiresAt}`);
|
||||
@@ -616,11 +630,40 @@ async function sendPasswordResetByIdentifier(identifier, auditSource) {
|
||||
await update(user, { resetToken,resetTokenCreationTime }, auditSource);
|
||||
|
||||
const resetLink = `${settings.dashboardOrigin()}/login.html?resetToken=${user.resetToken}`;
|
||||
await mailer.passwordReset(user, resetLink);
|
||||
await mailer.passwordReset(user, user.fallbackEmail || user.email, resetLink);
|
||||
}
|
||||
|
||||
async function getPasswordResetLink(user, auditSource) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
|
||||
let resetToken = user.resetToken;
|
||||
let resetTokenCreationTime = user.resetTokenCreationTime || 0;
|
||||
|
||||
if (!resetToken || (Date.now() - resetTokenCreationTime > 7 * 24 * 60 * 60 * 1000)) {
|
||||
resetToken = hat(256);
|
||||
resetTokenCreationTime = new Date();
|
||||
|
||||
await update(user, { resetToken, resetTokenCreationTime }, auditSource);
|
||||
}
|
||||
|
||||
const resetLink = `${settings.dashboardOrigin()}/login.html?resetToken=${resetToken}`;
|
||||
|
||||
return resetLink;
|
||||
}
|
||||
|
||||
async function sendPasswordResetEmail(user, email, auditSource) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
|
||||
const error = validateEmail(email);
|
||||
if (error) throw error;
|
||||
|
||||
const resetLink = await getPasswordResetLink(user, auditSource);
|
||||
await mailer.passwordReset(user, email, resetLink);
|
||||
}
|
||||
|
||||
async function notifyLoginLocation(user, ip, userAgent, auditSource) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
@@ -701,21 +744,15 @@ async function createOwner(email, username, password, displayName, auditSource)
|
||||
const activated = await isActivated();
|
||||
if (activated) throw new BoxError(BoxError.ALREADY_EXISTS, 'Cloudron already activated');
|
||||
|
||||
return await add(email, { username, password, displayName, role: exports.ROLE_OWNER }, auditSource);
|
||||
return await add(email, { username, password, fallbackEmail: '', displayName, role: exports.ROLE_OWNER }, auditSource);
|
||||
}
|
||||
|
||||
async function sendInvite(user, options, auditSource) {
|
||||
async function getInviteLink(user, auditSource) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
|
||||
if (user.source) throw new BoxError(BoxError.CONFLICT, 'User is from an external directory');
|
||||
|
||||
// not sure if this can ever be the case
|
||||
if (!user.inviteToken) {
|
||||
const inviteToken = hat(256);
|
||||
user.inviteToken = inviteToken;
|
||||
await update(user, { inviteToken }, auditSource);
|
||||
}
|
||||
if (!user.inviteToken) throw new BoxError(BoxError.BAD_STATE, 'User already used invite link');
|
||||
|
||||
const directoryConfig = await settings.getDirectoryConfig();
|
||||
let inviteLink = `${settings.dashboardOrigin()}/setupaccount.html?inviteToken=${user.inviteToken}&email=${encodeURIComponent(user.email)}`;
|
||||
@@ -724,11 +761,21 @@ async function sendInvite(user, options, auditSource) {
|
||||
if (user.displayName) inviteLink += `&displayName=${encodeURIComponent(user.displayName)}`;
|
||||
if (directoryConfig.lockUserProfiles) inviteLink += '&profileLocked=true';
|
||||
|
||||
await mailer.sendInvite(user, options.invitor || null, inviteLink);
|
||||
|
||||
return inviteLink;
|
||||
}
|
||||
|
||||
async function sendInviteEmail(user, email, auditSource) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
|
||||
const error = validateEmail(email);
|
||||
if (error) throw error;
|
||||
|
||||
const inviteLink = await getInviteLink(user, auditSource);
|
||||
await mailer.sendInvite(user, null /* invitor */, email, inviteLink);
|
||||
}
|
||||
|
||||
async function setupAccount(user, data, auditSource) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof data, 'object');
|
||||
|
||||
Reference in New Issue
Block a user