Compare commits
373 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dd44edde0a | |||
| 885e90e810 | |||
| 9cdf5dd0f3 | |||
| df6e3eb1e6 | |||
| 05026771e1 | |||
| 7039108438 | |||
| 02ee13cfb2 | |||
| 096e244252 | |||
| bf5b7294a0 | |||
| a5da266643 | |||
| cf7bb49e15 | |||
| 208b732bda | |||
| c73d93b8bd | |||
| 98a96eae2b | |||
| 2f9fe30c9d | |||
| aeee8afc02 | |||
| e85f0a4f52 | |||
| da98649667 | |||
| 5ac08cc06b | |||
| da72597dd3 | |||
| 1f1c94de70 | |||
| 60b3fceea6 | |||
| 5073809486 | |||
| debd779cfd | |||
| 6b9454100e | |||
| 779ad24542 | |||
| b94dbf5fa3 | |||
| 45c49c9757 | |||
| 91288c96b1 | |||
| f8e22a0730 | |||
| 114b45882a | |||
| b1b6f70118 | |||
| 648d42dfe4 | |||
| 99f989c384 | |||
| 2112c7d096 | |||
| ac63d00c93 | |||
| e04871f79f | |||
| 182c162dc4 | |||
| 822b38cc89 | |||
| d564003c87 | |||
| 1b307632ab | |||
| aa747cea85 | |||
| f4a322478d | |||
| d2882433a5 | |||
| a94b175805 | |||
| 37d81da806 | |||
| d089444441 | |||
| b0d65a1bae | |||
| 16288cf277 | |||
| 7ddbabf781 | |||
| fe35f4497b | |||
| 625463f6ab | |||
| ff632b6816 | |||
| fbc666f178 | |||
| d89bbdd50c | |||
| 96f9aa39b2 | |||
| 7330814d0f | |||
| 312efdcd94 | |||
| 5db78ae359 | |||
| 97967e60e8 | |||
| 9106b5d182 | |||
| 74bdb6cb9d | |||
| 0a44d426fa | |||
| e1718c4e8d | |||
| f511a610b5 | |||
| 4d5715188d | |||
| 2ea21be5bd | |||
| 5bb0419699 | |||
| a8131eed71 | |||
| ed09c06ba4 | |||
| 3c59a0ff31 | |||
| a6d24b3e48 | |||
| 060135eecb | |||
| ef296c24fe | |||
| 707aaf25ec | |||
| 7edeb0c358 | |||
| e516af14b2 | |||
| 4086f2671d | |||
| 23c4550430 | |||
| 31d25cd6be | |||
| 07b3c7a245 | |||
| a00b7281a7 | |||
| ddeee0c970 | |||
| 8aad71efd0 | |||
| 2028f6b984 | |||
| bff4999d27 | |||
| d429015f83 | |||
| e2628e2d43 | |||
| 05dcbee7e3 | |||
| a81919262e | |||
| b14b5f141b | |||
| 1259d11173 | |||
| 0a7b132be8 | |||
| ed9210eede | |||
| 9ee6aa54c6 | |||
| 7cfc455cd3 | |||
| a481ceac8c | |||
| 8c7eff4e24 | |||
| c6c584ff74 | |||
| ba50eb121d | |||
| aa8ebbd7ea | |||
| 64bc9c6dbe | |||
| bba9963b7c | |||
| 6ea2aa4a54 | |||
| 3c3f81365b | |||
| 3adeed381b | |||
| 0f5b7278b8 | |||
| f94ff49fb9 | |||
| d512a9c30d | |||
| 0c5113ed5b | |||
| 2469f4cdff | |||
| 9c53bfb7fb | |||
| 8b8144588d | |||
| 77553da4c1 | |||
| cbcf943691 | |||
| 725a19e5b5 | |||
| f9115f902a | |||
| e4faf26d74 | |||
| 1c96fbb533 | |||
| 3dc163c33d | |||
| edae94cf2e | |||
| d1ff8e9d6b | |||
| 70743bd285 | |||
| 493f1505f0 | |||
| 007e3b5eef | |||
| d9bf6c0933 | |||
| 324344d118 | |||
| 5cb71e9443 | |||
| cca19f00c5 | |||
| 6648f41f3d | |||
| c1e6b47fd6 | |||
| 0f103ccce1 | |||
| bc6e652293 | |||
| 85b4f2dbdd | |||
| d47b83a63b | |||
| b2e9fa7e0d | |||
| a9fb444622 | |||
| 33ba22a021 | |||
| 57de0282cd | |||
| 8568fd26d8 | |||
| 84f41e08cf | |||
| a96da20536 | |||
| 5199a9342e | |||
| 893ecec0fa | |||
| e3da6419f5 | |||
| 0750d2ba50 | |||
| f1fcb65fbe | |||
| 215aa65d5a | |||
| 85f67c13da | |||
| 6dcc478aeb | |||
| 3f2496db6f | |||
| 612f79f9e0 | |||
| 90fb1cd735 | |||
| 7c24d9c6c6 | |||
| 60f1b2356a | |||
| 0b8f21508f | |||
| ae128c0fa4 | |||
| 1b4ec9ecf9 | |||
| b0ce0b61d6 | |||
| e1ffdaddfa | |||
| af8344f482 | |||
| 7dc2596b3b | |||
| 0109956fc2 | |||
| 945fe3f3ec | |||
| 9c868135f3 | |||
| 5be288023b | |||
| a03f97186c | |||
| 0aab891980 | |||
| 5268d3f57d | |||
| 129cbb5beb | |||
| 2601d2945d | |||
| e3829eb24b | |||
| f6cb1a0863 | |||
| 4f964101a0 | |||
| f6dcba025f | |||
| d6ec65d456 | |||
| 65d8074a07 | |||
| e3af61ca4a | |||
| a58f1268f0 | |||
| 41eacc4bc5 | |||
| aabb9dee13 | |||
| c855d75f35 | |||
| 8f5cdcf439 | |||
| 984559427e | |||
| 89494ced41 | |||
| ef764c2393 | |||
| 8624e2260d | |||
| aa011f4add | |||
| 3df61c9ab8 | |||
| a4516776d6 | |||
| 54d0ade997 | |||
| 3557fcd129 | |||
| 330b4a613c | |||
| 7ba3412aae | |||
| 6f60495d4d | |||
| 0b2eb8fb9e | |||
| 48af17e052 | |||
| b7b1055530 | |||
| e7029c0afd | |||
| cba3674ac0 | |||
| 865a549885 | |||
| 50dcf827a5 | |||
| f5fb582f83 | |||
| dbba502f83 | |||
| aae49f16a2 | |||
| 45d5f8c74d | |||
| 6cfd64e536 | |||
| c5cc404b3e | |||
| 42cbcc6ce3 | |||
| 812bdcd462 | |||
| f275409ee8 | |||
| 8994ac3727 | |||
| 7c5ff5e4d5 | |||
| c5e84d5469 | |||
| c143450dc6 | |||
| 07b95c2c4b | |||
| c30734f7f3 | |||
| 91f506c17b | |||
| 7a17695ad5 | |||
| f5076c87d4 | |||
| a47d6e1f3a | |||
| f6ff1abb00 | |||
| 386aaf6470 | |||
| 2b3c4cf0ff | |||
| b602e921d0 | |||
| 2fc3cdc2a2 | |||
| e2cadbfc30 | |||
| 3ffa935da7 | |||
| 5f539e331a | |||
| 356d0fabda | |||
| 122ec75cb6 | |||
| a3a48e1a49 | |||
| 4ede765e1f | |||
| 4fa181b346 | |||
| 4f76d91ae9 | |||
| 20d1759fa5 | |||
| 433e783ede | |||
| 47f47d916d | |||
| b31ac7d1fd | |||
| ea47fb7305 | |||
| 82170f8f1b | |||
| acb2655f58 | |||
| b1464517e6 | |||
| 151e6351f6 | |||
| 154f768281 | |||
| 90c857e8fc | |||
| 7a3efa2631 | |||
| 38cc767f27 | |||
| e1a718c78f | |||
| 32a4450e5e | |||
| fca3f606d2 | |||
| 4a0a934a76 | |||
| f7c406bec9 | |||
| f4807a6354 | |||
| 0960008b7b | |||
| 04a1aa38b4 | |||
| f84622efa1 | |||
| f6c4614275 | |||
| 7d36533524 | |||
| 5cd3df4869 | |||
| b0480f48f3 | |||
| 2e820c343a | |||
| ce927a2247 | |||
| ae810d59e9 | |||
| 1438ee52a1 | |||
| de4b3e55fa | |||
| d2cd78c5cb | |||
| d000719fa2 | |||
| efea4ed615 | |||
| 67a931c4b8 | |||
| bdcc5c0629 | |||
| d113cfc0ba | |||
| 4a3ab50878 | |||
| b39261c8cf | |||
| 7efb57c8da | |||
| 90c24cf356 | |||
| 54abada561 | |||
| f1922660be | |||
| 795e3c57da | |||
| 3f201464a5 | |||
| 8ac0be6bb5 | |||
| 130805e7bd | |||
| b8c7357fea | |||
| 819f8e338f | |||
| 9569e46ff8 | |||
| b7baab2d0f | |||
| e2d284797d | |||
| a3ac343fe2 | |||
| dadde96e41 | |||
| 99475c51e8 | |||
| cc9b4e26b5 | |||
| 32f232d3c0 | |||
| 235047ad0b | |||
| 228f75de0b | |||
| 2f89e7e2b4 | |||
| 437f39deb3 | |||
| 59582f16c4 | |||
| af9e3e38ce | |||
| d992702b87 | |||
| 6a9fe1128f | |||
| 573da29a4d | |||
| 00cff1a728 | |||
| 9bdeff0a39 | |||
| a1f263c048 | |||
| 346eac389c | |||
| f52c16b209 | |||
| 4faf880aa4 | |||
| f417a49b34 | |||
| 66fd713d12 | |||
| 2e7630f97e | |||
| 3f10524532 | |||
| 51f9826918 | |||
| f5bb76333b | |||
| 4947faa5ca | |||
| 101dc3a93c | |||
| bd3ee0fa24 | |||
| 2c52668a74 | |||
| 03edd8c96b | |||
| 37dfa41e01 | |||
| ea8a3d798e | |||
| 1df94fd84d | |||
| 5af957dc9c | |||
| 21073c627e | |||
| 66cdba9c1a | |||
| 56d3b38ce6 | |||
| 15d0275045 | |||
| 991c1a0137 | |||
| 7d549dbbd5 | |||
| e27c5583bb | |||
| 650c49637f | |||
| eb5dcf1c3e | |||
| ed2b61b709 | |||
| 41466a3018 | |||
| 2e130ef99d | |||
| a96fb39a82 | |||
| c9923c8d4b | |||
| 74b0ff338b | |||
| dcaccc2d7a | |||
| d60714e4e6 | |||
| d513d5d887 | |||
| 386566fd4b | |||
| 3357ca76fe | |||
| a183ce13ee | |||
| e9d0ed8e1e | |||
| 66f66fd14f | |||
| b49d30b477 | |||
| 73d83ec57e | |||
| efb39fb24b | |||
| 73623f2e92 | |||
| fbcc4cfa50 | |||
| 474a3548e0 | |||
| 2cdf68379b | |||
| cc8509f8eb | |||
| a520c1b1cb | |||
| 75fc2cbcfb | |||
| b8bb69f730 | |||
| b46d3e74d6 | |||
| 77a1613107 | |||
| 62fab7b09f | |||
| 5d87352b28 | |||
| ff60f5a381 | |||
| 7f666d9369 | |||
| 442f16dbd0 | |||
| 2dcab77ed1 | |||
| 13be04a169 | |||
| e3767c3a54 | |||
| ce957c8dd5 | |||
| 0606b2994c | |||
| 33acccbaaa | |||
| 1e097abe86 | |||
| e51705c41d | |||
| 7eafa661fe | |||
| 2fe323e587 |
@@ -1909,3 +1909,159 @@
|
|||||||
* Add ECDHE-RSA-AES128-SHA256 to cipher list
|
* Add ECDHE-RSA-AES128-SHA256 to cipher list
|
||||||
* Fix GPG signature verification
|
* Fix GPG signature verification
|
||||||
|
|
||||||
|
[5.1.5]
|
||||||
|
* Check for .well-known routes upstream as fallback. This broke nextcloud's caldav/carddav
|
||||||
|
|
||||||
|
[5.2.0]
|
||||||
|
* acme: request ECC certs
|
||||||
|
* less-strict DKIM check to allow users to set a stronger DKIM key
|
||||||
|
* Add members only flag to mailing list
|
||||||
|
* oauth: add backward compat layer for backup and uninstall
|
||||||
|
* fix bug in disk usage sorting
|
||||||
|
* mail: aliases can be across domains
|
||||||
|
* mail: allow an external MX to be set
|
||||||
|
* Add UI to download backup config as JSON (and import it)
|
||||||
|
* Ensure stopped apps are getting backed up
|
||||||
|
* Add OVH Object Storage backend
|
||||||
|
* Add per-app redis status and configuration to Services
|
||||||
|
* spam: large emails were not scanned
|
||||||
|
* mail relay: fix delivery event log
|
||||||
|
* manual update check always gets the latest updates
|
||||||
|
* graphs: fix issue where large number of apps would crash the box code (query param limit exceeded)
|
||||||
|
* backups: fix various security issues in encypted backups (thanks @mehdi)
|
||||||
|
* graphs: add app graphs
|
||||||
|
* older encrypted backups cannot be used in this version
|
||||||
|
* Add backup listing UI
|
||||||
|
* stopping an app will stop dependent services
|
||||||
|
* Add new wasabi s3 storage region us-east-2
|
||||||
|
* mail: Fix bug where SRS translation was done on the main domain instead of mailing list domain
|
||||||
|
* backups: add retention policy
|
||||||
|
* Drop `NET_RAW` caps from container preventing sniffing of network traffic
|
||||||
|
|
||||||
|
[5.2.1]
|
||||||
|
* Fix app disk graphs
|
||||||
|
* restart apps on addon container change
|
||||||
|
|
||||||
|
[5.2.2]
|
||||||
|
* regression: import UI
|
||||||
|
* Mbps -> MBps
|
||||||
|
* Remove verbose logs
|
||||||
|
* Set dmode in tar extract
|
||||||
|
* mail: fix crash in audit logs
|
||||||
|
* import: fix crash because encryption is unset
|
||||||
|
* create redis with the correct label
|
||||||
|
|
||||||
|
[5.2.3]
|
||||||
|
* Do not restart stopped apps
|
||||||
|
|
||||||
|
[5.2.4]
|
||||||
|
* mail: enable/disable incoming mail was showing an error
|
||||||
|
* Do not trigger backup of stopped apps. Instead, we will just retain it's existing backups
|
||||||
|
based on retention policy
|
||||||
|
* remove broken disk graphs
|
||||||
|
* fix OVH backups
|
||||||
|
|
||||||
|
[5.3.0]
|
||||||
|
* better nginx config for higher loads
|
||||||
|
* backups: add CIFS storage provider
|
||||||
|
* backups: add SSHFS storage provider
|
||||||
|
* backups: add NFS storage provider
|
||||||
|
* s3: use vhost style
|
||||||
|
* Fix crash when redis config was set
|
||||||
|
* Update schedule was unselected in the UI
|
||||||
|
* cloudron-setup: --provider is now optional
|
||||||
|
* show warning for unstable updates
|
||||||
|
* add forumUrl to app manifest
|
||||||
|
* postgresql: add unaccent extension for peertube
|
||||||
|
* mail: Add Auto-Submitted header to NDRs
|
||||||
|
* backups: ensure that the latest backup of installed apps is always preserved
|
||||||
|
* add nginx logs
|
||||||
|
* mail: make authentication case insensitive
|
||||||
|
* Fix timeout issues in postgresql and mysql addon
|
||||||
|
* Do not count stopped apps for memory use
|
||||||
|
* LDAP group synchronization
|
||||||
|
|
||||||
|
[5.3.1]
|
||||||
|
* better nginx config for higher loads
|
||||||
|
* backups: add CIFS storage provider
|
||||||
|
* backups: add SSHFS storage provider
|
||||||
|
* backups: add NFS storage provider
|
||||||
|
* s3: use vhost style
|
||||||
|
* Fix crash when redis config was set
|
||||||
|
* Update schedule was unselected in the UI
|
||||||
|
* cloudron-setup: --provider is now optional
|
||||||
|
* show warning for unstable updates
|
||||||
|
* add forumUrl to app manifest
|
||||||
|
* postgresql: add unaccent extension for peertube
|
||||||
|
* mail: Add Auto-Submitted header to NDRs
|
||||||
|
* backups: ensure that the latest backup of installed apps is always preserved
|
||||||
|
* add nginx logs
|
||||||
|
* mail: make authentication case insensitive
|
||||||
|
* Fix timeout issues in postgresql and mysql addon
|
||||||
|
* Do not count stopped apps for memory use
|
||||||
|
* LDAP group synchronization
|
||||||
|
|
||||||
|
[5.3.2]
|
||||||
|
* Do not install sshfs package
|
||||||
|
* 'provider' is not required anymore in various API calls
|
||||||
|
* redis: Set maxmemory and maxmemory-policy
|
||||||
|
* Add mlock capability to manifest (for vault app)
|
||||||
|
|
||||||
|
[5.3.3]
|
||||||
|
* Fix issue where some postinstall messages where causing angular to infinite loop
|
||||||
|
|
||||||
|
[5.3.4]
|
||||||
|
* Fix issue in database error handling
|
||||||
|
|
||||||
|
[5.4.0]
|
||||||
|
* Update nginx to 1.18 for various security fixes
|
||||||
|
* Add ping capability (for statping app)
|
||||||
|
* Fix bug where aliases were displayed incorrectly in SOGo
|
||||||
|
* Add univention as LDAP provider
|
||||||
|
* Bump max_connection for postgres addon to 200
|
||||||
|
* mail: Add pagination to mailing list API
|
||||||
|
* Allow admin to lock email and display name of users
|
||||||
|
* Allow admin to ensure all users have 2FA setup
|
||||||
|
* ami: fix regression where we didn't send provider as part of get status call
|
||||||
|
* nginx: hide version
|
||||||
|
* backups: add b2 provider
|
||||||
|
* Add filemanager webinterface
|
||||||
|
* Add darkmode
|
||||||
|
* Add note that password reset and invite links expire in 24 hours
|
||||||
|
|
||||||
|
[5.4.1]
|
||||||
|
* Update nginx to 1.18 for various security fixes
|
||||||
|
* Add ping capability (for statping app)
|
||||||
|
* Fix bug where aliases were displayed incorrectly in SOGo
|
||||||
|
* Add univention as LDAP provider
|
||||||
|
* Bump max_connection for postgres addon to 200
|
||||||
|
* mail: Add pagination to mailing list API
|
||||||
|
* Allow admin to lock email and display name of users
|
||||||
|
* Allow admin to ensure all users have 2FA setup
|
||||||
|
* ami: fix regression where we didn't send provider as part of get status call
|
||||||
|
* nginx: hide version
|
||||||
|
* backups: add b2 provider
|
||||||
|
* Add filemanager webinterface
|
||||||
|
* Add darkmode
|
||||||
|
* Add note that password reset and invite links expire in 24 hours
|
||||||
|
|
||||||
|
[5.5.0]
|
||||||
|
* postgresql: update to PostgreSQL 11
|
||||||
|
* postgresql: add citext extension to whitelist for loomio
|
||||||
|
* postgresql: add btree_gist,postgres_fdw,pg_stat_statements,plpgsql extensions for gitlab
|
||||||
|
* SFTP/Filebrowser: fix access of external data directories
|
||||||
|
* Fix contrast issues in dark mode
|
||||||
|
* Add option to delete mailbox data when mailbox is delete
|
||||||
|
* Allow days/hours of backups and updates to be configurable
|
||||||
|
* backup cleaner: fix issue where referenced backups where not counted against time periods
|
||||||
|
* route53: fix issue where verification failed if user had more than 100 zones
|
||||||
|
* rework task workers to run them in a separate cgroup
|
||||||
|
* backups: now much faster thanks to reworking of task worker
|
||||||
|
* When custom fallback cert is set, make sure it's used over LE certs
|
||||||
|
* mongodb: update to MongoDB 4.0.19
|
||||||
|
* List groups ordered by name
|
||||||
|
* Invite links are now valid for a week
|
||||||
|
* Update release GPG key
|
||||||
|
* Add pre-defined variables ($CLOUDRON_APPID) for better post install messages
|
||||||
|
* filemanager: show folder first
|
||||||
|
|
||||||
|
|||||||
@@ -48,18 +48,8 @@ the dashboard, database addons, graph container, base image etc. Cloudron also r
|
|||||||
on external services such as the App Store for apps to be installed. As such, don't
|
on external services such as the App Store for apps to be installed. As such, don't
|
||||||
clone this repo and npm install and expect something to work.
|
clone this repo and npm install and expect something to work.
|
||||||
|
|
||||||
## Documentation
|
## Support
|
||||||
|
|
||||||
* [Documentation](https://cloudron.io/documentation/)
|
* [Documentation](https://cloudron.io/documentation/)
|
||||||
|
|
||||||
## Related repos
|
|
||||||
|
|
||||||
The [base image repo](https://git.cloudron.io/cloudron/docker-base-image) is the parent image of all
|
|
||||||
the containers in the Cloudron.
|
|
||||||
|
|
||||||
## Community
|
|
||||||
|
|
||||||
* [Chat](https://chat.cloudron.io)
|
|
||||||
* [Forum](https://forum.cloudron.io/)
|
* [Forum](https://forum.cloudron.io/)
|
||||||
* [Support](mailto:support@cloudron.io)
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ set -euv -o pipefail
|
|||||||
|
|
||||||
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
readonly arg_provider="${1:-generic}"
|
readonly arg_infraversionpath="${SOURCE_DIR}/../src"
|
||||||
readonly arg_infraversionpath="${SOURCE_DIR}/${2:-}"
|
|
||||||
|
|
||||||
function die {
|
function die {
|
||||||
echo $1
|
echo $1
|
||||||
@@ -14,6 +13,9 @@ function die {
|
|||||||
|
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
readonly ubuntu_codename=$(lsb_release -cs)
|
||||||
|
readonly ubuntu_version=$(lsb_release -rs)
|
||||||
|
|
||||||
# hold grub since updating it breaks on some VPS providers. also, dist-upgrade will trigger it
|
# hold grub since updating it breaks on some VPS providers. also, dist-upgrade will trigger it
|
||||||
apt-mark hold grub* >/dev/null
|
apt-mark hold grub* >/dev/null
|
||||||
apt-get -o Dpkg::Options::="--force-confdef" update -y
|
apt-get -o Dpkg::Options::="--force-confdef" update -y
|
||||||
@@ -27,8 +29,6 @@ debconf-set-selections <<< 'mysql-server mysql-server/root_password_again passwo
|
|||||||
|
|
||||||
# this enables automatic security upgrades (https://help.ubuntu.com/community/AutomaticSecurityUpdates)
|
# this enables automatic security upgrades (https://help.ubuntu.com/community/AutomaticSecurityUpdates)
|
||||||
# resolvconf is needed for unbound to work property after disabling systemd-resolved in 18.04
|
# resolvconf is needed for unbound to work property after disabling systemd-resolved in 18.04
|
||||||
ubuntu_version=$(lsb_release -rs)
|
|
||||||
ubuntu_codename=$(lsb_release -cs)
|
|
||||||
gpg_package=$([[ "${ubuntu_version}" == "16.04" ]] && echo "gnupg" || echo "gpg")
|
gpg_package=$([[ "${ubuntu_version}" == "16.04" ]] && echo "gnupg" || echo "gpg")
|
||||||
apt-get -y install \
|
apt-get -y install \
|
||||||
acl \
|
acl \
|
||||||
@@ -53,16 +53,11 @@ apt-get -y install \
|
|||||||
unbound \
|
unbound \
|
||||||
xfsprogs
|
xfsprogs
|
||||||
|
|
||||||
if [[ "${ubuntu_version}" == "16.04" ]]; then
|
echo "==> installing nginx for xenial for TLSv3 support"
|
||||||
echo "==> installing nginx for xenial for TLSv3 support"
|
curl -sL http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/nginx_1.18.0-1~${ubuntu_codename}_amd64.deb -o /tmp/nginx.deb
|
||||||
|
# apt install with install deps (as opposed to dpkg -i)
|
||||||
curl -sL http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/nginx_1.14.0-1~xenial_amd64.deb -o /tmp/nginx.deb
|
apt install -y /tmp/nginx.deb
|
||||||
# apt install with install deps (as opposed to dpkg -i)
|
rm /tmp/nginx.deb
|
||||||
apt install -y /tmp/nginx.deb
|
|
||||||
rm /tmp/nginx.deb
|
|
||||||
else
|
|
||||||
apt install -y nginx-full
|
|
||||||
fi
|
|
||||||
|
|
||||||
# on some providers like scaleway the sudo file is changed and we want to keep the old one
|
# on some providers like scaleway the sudo file is changed and we want to keep the old one
|
||||||
apt-get -o Dpkg::Options::="--force-confold" install -y sudo
|
apt-get -o Dpkg::Options::="--force-confold" install -y sudo
|
||||||
@@ -73,7 +68,7 @@ cp /usr/share/unattended-upgrades/20auto-upgrades /etc/apt/apt.conf.d/20auto-upg
|
|||||||
|
|
||||||
echo "==> Installing node.js"
|
echo "==> Installing node.js"
|
||||||
mkdir -p /usr/local/node-10.18.1
|
mkdir -p /usr/local/node-10.18.1
|
||||||
curl -sL https://nodejs.org/dist/v10.18.1/node-v10.18.1-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-10.18.1
|
curl -sL https://nodejs.org/dist/v10.18.1/node-v10.18.1-linux-x64.tar.gz | tar zxf - --strip-components=1 -C /usr/local/node-10.18.1
|
||||||
ln -sf /usr/local/node-10.18.1/bin/node /usr/bin/node
|
ln -sf /usr/local/node-10.18.1/bin/node /usr/bin/node
|
||||||
ln -sf /usr/local/node-10.18.1/bin/npm /usr/bin/npm
|
ln -sf /usr/local/node-10.18.1/bin/npm /usr/bin/npm
|
||||||
apt-get install -y python # Install python which is required for npm rebuild
|
apt-get install -y python # Install python which is required for npm rebuild
|
||||||
|
|||||||
@@ -2,57 +2,60 @@
|
|||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// prefix all output with a timestamp
|
|
||||||
// debug() already prefixes and uses process.stderr NOT console.*
|
|
||||||
['log', 'info', 'warn', 'debug', 'error'].forEach(function (log) {
|
|
||||||
var orig = console[log];
|
|
||||||
console[log] = function () {
|
|
||||||
orig.apply(console, [new Date().toISOString()].concat(Array.prototype.slice.call(arguments)));
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
require('supererror')({ splatchError: true });
|
|
||||||
|
|
||||||
let async = require('async'),
|
let async = require('async'),
|
||||||
constants = require('./src/constants.js'),
|
|
||||||
dockerProxy = require('./src/dockerproxy.js'),
|
dockerProxy = require('./src/dockerproxy.js'),
|
||||||
|
fs = require('fs'),
|
||||||
ldap = require('./src/ldap.js'),
|
ldap = require('./src/ldap.js'),
|
||||||
server = require('./src/server.js');
|
paths = require('./src/paths.js'),
|
||||||
|
server = require('./src/server.js'),
|
||||||
|
util = require('util');
|
||||||
|
|
||||||
console.log();
|
const NOOP_CALLBACK = function () { };
|
||||||
console.log('==========================================');
|
|
||||||
console.log(` Cloudron ${constants.VERSION} `);
|
function setupLogging(callback) {
|
||||||
console.log('==========================================');
|
if (process.env.BOX_ENV === 'test') return callback();
|
||||||
console.log();
|
|
||||||
|
fs.open(paths.BOX_LOG_FILE, 'a', function (error, fd) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
require('debug').log = function (...args) {
|
||||||
|
fs.appendFileSync(fd, util.format(...args) + '\n');
|
||||||
|
};
|
||||||
|
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async.series([
|
async.series([
|
||||||
|
setupLogging,
|
||||||
server.start,
|
server.start,
|
||||||
ldap.start,
|
ldap.start,
|
||||||
dockerProxy.start
|
dockerProxy.start
|
||||||
], function (error) {
|
], function (error) {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error('Error starting server', error);
|
console.log('Error starting server', error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
console.log('Cloudron is up and running');
|
|
||||||
});
|
const debug = require('debug')('box:box'); // require this here so that logging handler is already setup
|
||||||
|
|
||||||
var NOOP_CALLBACK = function () { };
|
process.on('SIGINT', function () {
|
||||||
|
debug('Received SIGINT. Shutting down.');
|
||||||
process.on('SIGINT', function () {
|
|
||||||
console.log('Received SIGINT. Shutting down.');
|
server.stop(NOOP_CALLBACK);
|
||||||
|
ldap.stop(NOOP_CALLBACK);
|
||||||
server.stop(NOOP_CALLBACK);
|
dockerProxy.stop(NOOP_CALLBACK);
|
||||||
ldap.stop(NOOP_CALLBACK);
|
setTimeout(process.exit.bind(process), 3000);
|
||||||
dockerProxy.stop(NOOP_CALLBACK);
|
});
|
||||||
setTimeout(process.exit.bind(process), 3000);
|
|
||||||
});
|
process.on('SIGTERM', function () {
|
||||||
|
debug('Received SIGTERM. Shutting down.');
|
||||||
process.on('SIGTERM', function () {
|
|
||||||
console.log('Received SIGTERM. Shutting down.');
|
server.stop(NOOP_CALLBACK);
|
||||||
|
ldap.stop(NOOP_CALLBACK);
|
||||||
server.stop(NOOP_CALLBACK);
|
dockerProxy.stop(NOOP_CALLBACK);
|
||||||
ldap.stop(NOOP_CALLBACK);
|
setTimeout(process.exit.bind(process), 3000);
|
||||||
dockerProxy.stop(NOOP_CALLBACK);
|
});
|
||||||
setTimeout(process.exit.bind(process), 3000);
|
|
||||||
|
console.log(`Cloudron is up and running. Logs are at ${paths.BOX_LOG_FILE}`); // this goes to journalctl
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ exports.up = function(db, callback) {
|
|||||||
db.all('SELECT * FROM users WHERE admin=1', function (error, results) {
|
db.all('SELECT * FROM users WHERE admin=1', function (error, results) {
|
||||||
if (error) return done(error);
|
if (error) return done(error);
|
||||||
|
|
||||||
console.dir(results);
|
|
||||||
|
|
||||||
async.eachSeries(results, function (r, next) {
|
async.eachSeries(results, function (r, next) {
|
||||||
db.runSql('INSERT INTO groupMembers (groupId, userId) VALUES (?, ?)', [ ADMIN_GROUP_ID, r.id ], next);
|
db.runSql('INSERT INTO groupMembers (groupId, userId) VALUES (?, ?)', [ ADMIN_GROUP_ID, r.id ], next);
|
||||||
}, done);
|
}, done);
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
exports.up = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE mailboxes ADD COLUMN membersOnly BOOLEAN DEFAULT 0', function (error) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE users DROP COLUMN membersOnly', function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var async = require('async');
|
||||||
|
|
||||||
|
exports.up = function(db, callback) {
|
||||||
|
async.series([
|
||||||
|
db.runSql.bind(db, 'ALTER TABLE mailboxes ADD COLUMN aliasDomain VARCHAR(128)'),
|
||||||
|
function setAliasDomain(done) {
|
||||||
|
db.all('SELECT * FROM mailboxes', function (error, mailboxes) {
|
||||||
|
async.eachSeries(mailboxes, function (mailbox, iteratorDone) {
|
||||||
|
if (!mailbox.aliasTarget) return iteratorDone();
|
||||||
|
|
||||||
|
db.runSql('UPDATE mailboxes SET aliasDomain=? WHERE name=? AND domain=?', [ mailbox.domain, mailbox.name, mailbox.domain ], iteratorDone);
|
||||||
|
}, done);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
db.runSql.bind(db, 'ALTER TABLE mailboxes ADD CONSTRAINT mailboxes_aliasDomain_constraint FOREIGN KEY(aliasDomain) REFERENCES mail(domain)'),
|
||||||
|
db.runSql.bind(db, 'ALTER TABLE mailboxes CHANGE aliasTarget aliasName VARCHAR(128)')
|
||||||
|
], callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(db, callback) {
|
||||||
|
async.series([
|
||||||
|
db.runSql.bind(db, 'ALTER TABLE mailboxes DROP FOREIGN KEY mailboxes_aliasDomain_constraint'),
|
||||||
|
db.runSql.bind(db, 'ALTER TABLE mailboxes DROP COLUMN aliasDomain'),
|
||||||
|
db.runSql.bind(db, 'ALTER TABLE mailboxes CHANGE aliasName aliasTarget VARCHAR(128)')
|
||||||
|
], callback);
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
exports.up = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE apps ADD COLUMN servicesConfigJson TEXT', function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE apps DROP COLUMN servicesConfigJson', function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
exports.up = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE apps ADD COLUMN bindsJson TEXT', function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE apps DROP COLUMN bindsJson', function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const backups = require('../src/backups.js'),
|
||||||
|
fs = require('fs');
|
||||||
|
|
||||||
|
exports.up = function(db, callback) {
|
||||||
|
db.all('SELECT value FROM settings WHERE name="backup_config"', function (error, results) {
|
||||||
|
if (error || results.length === 0) return callback(error);
|
||||||
|
|
||||||
|
var backupConfig = JSON.parse(results[0].value);
|
||||||
|
if (backupConfig.key) {
|
||||||
|
backupConfig.encryption = backups.generateEncryptionKeysSync(backupConfig.key);
|
||||||
|
backups.cleanupCacheFilesSync();
|
||||||
|
|
||||||
|
fs.writeFileSync('/home/yellowtent/platformdata/BACKUP_PASSWORD',
|
||||||
|
'This file contains your Cloudron backup password.\nBefore Cloudron v5.2, this was saved in the database.' +
|
||||||
|
'From Cloudron 5.2, this password is not required anymore. We generate strong keys based off this password and use those keys to encrypt the backups.\n' +
|
||||||
|
'This means that the password is only required at decryption/restore time.\n\n' +
|
||||||
|
'This file can be safely removed and only exists for the off-chance that you do not remember your backup password.\n\n' +
|
||||||
|
`Password: ${backupConfig.key}\n`,
|
||||||
|
'utf8');
|
||||||
|
|
||||||
|
} else {
|
||||||
|
backupConfig.encryption = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete backupConfig.key;
|
||||||
|
|
||||||
|
db.runSql('UPDATE settings SET value=? WHERE name="backup_config"', [ JSON.stringify(backupConfig) ], callback);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(db, callback) {
|
||||||
|
callback();
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
exports.up = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE backups CHANGE version packageVersion VARCHAR(128) NOT NULL', [], function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE backups CHANGE packageVersion version VARCHAR(128) NOT NULL', [], function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
exports.up = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE backups ADD COLUMN encryptionVersion INTEGER', function (error) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
db.all('SELECT value FROM settings WHERE name="backup_config"', function (error, results) {
|
||||||
|
if (error || results.length === 0) return callback(error);
|
||||||
|
|
||||||
|
var backupConfig = JSON.parse(results[0].value);
|
||||||
|
if (!backupConfig.encryption) return callback(null);
|
||||||
|
|
||||||
|
// mark old encrypted backups as v1
|
||||||
|
db.runSql('UPDATE backups SET encryptionVersion=1', callback);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE backups DROP COLUMN encryptionVersion', function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
exports.up = function(db, callback) {
|
||||||
|
db.all('SELECT value FROM settings WHERE name="backup_config"', function (error, results) {
|
||||||
|
if (error || results.length === 0) return callback(error);
|
||||||
|
|
||||||
|
var backupConfig = JSON.parse(results[0].value);
|
||||||
|
backupConfig.retentionPolicy = { keepWithinSecs: backupConfig.retentionSecs };
|
||||||
|
delete backupConfig.retentionSecs;
|
||||||
|
|
||||||
|
// mark old encrypted backups as v1
|
||||||
|
db.runSql('UPDATE settings SET value=? WHERE name="backup_config"', [ JSON.stringify(backupConfig) ], callback);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(db, callback) {
|
||||||
|
callback();
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
exports.up = function(db, callback) {
|
||||||
|
db.all('SELECT value FROM settings WHERE name="backup_config"', function (error, results) {
|
||||||
|
if (error || results.length === 0) return callback(error);
|
||||||
|
|
||||||
|
var backupConfig = JSON.parse(results[0].value);
|
||||||
|
if (backupConfig.provider !== 'minio' && backupConfig.provider !== 's3-v4-compat') return callback();
|
||||||
|
|
||||||
|
backupConfig.s3ForcePathStyle = true; // usually minio is self-hosted. s3 v4 compat, we don't know
|
||||||
|
|
||||||
|
db.runSql('UPDATE settings SET value=? WHERE name="backup_config"', [ JSON.stringify(backupConfig) ], callback);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(db, callback) {
|
||||||
|
callback();
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var async = require('async');
|
||||||
|
|
||||||
|
exports.up = function(db, callback) {
|
||||||
|
|
||||||
|
// http://stackoverflow.com/questions/386294/what-is-the-maximum-length-of-a-valid-email-address
|
||||||
|
|
||||||
|
async.series([
|
||||||
|
db.runSql.bind(db, 'ALTER TABLE appPasswords DROP INDEX name'),
|
||||||
|
db.runSql.bind(db, 'ALTER TABLE appPasswords ADD CONSTRAINT appPasswords_name_userId_identifier UNIQUE (name, userId, identifier)'),
|
||||||
|
], callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(db, callback) {
|
||||||
|
callback();
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
exports.up = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE userGroups ADD COLUMN source VARCHAR(128) DEFAULT ""', function (error) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE userGroups DROP COLUMN source', function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const async = require('async');
|
||||||
|
|
||||||
|
exports.up = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE backups ADD COLUMN identifier VARCHAR(128)', function (error) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
|
||||||
|
db.all('SELECT * FROM backups', function (error, backups) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
async.eachSeries(backups, function (backup, next) {
|
||||||
|
let identifier = 'unknown';
|
||||||
|
|
||||||
|
if (backup.type === 'box') {
|
||||||
|
identifier = 'box';
|
||||||
|
} else {
|
||||||
|
const match = backup.id.match(/app_(.+?)_.+/);
|
||||||
|
if (match) identifier = match[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
db.runSql('UPDATE backups SET identifier=? WHERE id=?', [ identifier, backup.id ], next);
|
||||||
|
}, function (error) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
db.runSql('ALTER TABLE backups MODIFY COLUMN identifier VARCHAR(128) NOT NULL', callback);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE backups DROP COLUMN identifier', function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
exports.up = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE users ADD COLUMN ts TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP', function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
|
||||||
|
db.runSql('ALTER TABLE users DROP COLUMN modifiedAt', callback);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE users DROP COLUMN ts', function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
exports.up = function(db, callback) {
|
||||||
|
db.all('SELECT value FROM settings WHERE name="backup_config"', function (error, results) {
|
||||||
|
if (error || results.length === 0) return callback(error);
|
||||||
|
|
||||||
|
var backupConfig = JSON.parse(results[0].value);
|
||||||
|
if (backupConfig.intervalSecs === 6 * 60 * 60) { // every 6 hours
|
||||||
|
backupConfig.schedulePattern = '00 00 5,11,17,23 * * *';
|
||||||
|
} else if (backupConfig.intervalSecs === 12 * 60 * 60) { // every 12 hours
|
||||||
|
backupConfig.schedulePattern = '00 00 5,17 * * *';
|
||||||
|
} else if (backupConfig.intervalSecs === 24 * 60 * 60) { // every day
|
||||||
|
backupConfig.schedulePattern = '00 00 23 * * *';
|
||||||
|
} else if (backupConfig.intervalSecs === 3 * 24 * 60 * 60) { // every 3 days (based on day)
|
||||||
|
backupConfig.schedulePattern = '00 00 23 * * 1,3,5';
|
||||||
|
} else if (backupConfig.intervalSecs === 7 * 24 * 60 * 60) { // every week (saturday)
|
||||||
|
backupConfig.schedulePattern = '00 00 23 * * 6';
|
||||||
|
} else { // default to everyday
|
||||||
|
backupConfig.schedulePattern = '00 00 23 * * *';
|
||||||
|
}
|
||||||
|
|
||||||
|
delete backupConfig.intervalSecs;
|
||||||
|
db.runSql('UPDATE settings SET value=? WHERE name="backup_config"', [ JSON.stringify(backupConfig) ], callback);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(db, callback) {
|
||||||
|
callback();
|
||||||
|
};
|
||||||
+13
-5
@@ -21,7 +21,7 @@ CREATE TABLE IF NOT EXISTS users(
|
|||||||
password VARCHAR(1024) NOT NULL,
|
password VARCHAR(1024) NOT NULL,
|
||||||
salt VARCHAR(512) NOT NULL,
|
salt VARCHAR(512) NOT NULL,
|
||||||
createdAt VARCHAR(512) NOT NULL,
|
createdAt VARCHAR(512) NOT NULL,
|
||||||
modifiedAt VARCHAR(512) NOT NULL,
|
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
displayName VARCHAR(512) DEFAULT "",
|
displayName VARCHAR(512) DEFAULT "",
|
||||||
fallbackEmail VARCHAR(512) DEFAULT "",
|
fallbackEmail VARCHAR(512) DEFAULT "",
|
||||||
twoFactorAuthenticationSecret VARCHAR(128) DEFAULT "",
|
twoFactorAuthenticationSecret VARCHAR(128) DEFAULT "",
|
||||||
@@ -37,6 +37,7 @@ CREATE TABLE IF NOT EXISTS users(
|
|||||||
CREATE TABLE IF NOT EXISTS userGroups(
|
CREATE TABLE IF NOT EXISTS userGroups(
|
||||||
id VARCHAR(128) NOT NULL UNIQUE,
|
id VARCHAR(128) NOT NULL UNIQUE,
|
||||||
name VARCHAR(254) NOT NULL UNIQUE,
|
name VARCHAR(254) NOT NULL UNIQUE,
|
||||||
|
source VARCHAR(128) DEFAULT "",
|
||||||
PRIMARY KEY(id));
|
PRIMARY KEY(id));
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS groupMembers(
|
CREATE TABLE IF NOT EXISTS groupMembers(
|
||||||
@@ -86,6 +87,8 @@ CREATE TABLE IF NOT EXISTS apps(
|
|||||||
dataDir VARCHAR(256) UNIQUE,
|
dataDir VARCHAR(256) UNIQUE,
|
||||||
taskId INTEGER, // current task
|
taskId INTEGER, // current task
|
||||||
errorJson TEXT,
|
errorJson TEXT,
|
||||||
|
bindsJson TEXT, // bind mounts
|
||||||
|
servicesConfigJson TEXT, // app services configuration
|
||||||
|
|
||||||
FOREIGN KEY(mailboxDomain) REFERENCES domains(domain),
|
FOREIGN KEY(mailboxDomain) REFERENCES domains(domain),
|
||||||
FOREIGN KEY(taskId) REFERENCES tasks(id),
|
FOREIGN KEY(taskId) REFERENCES tasks(id),
|
||||||
@@ -120,8 +123,10 @@ CREATE TABLE IF NOT EXISTS appEnvVars(
|
|||||||
CREATE TABLE IF NOT EXISTS backups(
|
CREATE TABLE IF NOT EXISTS backups(
|
||||||
id VARCHAR(128) NOT NULL,
|
id VARCHAR(128) NOT NULL,
|
||||||
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
version VARCHAR(128) NOT NULL, /* app version or box version */
|
packageVersion VARCHAR(128) NOT NULL, /* app version or box version */
|
||||||
|
encryptionVersion INTEGER, /* when null, unencrypted backup */
|
||||||
type VARCHAR(16) NOT NULL, /* 'box' or 'app' */
|
type VARCHAR(16) NOT NULL, /* 'box' or 'app' */
|
||||||
|
identifier VARCHAR(128) NOT NULL, /* 'box' or the app id */
|
||||||
dependsOn TEXT, /* comma separate list of objects this backup depends on */
|
dependsOn TEXT, /* comma separate list of objects this backup depends on */
|
||||||
state VARCHAR(16) NOT NULL,
|
state VARCHAR(16) NOT NULL,
|
||||||
manifestJson TEXT, /* to validate if the app can be installed in this version of box */
|
manifestJson TEXT, /* to validate if the app can be installed in this version of box */
|
||||||
@@ -177,12 +182,15 @@ CREATE TABLE IF NOT EXISTS mailboxes(
|
|||||||
name VARCHAR(128) NOT NULL,
|
name VARCHAR(128) NOT NULL,
|
||||||
type VARCHAR(16) NOT NULL, /* 'mailbox', 'alias', 'list' */
|
type VARCHAR(16) NOT NULL, /* 'mailbox', 'alias', 'list' */
|
||||||
ownerId VARCHAR(128) NOT NULL, /* user id */
|
ownerId VARCHAR(128) NOT NULL, /* user id */
|
||||||
aliasTarget VARCHAR(128), /* the target name type is an alias */
|
aliasName VARCHAR(128), /* the target name type is an alias */
|
||||||
|
aliasDomain VARCHAR(128), /* the target domain */
|
||||||
membersJson TEXT, /* members of a group. fully qualified */
|
membersJson TEXT, /* members of a group. fully qualified */
|
||||||
|
membersOnly BOOLEAN DEFAULT false,
|
||||||
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
domain VARCHAR(128),
|
domain VARCHAR(128),
|
||||||
|
|
||||||
FOREIGN KEY(domain) REFERENCES mail(domain),
|
FOREIGN KEY(domain) REFERENCES mail(domain),
|
||||||
|
FOREIGN KEY(aliasDomain) REFERENCES mail(domain),
|
||||||
UNIQUE (name, domain));
|
UNIQUE (name, domain));
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS subdomains(
|
CREATE TABLE IF NOT EXISTS subdomains(
|
||||||
@@ -214,7 +222,7 @@ CREATE TABLE IF NOT EXISTS notifications(
|
|||||||
message TEXT,
|
message TEXT,
|
||||||
acknowledged BOOLEAN DEFAULT false,
|
acknowledged BOOLEAN DEFAULT false,
|
||||||
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY appPasswords_name_appId_identifier (name, userId, identifier),
|
||||||
PRIMARY KEY (id)
|
PRIMARY KEY (id)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -226,7 +234,7 @@ CREATE TABLE IF NOT EXISTS appPasswords(
|
|||||||
hashedPassword VARCHAR(1024) NOT NULL,
|
hashedPassword VARCHAR(1024) NOT NULL,
|
||||||
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY(userId) REFERENCES users(id),
|
FOREIGN KEY(userId) REFERENCES users(id),
|
||||||
UNIQUE (name, userId),
|
|
||||||
PRIMARY KEY (id)
|
PRIMARY KEY (id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Generated
+360
-326
File diff suppressed because it is too large
Load Diff
+21
-21
@@ -18,32 +18,33 @@
|
|||||||
"@google-cloud/storage": "^2.5.0",
|
"@google-cloud/storage": "^2.5.0",
|
||||||
"@sindresorhus/df": "git+https://github.com/cloudron-io/df.git#type",
|
"@sindresorhus/df": "git+https://github.com/cloudron-io/df.git#type",
|
||||||
"async": "^2.6.3",
|
"async": "^2.6.3",
|
||||||
"aws-sdk": "^2.610.0",
|
"aws-sdk": "^2.685.0",
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
"cloudron-manifestformat": "^5.1.1",
|
"cloudron-manifestformat": "^5.5.0",
|
||||||
"connect": "^3.7.0",
|
"connect": "^3.7.0",
|
||||||
"connect-lastmile": "^1.2.2",
|
"connect-lastmile": "^2.0.0",
|
||||||
"connect-timeout": "^1.9.0",
|
"connect-timeout": "^1.9.0",
|
||||||
"cookie-session": "^1.4.0",
|
"cookie-session": "^1.4.0",
|
||||||
"cron": "^1.8.2",
|
"cron": "^1.8.2",
|
||||||
"db-migrate": "^0.11.6",
|
"db-migrate": "^0.11.11",
|
||||||
"db-migrate-mysql": "^1.1.10",
|
"db-migrate-mysql": "^1.1.10",
|
||||||
"debug": "^4.1.1",
|
"debug": "^4.1.1",
|
||||||
"dockerode": "^2.5.8",
|
"dockerode": "^2.5.8",
|
||||||
"ejs": "^2.6.1",
|
"ejs": "^2.6.1",
|
||||||
"ejs-cli": "^2.1.1",
|
"ejs-cli": "^2.2.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"js-yaml": "^3.13.1",
|
"js-yaml": "^3.14.0",
|
||||||
"json": "^9.0.6",
|
"json": "^9.0.6",
|
||||||
"ldapjs": "^1.0.2",
|
"ldapjs": "^1.0.2",
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.15",
|
||||||
"lodash.chunk": "^4.2.0",
|
"lodash.chunk": "^4.2.0",
|
||||||
"mime": "^2.4.4",
|
"mime": "^2.4.6",
|
||||||
"moment-timezone": "^0.5.27",
|
"moment": "^2.26.0",
|
||||||
"morgan": "^1.9.1",
|
"moment-timezone": "^0.5.31",
|
||||||
|
"morgan": "^1.10.0",
|
||||||
"multiparty": "^4.2.1",
|
"multiparty": "^4.2.1",
|
||||||
"mysql": "^2.18.1",
|
"mysql": "^2.18.1",
|
||||||
"nodemailer": "^6.4.2",
|
"nodemailer": "^6.4.6",
|
||||||
"nodemailer-smtp-transport": "^2.7.4",
|
"nodemailer-smtp-transport": "^2.7.4",
|
||||||
"once": "^1.4.0",
|
"once": "^1.4.0",
|
||||||
"parse-links": "^0.1.0",
|
"parse-links": "^0.1.0",
|
||||||
@@ -51,34 +52,33 @@
|
|||||||
"progress-stream": "^2.0.0",
|
"progress-stream": "^2.0.0",
|
||||||
"proxy-middleware": "^0.15.0",
|
"proxy-middleware": "^0.15.0",
|
||||||
"qrcode": "^1.4.4",
|
"qrcode": "^1.4.4",
|
||||||
"readdirp": "^3.3.0",
|
"readdirp": "^3.4.0",
|
||||||
"request": "^2.88.0",
|
"request": "^2.88.2",
|
||||||
"rimraf": "^2.6.3",
|
"rimraf": "^2.6.3",
|
||||||
"s3-block-read-stream": "^0.5.0",
|
"s3-block-read-stream": "^0.5.0",
|
||||||
"safetydance": "^1.0.0",
|
"safetydance": "^1.1.1",
|
||||||
"semver": "^6.1.1",
|
"semver": "^6.1.1",
|
||||||
"showdown": "^1.9.1",
|
"showdown": "^1.9.1",
|
||||||
"speakeasy": "^2.0.0",
|
"speakeasy": "^2.0.0",
|
||||||
"split": "^1.0.1",
|
"split": "^1.0.1",
|
||||||
"superagent": "^5.2.1",
|
"superagent": "^5.2.2",
|
||||||
"supererror": "^0.7.2",
|
|
||||||
"tar-fs": "github:cloudron-io/tar-fs#ignore_stat_error",
|
"tar-fs": "github:cloudron-io/tar-fs#ignore_stat_error",
|
||||||
"tar-stream": "^2.1.0",
|
"tar-stream": "^2.1.2",
|
||||||
"tldjs": "^2.3.1",
|
"tldjs": "^2.3.1",
|
||||||
"underscore": "^1.9.2",
|
"underscore": "^1.10.2",
|
||||||
"uuid": "^3.4.0",
|
"uuid": "^3.4.0",
|
||||||
"validator": "^11.0.0",
|
"validator": "^11.0.0",
|
||||||
"ws": "^7.2.1",
|
"ws": "^7.3.0",
|
||||||
"xml2js": "^0.4.23"
|
"xml2js": "^0.4.23"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"expect.js": "*",
|
"expect.js": "*",
|
||||||
"hock": "^1.3.3",
|
"hock": "^1.4.1",
|
||||||
"js2xmlparser": "^4.0.0",
|
"js2xmlparser": "^4.0.1",
|
||||||
"mocha": "^6.1.4",
|
"mocha": "^6.1.4",
|
||||||
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
|
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
|
||||||
"nock": "^10.0.6",
|
"nock": "^10.0.6",
|
||||||
"node-sass": "^4.12.0",
|
"node-sass": "^4.14.1",
|
||||||
"recursive-readdir": "^2.2.2"
|
"recursive-readdir": "^2.2.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+7
-59
@@ -41,16 +41,14 @@ if systemctl -q is-active box; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
initBaseImage="true"
|
initBaseImage="true"
|
||||||
# provisioning data
|
provider="generic"
|
||||||
provider=""
|
|
||||||
requestedVersion=""
|
requestedVersion=""
|
||||||
apiServerOrigin="https://api.cloudron.io"
|
apiServerOrigin="https://api.cloudron.io"
|
||||||
webServerOrigin="https://cloudron.io"
|
webServerOrigin="https://cloudron.io"
|
||||||
sourceTarballUrl=""
|
sourceTarballUrl=""
|
||||||
rebootServer="true"
|
rebootServer="true"
|
||||||
license=""
|
|
||||||
|
|
||||||
args=$(getopt -o "" -l "help,skip-baseimage-init,provider:,version:,env:,skip-reboot,license:" -n "$0" -- "$@")
|
args=$(getopt -o "" -l "help,skip-baseimage-init,provider:,version:,env:,skip-reboot" -n "$0" -- "$@")
|
||||||
eval set -- "${args}"
|
eval set -- "${args}"
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
@@ -67,7 +65,6 @@ while true; do
|
|||||||
webServerOrigin="https://staging.cloudron.io"
|
webServerOrigin="https://staging.cloudron.io"
|
||||||
fi
|
fi
|
||||||
shift 2;;
|
shift 2;;
|
||||||
--license) license="$2"; shift 2;;
|
|
||||||
--skip-baseimage-init) initBaseImage="false"; shift;;
|
--skip-baseimage-init) initBaseImage="false"; shift;;
|
||||||
--skip-reboot) rebootServer="false"; shift;;
|
--skip-reboot) rebootServer="false"; shift;;
|
||||||
--) break;;
|
--) break;;
|
||||||
@@ -91,48 +88,6 @@ fi
|
|||||||
# Can only write after we have confirmed script has root access
|
# Can only write after we have confirmed script has root access
|
||||||
echo "Running cloudron-setup with args : $@" > "${LOG_FILE}"
|
echo "Running cloudron-setup with args : $@" > "${LOG_FILE}"
|
||||||
|
|
||||||
# validate arguments in the absence of data
|
|
||||||
readonly AVAILABLE_PROVIDERS="azure, caas, cloudscale, contabo, digitalocean, ec2, exoscale, gce, hetzner, interox, lightsail, linode, netcup, ovh, rosehosting, scaleway, skysilk, time4vps, upcloud, vultr or generic"
|
|
||||||
if [[ -z "${provider}" ]]; then
|
|
||||||
echo "--provider is required ($AVAILABLE_PROVIDERS)"
|
|
||||||
exit 1
|
|
||||||
elif [[ \
|
|
||||||
"${provider}" != "ami" && \
|
|
||||||
"${provider}" != "azure" && \
|
|
||||||
"${provider}" != "azure-image" && \
|
|
||||||
"${provider}" != "caas" && \
|
|
||||||
"${provider}" != "cloudscale" && \
|
|
||||||
"${provider}" != "contabo" && \
|
|
||||||
"${provider}" != "digitalocean" && \
|
|
||||||
"${provider}" != "digitalocean-mp" && \
|
|
||||||
"${provider}" != "ec2" && \
|
|
||||||
"${provider}" != "exoscale" && \
|
|
||||||
"${provider}" != "gce" && \
|
|
||||||
"${provider}" != "hetzner" && \
|
|
||||||
"${provider}" != "interox" && \
|
|
||||||
"${provider}" != "interox-image" && \
|
|
||||||
"${provider}" != "lightsail" && \
|
|
||||||
"${provider}" != "linode" && \
|
|
||||||
"${provider}" != "linode-oneclick" && \
|
|
||||||
"${provider}" != "linode-stackscript" && \
|
|
||||||
"${provider}" != "netcup" && \
|
|
||||||
"${provider}" != "netcup-image" && \
|
|
||||||
"${provider}" != "ovh" && \
|
|
||||||
"${provider}" != "rosehosting" && \
|
|
||||||
"${provider}" != "scaleway" && \
|
|
||||||
"${provider}" != "skysilk" && \
|
|
||||||
"${provider}" != "skysilk-image" && \
|
|
||||||
"${provider}" != "time4vps" && \
|
|
||||||
"${provider}" != "time4vps-image" && \
|
|
||||||
"${provider}" != "upcloud" && \
|
|
||||||
"${provider}" != "upcloud-image" && \
|
|
||||||
"${provider}" != "vultr" && \
|
|
||||||
"${provider}" != "generic" \
|
|
||||||
]]; then
|
|
||||||
echo "--provider must be one of: $AVAILABLE_PROVIDERS"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "##############################################"
|
echo "##############################################"
|
||||||
echo " Cloudron Setup (${requestedVersion:-latest})"
|
echo " Cloudron Setup (${requestedVersion:-latest})"
|
||||||
@@ -151,12 +106,6 @@ if [[ "${initBaseImage}" == "true" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "=> Ensure required apt sources"
|
|
||||||
if ! add-apt-repository universe &>> "${LOG_FILE}"; then
|
|
||||||
echo "Could not add required apt sources (for nginx-full). See ${LOG_FILE}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "=> Updating apt and installing script dependencies"
|
echo "=> Updating apt and installing script dependencies"
|
||||||
if ! apt-get update &>> "${LOG_FILE}"; then
|
if ! apt-get update &>> "${LOG_FILE}"; then
|
||||||
echo "Could not update package repositories. See ${LOG_FILE}"
|
echo "Could not update package repositories. See ${LOG_FILE}"
|
||||||
@@ -196,20 +145,19 @@ fi
|
|||||||
|
|
||||||
if [[ "${initBaseImage}" == "true" ]]; then
|
if [[ "${initBaseImage}" == "true" ]]; then
|
||||||
echo -n "=> Installing base dependencies and downloading docker images (this takes some time) ..."
|
echo -n "=> Installing base dependencies and downloading docker images (this takes some time) ..."
|
||||||
if ! /bin/bash "${box_src_tmp_dir}/baseimage/initializeBaseUbuntuImage.sh" "${provider}" "../src" &>> "${LOG_FILE}"; then
|
# initializeBaseUbuntuImage.sh args (provider, infraversion path) are only to support installation of pre 5.3 Cloudrons
|
||||||
|
if ! /bin/bash "${box_src_tmp_dir}/baseimage/initializeBaseUbuntuImage.sh" "generic" "../src" &>> "${LOG_FILE}"; then
|
||||||
echo "Init script failed. See ${LOG_FILE} for details"
|
echo "Init script failed. See ${LOG_FILE} for details"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# NOTE: this install script only supports 4.2 and above
|
# The provider flag is still used for marketplace images
|
||||||
echo "=> Installing version ${version} (this takes some time) ..."
|
echo "=> Installing version ${version} (this takes some time) ..."
|
||||||
mkdir -p /etc/cloudron
|
mkdir -p /etc/cloudron
|
||||||
echo "${provider}" > /etc/cloudron/PROVIDER
|
echo "${provider}" > /etc/cloudron/PROVIDER
|
||||||
|
|
||||||
[[ -n "${license}" ]] && echo -n "$license" > /etc/cloudron/LICENSE
|
|
||||||
|
|
||||||
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" &>> "${LOG_FILE}"; then
|
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" &>> "${LOG_FILE}"; then
|
||||||
echo "Failed to install cloudron. See ${LOG_FILE} for details"
|
echo "Failed to install cloudron. See ${LOG_FILE} for details"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -221,13 +169,13 @@ mysql -uroot -ppassword -e "REPLACE INTO box.settings (name, value) VALUES ('web
|
|||||||
echo -n "=> Waiting for cloudron to be ready (this takes some time) ..."
|
echo -n "=> Waiting for cloudron to be ready (this takes some time) ..."
|
||||||
while true; do
|
while true; do
|
||||||
echo -n "."
|
echo -n "."
|
||||||
if status=$($curl -q -f "http://localhost:3000/api/v1/cloudron/status" 2>/dev/null); then
|
if status=$($curl -s -f "http://localhost:3000/api/v1/cloudron/status" 2>/dev/null); then
|
||||||
break # we are up and running
|
break # we are up and running
|
||||||
fi
|
fi
|
||||||
sleep 10
|
sleep 10
|
||||||
done
|
done
|
||||||
|
|
||||||
if ! ip=$(curl --fail --connect-timeout 2 --max-time 2 -q https://api.cloudron.io/api/v1/helper/public_ip | sed -n -e 's/.*"ip": "\(.*\)"/\1/p'); then
|
if ! ip=$(curl -s --fail --connect-timeout 2 --max-time 2 https://api.cloudron.io/api/v1/helper/public_ip | sed -n -e 's/.*"ip": "\(.*\)"/\1/p'); then
|
||||||
ip='<IP>'
|
ip='<IP>'
|
||||||
fi
|
fi
|
||||||
echo -e "\n\n${GREEN}Visit https://${ip} and accept the self-signed certificate to finish setup.${DONE}\n"
|
echo -e "\n\n${GREEN}Visit https://${ip} and accept the self-signed certificate to finish setup.${DONE}\n"
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ echo -e $LINE"Backup stats (possibly misleading)"$LINE >> $OUT
|
|||||||
du -hcsL /var/backups/* &>> $OUT || true
|
du -hcsL /var/backups/* &>> $OUT || true
|
||||||
|
|
||||||
echo -e $LINE"System daemon status"$LINE >> $OUT
|
echo -e $LINE"System daemon status"$LINE >> $OUT
|
||||||
systemctl status --lines=100 cloudron.target box mysql unbound cloudron-syslog nginx collectd docker &>> $OUT
|
systemctl status --lines=100 box mysql unbound cloudron-syslog nginx collectd docker &>> $OUT
|
||||||
|
|
||||||
echo -e $LINE"Box logs"$LINE >> $OUT
|
echo -e $LINE"Box logs"$LINE >> $OUT
|
||||||
tail -n 100 /home/yellowtent/platformdata/logs/box.log &>> $OUT
|
tail -n 100 /home/yellowtent/platformdata/logs/box.log &>> $OUT
|
||||||
@@ -112,7 +112,7 @@ if [[ "${enableSSH}" == "true" ]]; then
|
|||||||
permit_root_login=$(grep -q ^PermitRootLogin.*yes /etc/ssh/sshd_config && echo "yes" || echo "no")
|
permit_root_login=$(grep -q ^PermitRootLogin.*yes /etc/ssh/sshd_config && echo "yes" || echo "no")
|
||||||
|
|
||||||
# support.js uses similar logic
|
# support.js uses similar logic
|
||||||
if $(grep -q "ec2\|lightsail\|ami" /etc/cloudron/PROVIDER); then
|
if [[ -d /home/ubuntu ]]; then
|
||||||
ssh_user="ubuntu"
|
ssh_user="ubuntu"
|
||||||
keys_file="/home/ubuntu/.ssh/authorized_keys"
|
keys_file="/home/ubuntu/.ssh/authorized_keys"
|
||||||
else
|
else
|
||||||
|
|||||||
+16
-15
@@ -11,9 +11,8 @@ if [[ ${EUID} -ne 0 ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
readonly USER=yellowtent
|
readonly user=yellowtent
|
||||||
readonly BOX_SRC_DIR=/home/${USER}/box
|
readonly box_src_dir=/home/${user}/box
|
||||||
readonly BASE_DATA_DIR=/home/${USER}
|
|
||||||
|
|
||||||
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
|
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
|
||||||
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
@@ -24,6 +23,8 @@ readonly ubuntu_codename=$(lsb_release -cs)
|
|||||||
|
|
||||||
readonly is_update=$(systemctl is-active box && echo "yes" || echo "no")
|
readonly is_update=$(systemctl is-active box && echo "yes" || echo "no")
|
||||||
|
|
||||||
|
echo "==> installer: Updating from $(cat $box_src_dir/VERSION) to $(cat $box_src_tmp_dir/VERSION) <=="
|
||||||
|
|
||||||
echo "==> installer: updating docker"
|
echo "==> installer: updating docker"
|
||||||
|
|
||||||
if [[ $(docker version --format {{.Client.Version}}) != "18.09.2" ]]; then
|
if [[ $(docker version --format {{.Client.Version}}) != "18.09.2" ]]; then
|
||||||
@@ -56,10 +57,10 @@ if [[ $(docker version --format {{.Client.Version}}) != "18.09.2" ]]; then
|
|||||||
rm /tmp/containerd.deb /tmp/docker-ce-cli.deb /tmp/docker.deb
|
rm /tmp/containerd.deb /tmp/docker-ce-cli.deb /tmp/docker.deb
|
||||||
fi
|
fi
|
||||||
|
|
||||||
readonly nginx_version=$(nginx -v)
|
readonly nginx_version=$(nginx -v 2>&1)
|
||||||
if [[ "${nginx_version}" != *"1.14."* && "${ubuntu_version}" == "16.04" ]]; then
|
if [[ "${nginx_version}" != *"1.18."* ]]; then
|
||||||
echo "==> installer: installing nginx for xenial for TLSv3 support"
|
echo "==> installer: installing nginx 1.18"
|
||||||
curl -sL http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/nginx_1.14.0-1~xenial_amd64.deb -o /tmp/nginx.deb
|
curl -sL http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/nginx_1.18.0-1~${ubuntu_codename}_amd64.deb -o /tmp/nginx.deb
|
||||||
# apt install with install deps (as opposed to dpkg -i)
|
# apt install with install deps (as opposed to dpkg -i)
|
||||||
apt install -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" --force-yes /tmp/nginx.deb
|
apt install -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" --force-yes /tmp/nginx.deb
|
||||||
rm /tmp/nginx.deb
|
rm /tmp/nginx.deb
|
||||||
@@ -118,22 +119,22 @@ while [[ ! -f "${CLOUDRON_SYSLOG}" || "$(${CLOUDRON_SYSLOG} --version)" != ${CLO
|
|||||||
sleep 5
|
sleep 5
|
||||||
done
|
done
|
||||||
|
|
||||||
if ! id "${USER}" 2>/dev/null; then
|
if ! id "${user}" 2>/dev/null; then
|
||||||
useradd "${USER}" -m
|
useradd "${user}" -m
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "${is_update}" == "yes" ]]; then
|
if [[ "${is_update}" == "yes" ]]; then
|
||||||
echo "==> installer: stop cloudron.target service for update"
|
echo "==> installer: stop box service for update"
|
||||||
${BOX_SRC_DIR}/setup/stop.sh
|
${box_src_dir}/setup/stop.sh
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ensure we are not inside the source directory, which we will remove now
|
# ensure we are not inside the source directory, which we will remove now
|
||||||
cd /root
|
cd /root
|
||||||
|
|
||||||
echo "==> installer: switching the box code"
|
echo "==> installer: switching the box code"
|
||||||
rm -rf "${BOX_SRC_DIR}"
|
rm -rf "${box_src_dir}"
|
||||||
mv "${box_src_tmp_dir}" "${BOX_SRC_DIR}"
|
mv "${box_src_tmp_dir}" "${box_src_dir}"
|
||||||
chown -R "${USER}:${USER}" "${BOX_SRC_DIR}"
|
chown -R "${user}:${user}" "${box_src_dir}"
|
||||||
|
|
||||||
echo "==> installer: calling box setup script"
|
echo "==> installer: calling box setup script"
|
||||||
"${BOX_SRC_DIR}/setup/start.sh"
|
"${box_src_dir}/setup/start.sh"
|
||||||
|
|||||||
+26
-4
@@ -20,6 +20,11 @@ readonly ubuntu_version=$(lsb_release -rs)
|
|||||||
|
|
||||||
cp -f "${script_dir}/../scripts/cloudron-support" /usr/bin/cloudron-support
|
cp -f "${script_dir}/../scripts/cloudron-support" /usr/bin/cloudron-support
|
||||||
|
|
||||||
|
# this needs to match the cloudron/base:2.0.0 gid
|
||||||
|
if ! getent group media; then
|
||||||
|
addgroup --gid 500 --system media
|
||||||
|
fi
|
||||||
|
|
||||||
echo "==> Configuring docker"
|
echo "==> Configuring docker"
|
||||||
cp "${script_dir}/start/docker-cloudron-app.apparmor" /etc/apparmor.d/docker-cloudron-app
|
cp "${script_dir}/start/docker-cloudron-app.apparmor" /etc/apparmor.d/docker-cloudron-app
|
||||||
systemctl enable apparmor
|
systemctl enable apparmor
|
||||||
@@ -80,6 +85,9 @@ systemctl daemon-reload
|
|||||||
systemctl restart systemd-journald
|
systemctl restart systemd-journald
|
||||||
setfacl -n -m u:${USER}:r /var/log/journal/*/system.journal
|
setfacl -n -m u:${USER}:r /var/log/journal/*/system.journal
|
||||||
|
|
||||||
|
# Give user access to nginx logs (uses adm group)
|
||||||
|
usermod -a -G adm ${USER}
|
||||||
|
|
||||||
echo "==> Setting up unbound"
|
echo "==> Setting up unbound"
|
||||||
# DO uses Google nameservers by default. This causes RBL queries to fail (host 2.0.0.127.zen.spamhaus.org)
|
# DO uses Google nameservers by default. This causes RBL queries to fail (host 2.0.0.127.zen.spamhaus.org)
|
||||||
# We do not use dnsmasq because it is not a recursive resolver and defaults to the value in the interfaces file (which is Google DNS!)
|
# We do not use dnsmasq because it is not a recursive resolver and defaults to the value in the interfaces file (which is Google DNS!)
|
||||||
@@ -92,11 +100,13 @@ unbound-anchor -a /var/lib/unbound/root.key
|
|||||||
|
|
||||||
echo "==> Adding systemd services"
|
echo "==> Adding systemd services"
|
||||||
cp -r "${script_dir}/start/systemd/." /etc/systemd/system/
|
cp -r "${script_dir}/start/systemd/." /etc/systemd/system/
|
||||||
|
systemctl disable cloudron.target || true
|
||||||
|
rm -f /etc/systemd/system/cloudron.target
|
||||||
[[ "${ubuntu_version}" == "16.04" ]] && sed -e 's/MemoryMax/MemoryLimit/g' -i /etc/systemd/system/box.service
|
[[ "${ubuntu_version}" == "16.04" ]] && sed -e 's/MemoryMax/MemoryLimit/g' -i /etc/systemd/system/box.service
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
systemctl enable unbound
|
systemctl enable unbound
|
||||||
systemctl enable cloudron-syslog
|
systemctl enable cloudron-syslog
|
||||||
systemctl enable cloudron.target
|
systemctl enable box
|
||||||
systemctl enable cloudron-firewall
|
systemctl enable cloudron-firewall
|
||||||
|
|
||||||
# update firewall rules
|
# update firewall rules
|
||||||
@@ -145,8 +155,15 @@ cp "${script_dir}/start/nginx/mime.types" "${PLATFORM_DATA_DIR}/nginx/mime.types
|
|||||||
if ! grep -q "^Restart=" /etc/systemd/system/multi-user.target.wants/nginx.service; then
|
if ! grep -q "^Restart=" /etc/systemd/system/multi-user.target.wants/nginx.service; then
|
||||||
# default nginx service file does not restart on crash
|
# default nginx service file does not restart on crash
|
||||||
echo -e "\n[Service]\nRestart=always\n" >> /etc/systemd/system/multi-user.target.wants/nginx.service
|
echo -e "\n[Service]\nRestart=always\n" >> /etc/systemd/system/multi-user.target.wants/nginx.service
|
||||||
systemctl daemon-reload
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# worker_rlimit_nofile in nginx config can be max this number
|
||||||
|
mkdir -p /etc/systemd/system/nginx.service.d
|
||||||
|
if ! grep -q "^LimitNOFILE=" /etc/systemd/system/nginx.service.d/cloudron.conf; then
|
||||||
|
echo -e "[Service]\nLimitNOFILE=16384\n" > /etc/systemd/system/nginx.service.d/cloudron.conf
|
||||||
|
fi
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
systemctl start nginx
|
systemctl start nginx
|
||||||
|
|
||||||
# restart mysql to make sure it has latest config
|
# restart mysql to make sure it has latest config
|
||||||
@@ -171,9 +188,11 @@ readonly mysql_root_password="password"
|
|||||||
mysqladmin -u root -ppassword password password # reset default root password
|
mysqladmin -u root -ppassword password password # reset default root password
|
||||||
mysql -u root -p${mysql_root_password} -e 'CREATE DATABASE IF NOT EXISTS box'
|
mysql -u root -p${mysql_root_password} -e 'CREATE DATABASE IF NOT EXISTS box'
|
||||||
|
|
||||||
|
# set HOME explicity, because it's not set when the installer calls it. this is done because
|
||||||
|
# paths.js uses this env var and some of the migrate code requires box code
|
||||||
echo "==> Migrating data"
|
echo "==> Migrating data"
|
||||||
cd "${BOX_SRC_DIR}"
|
cd "${BOX_SRC_DIR}"
|
||||||
if ! BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@127.0.0.1/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up; then
|
if ! HOME=${HOME_DIR} BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@127.0.0.1/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up; then
|
||||||
echo "DB migration failed"
|
echo "DB migration failed"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -192,6 +211,9 @@ fi
|
|||||||
echo "==> Cleaning up stale redis directories"
|
echo "==> Cleaning up stale redis directories"
|
||||||
find "${APPS_DATA_DIR}" -maxdepth 2 -type d -name redis -exec rm -rf {} +
|
find "${APPS_DATA_DIR}" -maxdepth 2 -type d -name redis -exec rm -rf {} +
|
||||||
|
|
||||||
|
echo "==> Cleaning up old logs"
|
||||||
|
rm -f /home/yellowtent/platformdata/logs/*/*.log.* || true
|
||||||
|
|
||||||
echo "==> Changing ownership"
|
echo "==> Changing ownership"
|
||||||
# be careful of what is chown'ed here. subdirs like mysql,redis etc are owned by the containers and will stop working if perms change
|
# be careful of what is chown'ed here. subdirs like mysql,redis etc are owned by the containers and will stop working if perms change
|
||||||
chown -R "${USER}" /etc/cloudron
|
chown -R "${USER}" /etc/cloudron
|
||||||
@@ -207,7 +229,7 @@ chown "${USER}:${USER}" "${BOX_DATA_DIR}/mail"
|
|||||||
chown "${USER}:${USER}" -R "${BOX_DATA_DIR}/mail/dkim" # this is owned by box currently since it generates the keys
|
chown "${USER}:${USER}" -R "${BOX_DATA_DIR}/mail/dkim" # this is owned by box currently since it generates the keys
|
||||||
|
|
||||||
echo "==> Starting Cloudron"
|
echo "==> Starting Cloudron"
|
||||||
systemctl start cloudron.target
|
systemctl start box
|
||||||
|
|
||||||
sleep 2 # give systemd sometime to start the processes
|
sleep 2 # give systemd sometime to start the processes
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import collectd,os,subprocess,sys,re,time
|
|||||||
# https://www.programcreek.com/python/example/106897/collectd.register_read
|
# https://www.programcreek.com/python/example/106897/collectd.register_read
|
||||||
|
|
||||||
PATHS = [] # { name, dir, exclude }
|
PATHS = [] # { name, dir, exclude }
|
||||||
|
# there is a pattern in carbon/storage-schemas.conf which stores values every 12h for a year
|
||||||
INTERVAL = 60 * 60 * 12 # twice a day. change values in docker-graphite if you change this
|
INTERVAL = 60 * 60 * 12 # twice a day. change values in docker-graphite if you change this
|
||||||
|
|
||||||
def du(pathinfo):
|
def du(pathinfo):
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
/home/yellowtent/platformdata/logs/redis-*/*.log
|
/home/yellowtent/platformdata/logs/redis-*/*.log
|
||||||
/home/yellowtent/platformdata/logs/crash/*.log
|
/home/yellowtent/platformdata/logs/crash/*.log
|
||||||
/home/yellowtent/platformdata/logs/collectd/*.log
|
/home/yellowtent/platformdata/logs/collectd/*.log
|
||||||
|
/home/yellowtent/platformdata/logs/turn/*.log
|
||||||
/home/yellowtent/platformdata/logs/updater/*.log {
|
/home/yellowtent/platformdata/logs/updater/*.log {
|
||||||
# only keep one rotated file, we currently do not send that over the api
|
# only keep one rotated file, we currently do not send that over the api
|
||||||
rotate 1
|
rotate 1
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
missingok
|
missingok
|
||||||
# we never compress so we can simply tail the files
|
# we never compress so we can simply tail the files
|
||||||
nocompress
|
nocompress
|
||||||
|
# this truncates the original log file and not the rotated one
|
||||||
copytruncate
|
copytruncate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
user www-data;
|
user www-data;
|
||||||
|
|
||||||
worker_processes 1;
|
# detect based on available CPU cores
|
||||||
|
worker_processes auto;
|
||||||
|
|
||||||
|
# this is 4096 by default. See /proc/<PID>/limits and /etc/security/limits.conf
|
||||||
|
# usually twice the worker_connections (one for uptsream, one for downstream)
|
||||||
|
# see also LimitNOFILE=16384 in systemd drop-in
|
||||||
|
worker_rlimit_nofile 8192;
|
||||||
|
|
||||||
pid /run/nginx.pid;
|
pid /run/nginx.pid;
|
||||||
|
|
||||||
events {
|
events {
|
||||||
worker_connections 1024;
|
# a single worker has these many simultaneous connections max
|
||||||
|
worker_connections 4096;
|
||||||
}
|
}
|
||||||
|
|
||||||
http {
|
http {
|
||||||
|
|||||||
@@ -50,3 +50,12 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restartdocker.s
|
|||||||
Defaults!/home/yellowtent/box/src/scripts/restartunbound.sh env_keep="HOME BOX_ENV"
|
Defaults!/home/yellowtent/box/src/scripts/restartunbound.sh env_keep="HOME BOX_ENV"
|
||||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restartunbound.sh
|
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restartunbound.sh
|
||||||
|
|
||||||
|
Defaults!/home/yellowtent/box/src/scripts/rmmailbox.sh env_keep="HOME BOX_ENV"
|
||||||
|
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmmailbox.sh
|
||||||
|
|
||||||
|
Defaults!/home/yellowtent/box/src/scripts/starttask.sh env_keep="HOME BOX_ENV"
|
||||||
|
yellowtent ALL=(root) NOPASSWD:SETENV: /home/yellowtent/box/src/scripts/starttask.sh
|
||||||
|
|
||||||
|
Defaults!/home/yellowtent/box/src/scripts/stoptask.sh env_keep="HOME BOX_ENV"
|
||||||
|
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/stoptask.sh
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=Cloudron Admin
|
Description=Cloudron Admin
|
||||||
OnFailure=crashnotifier@%n.service
|
OnFailure=crashnotifier@%n.service
|
||||||
StopWhenUnneeded=true
|
|
||||||
; journald crashes result in a EPIPE in node. Cannot ignore it as it results in loss of logs.
|
; journald crashes result in a EPIPE in node. Cannot ignore it as it results in loss of logs.
|
||||||
BindsTo=systemd-journald.service
|
BindsTo=systemd-journald.service
|
||||||
After=mysql.service nginx.service
|
After=mysql.service nginx.service
|
||||||
; As cloudron-resize-fs is a one-shot, the Wants= automatically ensures that the service *finishes*
|
; As cloudron-resize-fs is a one-shot, the Wants= automatically ensures that the service *finishes*
|
||||||
Wants=cloudron-resize-fs.service
|
Wants=cloudron-resize-fs.service
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=idle
|
Type=idle
|
||||||
WorkingDirectory=/home/yellowtent/box
|
WorkingDirectory=/home/yellowtent/box
|
||||||
Restart=always
|
Restart=always
|
||||||
; Systemd does not append logs when logging to files, we spawn a shell first and exec to replace it after setting up the pipes
|
ExecStart=/home/yellowtent/box/box.js
|
||||||
ExecStart=/bin/sh -c 'echo "Logging to /home/yellowtent/platformdata/logs/box.log"; exec /usr/bin/node --max_old_space_size=150 /home/yellowtent/box/box.js >> /home/yellowtent/platformdata/logs/box.log 2>&1'
|
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box:*,connect-lastmile,-box:ldap" "BOX_ENV=cloudron" "NODE_ENV=production"
|
||||||
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmile" "BOX_ENV=cloudron" "NODE_ENV=production"
|
|
||||||
; kill apptask processes as well
|
; kill apptask processes as well
|
||||||
KillMode=control-group
|
KillMode=control-group
|
||||||
; Do not kill this process on OOM. Children inherit this score. Do not set it to -1000 so that MemoryMax can keep working
|
; Do not kill this process on OOM. Children inherit this score. Do not set it to -1000 so that MemoryMax can keep working
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Cloudron Smartserver
|
|
||||||
Documentation=https://cloudron.io/documentation.html
|
|
||||||
StopWhenUnneeded=true
|
|
||||||
Requires=box.service
|
|
||||||
After=box.service
|
|
||||||
# AllowIsolate=yes
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
+1
-1
@@ -4,4 +4,4 @@ set -eu -o pipefail
|
|||||||
|
|
||||||
echo "Stopping cloudron"
|
echo "Stopping cloudron"
|
||||||
|
|
||||||
systemctl stop cloudron.target
|
systemctl stop box
|
||||||
|
|||||||
+358
-138
@@ -7,6 +7,9 @@ exports = module.exports = {
|
|||||||
getServiceLogs: getServiceLogs,
|
getServiceLogs: getServiceLogs,
|
||||||
restartService: restartService,
|
restartService: restartService,
|
||||||
|
|
||||||
|
startAppServices,
|
||||||
|
stopAppServices,
|
||||||
|
|
||||||
startServices: startServices,
|
startServices: startServices,
|
||||||
updateServiceConfig: updateServiceConfig,
|
updateServiceConfig: updateServiceConfig,
|
||||||
|
|
||||||
@@ -20,7 +23,7 @@ exports = module.exports = {
|
|||||||
getMountsSync: getMountsSync,
|
getMountsSync: getMountsSync,
|
||||||
getContainerNamesSync: getContainerNamesSync,
|
getContainerNamesSync: getContainerNamesSync,
|
||||||
|
|
||||||
getServiceDetails: getServiceDetails,
|
getContainerDetails: getContainerDetails,
|
||||||
|
|
||||||
SERVICE_STATUS_STARTING: 'starting', // container up, waiting for healthcheck
|
SERVICE_STATUS_STARTING: 'starting', // container up, waiting for healthcheck
|
||||||
SERVICE_STATUS_ACTIVE: 'active',
|
SERVICE_STATUS_ACTIVE: 'active',
|
||||||
@@ -62,7 +65,7 @@ const RMADDONDIR_CMD = path.join(__dirname, 'scripts/rmaddondir.sh');
|
|||||||
|
|
||||||
// setup can be called multiple times for the same app (configure crash restart) and existing data must not be lost
|
// setup can be called multiple times for the same app (configure crash restart) and existing data must not be lost
|
||||||
// teardown is destructive. app data stored with the addon is lost
|
// teardown is destructive. app data stored with the addon is lost
|
||||||
var KNOWN_ADDONS = {
|
var ADDONS = {
|
||||||
turn: {
|
turn: {
|
||||||
setup: setupTurn,
|
setup: setupTurn,
|
||||||
teardown: teardownTurn,
|
teardown: teardownTurn,
|
||||||
@@ -146,10 +149,18 @@ var KNOWN_ADDONS = {
|
|||||||
backup: NOOP,
|
backup: NOOP,
|
||||||
restore: NOOP,
|
restore: NOOP,
|
||||||
clear: NOOP,
|
clear: NOOP,
|
||||||
|
},
|
||||||
|
oauth: { // kept for backward compatibility. keep teardown for uninstall to work
|
||||||
|
setup: NOOP,
|
||||||
|
teardown: teardownOauth,
|
||||||
|
backup: NOOP,
|
||||||
|
restore: NOOP,
|
||||||
|
clear: NOOP,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const KNOWN_SERVICES = {
|
// services are actual containers that are running. addons are the concepts requested by app
|
||||||
|
const SERVICES = {
|
||||||
turn: {
|
turn: {
|
||||||
status: statusTurn,
|
status: statusTurn,
|
||||||
restart: restartContainer.bind(null, 'turn'),
|
restart: restartContainer.bind(null, 'turn'),
|
||||||
@@ -202,6 +213,16 @@ const KNOWN_SERVICES = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const APP_SERVICES = {
|
||||||
|
redis: {
|
||||||
|
status: (instance, done) => containerStatus(`redis-${instance}`, 'CLOUDRON_REDIS_TOKEN', done),
|
||||||
|
start: (instance, done) => docker.startContainer(`redis-${instance}`, done),
|
||||||
|
stop: (instance, done) => docker.stopContainer(`redis-${instance}`, done),
|
||||||
|
restart: (instance, done) => restartContainer(`redis-${instance}`, done),
|
||||||
|
defaultMemoryLimit: 150 * 1024 * 1024
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function debugApp(app /*, args */) {
|
function debugApp(app /*, args */) {
|
||||||
assert(typeof app === 'object');
|
assert(typeof app === 'object');
|
||||||
|
|
||||||
@@ -249,25 +270,22 @@ function rebuildService(serviceName, callback) {
|
|||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
|
|
||||||
function restartContainer(serviceName, callback) {
|
function restartContainer(name, callback) {
|
||||||
assert.strictEqual(typeof serviceName, 'string');
|
assert.strictEqual(typeof name, 'string');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
docker.stopContainer(serviceName, function (error) {
|
docker.restartContainer(name, function (error) {
|
||||||
|
if (error && error.reason === BoxError.NOT_FOUND) {
|
||||||
|
callback(null); // callback early since rebuilding takes long
|
||||||
|
return rebuildService(name, function (error) { if (error) debug(`restartContainer: Unable to rebuild service ${name}`, error); });
|
||||||
|
}
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
docker.startContainer(serviceName, function (error) {
|
callback(error);
|
||||||
if (error && error.reason === BoxError.NOT_FOUND) {
|
|
||||||
callback(null); // callback early since rebuilding takes long
|
|
||||||
return rebuildService(serviceName, function (error) { if (error) console.error(`Unable to rebuild service ${serviceName}`, error); });
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(error);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getServiceDetails(containerName, tokenEnvName, callback) {
|
function getContainerDetails(containerName, tokenEnvName, callback) {
|
||||||
assert.strictEqual(typeof containerName, 'string');
|
assert.strictEqual(typeof containerName, 'string');
|
||||||
assert.strictEqual(typeof tokenEnvName, 'string');
|
assert.strictEqual(typeof tokenEnvName, 'string');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
@@ -290,20 +308,20 @@ function getServiceDetails(containerName, tokenEnvName, callback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function containerStatus(addonName, addonTokenName, callback) {
|
function containerStatus(containerName, tokenEnvName, callback) {
|
||||||
assert.strictEqual(typeof addonName, 'string');
|
assert.strictEqual(typeof containerName, 'string');
|
||||||
assert.strictEqual(typeof addonTokenName, 'string');
|
assert.strictEqual(typeof tokenEnvName, 'string');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
getServiceDetails(addonName, addonTokenName, function (error, addonDetails) {
|
getContainerDetails(containerName, tokenEnvName, function (error, addonDetails) {
|
||||||
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, { status: exports.SERVICE_STATUS_STOPPED });
|
if (error && (error.reason === BoxError.NOT_FOUND || error.reason === BoxError.INACTIVE)) return callback(null, { status: exports.SERVICE_STATUS_STOPPED });
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
request.get(`https://${addonDetails.ip}:3000/healthcheck?access_token=${addonDetails.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
|
request.get(`https://${addonDetails.ip}:3000/healthcheck?access_token=${addonDetails.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
|
||||||
if (error) return callback(null, { status: exports.SERVICE_STATUS_STARTING, error: `Error waiting for ${addonName}: ${error.message}` });
|
if (error) return callback(null, { status: exports.SERVICE_STATUS_STARTING, error: `Error waiting for ${containerName}: ${error.message}` });
|
||||||
if (response.statusCode !== 200 || !response.body.status) return callback(null, { status: exports.SERVICE_STATUS_STARTING, error: `Error waiting for ${addonName}. Status code: ${response.statusCode} message: ${response.body.message}` });
|
if (response.statusCode !== 200 || !response.body.status) return callback(null, { status: exports.SERVICE_STATUS_STARTING, error: `Error waiting for ${containerName}. Status code: ${response.statusCode} message: ${response.body.message}` });
|
||||||
|
|
||||||
docker.memoryUsage(addonName, function (error, result) {
|
docker.memoryUsage(containerName, function (error, result) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
var tmp = {
|
var tmp = {
|
||||||
@@ -321,19 +339,59 @@ function containerStatus(addonName, addonTokenName, callback) {
|
|||||||
function getServices(callback) {
|
function getServices(callback) {
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
let services = Object.keys(KNOWN_SERVICES);
|
let services = Object.keys(SERVICES);
|
||||||
|
|
||||||
callback(null, services);
|
appdb.getAll(function (error, apps) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
for (let app of apps) {
|
||||||
|
if (app.manifest.addons && app.manifest.addons['redis']) services.push(`redis:${app.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(null, services);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getService(serviceName, callback) {
|
function getServicesConfig(id, callback) {
|
||||||
assert.strictEqual(typeof serviceName, 'string');
|
assert.strictEqual(typeof id, 'string');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
if (!KNOWN_SERVICES[serviceName]) return callback(new BoxError(BoxError.NOT_FOUND));
|
const [name, instance ] = id.split(':');
|
||||||
|
if (!instance) {
|
||||||
|
settings.getPlatformConfig(function (error, platformConfig) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
callback(null, SERVICES[name], platformConfig);
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
appdb.get(instance, function (error, app) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
callback(null, APP_SERVICES[name], app.servicesConfig);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getService(id, callback) {
|
||||||
|
assert.strictEqual(typeof id, 'string');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
const [name, instance ] = id.split(':');
|
||||||
|
let containerStatusFunc;
|
||||||
|
|
||||||
|
if (instance) {
|
||||||
|
if (!APP_SERVICES[name]) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||||
|
containerStatusFunc = APP_SERVICES[name].status.bind(null, instance);
|
||||||
|
} else if (SERVICES[name]) {
|
||||||
|
containerStatusFunc = SERVICES[name].status;
|
||||||
|
} else {
|
||||||
|
return callback(new BoxError(BoxError.NOT_FOUND));
|
||||||
|
}
|
||||||
|
|
||||||
var tmp = {
|
var tmp = {
|
||||||
name: serviceName,
|
name: name,
|
||||||
status: null,
|
status: null,
|
||||||
memoryUsed: 0,
|
memoryUsed: 0,
|
||||||
memoryPercent: 0,
|
memoryPercent: 0,
|
||||||
@@ -345,60 +403,76 @@ function getService(serviceName, callback) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
settings.getPlatformConfig(function (error, platformConfig) {
|
containerStatusFunc(function (error, result) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
if (platformConfig[serviceName] && platformConfig[serviceName].memory && platformConfig[serviceName].memorySwap) {
|
tmp.status = result.status;
|
||||||
tmp.config.memory = platformConfig[serviceName].memory;
|
tmp.memoryUsed = result.memoryUsed;
|
||||||
tmp.config.memorySwap = platformConfig[serviceName].memorySwap;
|
tmp.memoryPercent = result.memoryPercent;
|
||||||
} else if (KNOWN_SERVICES[serviceName].defaultMemoryLimit) {
|
tmp.error = result.error || null;
|
||||||
tmp.config.memory = KNOWN_SERVICES[serviceName].defaultMemoryLimit;
|
|
||||||
tmp.config.memorySwap = tmp.config.memory * 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
KNOWN_SERVICES[serviceName].status(function (error, result) {
|
getServicesConfig(id, function (error, service, servicesConfig) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
tmp.status = result.status;
|
const serviceConfig = servicesConfig[name];
|
||||||
tmp.memoryUsed = result.memoryUsed;
|
|
||||||
tmp.memoryPercent = result.memoryPercent;
|
if (serviceConfig && serviceConfig.memory && serviceConfig.memorySwap) {
|
||||||
tmp.error = result.error || null;
|
tmp.config.memory = serviceConfig.memory;
|
||||||
|
tmp.config.memorySwap = serviceConfig.memorySwap;
|
||||||
|
} else if (service.defaultMemoryLimit) {
|
||||||
|
tmp.config.memory = service.defaultMemoryLimit;
|
||||||
|
tmp.config.memorySwap = tmp.config.memory * 2;
|
||||||
|
}
|
||||||
|
|
||||||
callback(null, tmp);
|
callback(null, tmp);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function configureService(serviceName, data, callback) {
|
function configureService(id, data, callback) {
|
||||||
assert.strictEqual(typeof serviceName, 'string');
|
assert.strictEqual(typeof id, 'string');
|
||||||
assert.strictEqual(typeof data, 'object');
|
assert.strictEqual(typeof data, 'object');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
if (!KNOWN_SERVICES[serviceName]) return callback(new BoxError(BoxError.NOT_FOUND));
|
const [name, instance ] = id.split(':');
|
||||||
|
|
||||||
settings.getPlatformConfig(function (error, platformConfig) {
|
if (instance) {
|
||||||
|
if (!APP_SERVICES[name]) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||||
|
} else if (!SERVICES[name]) {
|
||||||
|
return callback(new BoxError(BoxError.NOT_FOUND));
|
||||||
|
}
|
||||||
|
|
||||||
|
getServicesConfig(id, function (error, service, servicesConfig) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
if (!platformConfig[serviceName]) platformConfig[serviceName] = {};
|
if (!servicesConfig[name]) servicesConfig[name] = {};
|
||||||
|
|
||||||
// if not specified we clear the entry and use defaults
|
// if not specified we clear the entry and use defaults
|
||||||
if (!data.memory || !data.memorySwap) {
|
if (!data.memory || !data.memorySwap) {
|
||||||
delete platformConfig[serviceName];
|
delete servicesConfig[name];
|
||||||
} else {
|
} else {
|
||||||
platformConfig[serviceName].memory = data.memory;
|
servicesConfig[name].memory = data.memory;
|
||||||
platformConfig[serviceName].memorySwap = data.memorySwap;
|
servicesConfig[name].memorySwap = data.memorySwap;
|
||||||
}
|
}
|
||||||
|
|
||||||
settings.setPlatformConfig(platformConfig, function (error) {
|
if (instance) {
|
||||||
if (error) return callback(error);
|
appdb.update(instance, { servicesConfig }, function (error) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
callback(null);
|
updateAppServiceConfig(name, instance, servicesConfig, callback);
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
settings.setPlatformConfig(servicesConfig, function (error) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
callback(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getServiceLogs(serviceName, options, callback) {
|
function getServiceLogs(id, options, callback) {
|
||||||
assert.strictEqual(typeof serviceName, 'string');
|
assert.strictEqual(typeof id, 'string');
|
||||||
assert(options && typeof options === 'object');
|
assert(options && typeof options === 'object');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
@@ -406,9 +480,15 @@ function getServiceLogs(serviceName, options, callback) {
|
|||||||
assert.strictEqual(typeof options.format, 'string');
|
assert.strictEqual(typeof options.format, 'string');
|
||||||
assert.strictEqual(typeof options.follow, 'boolean');
|
assert.strictEqual(typeof options.follow, 'boolean');
|
||||||
|
|
||||||
if (!KNOWN_SERVICES[serviceName]) return callback(new BoxError(BoxError.NOT_FOUND));
|
const [name, instance ] = id.split(':');
|
||||||
|
|
||||||
debug(`Getting logs for ${serviceName}`);
|
if (instance) {
|
||||||
|
if (!APP_SERVICES[name]) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||||
|
} else if (!SERVICES[name]) {
|
||||||
|
return callback(new BoxError(BoxError.NOT_FOUND));
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(`Getting logs for ${name}`);
|
||||||
|
|
||||||
var lines = options.lines,
|
var lines = options.lines,
|
||||||
format = options.format || 'json',
|
format = options.format || 'json',
|
||||||
@@ -417,21 +497,29 @@ function getServiceLogs(serviceName, options, callback) {
|
|||||||
let cmd, args = [];
|
let cmd, args = [];
|
||||||
|
|
||||||
// docker and unbound use journald
|
// docker and unbound use journald
|
||||||
if (serviceName === 'docker' || serviceName === 'unbound') {
|
if (name === 'docker' || name === 'unbound') {
|
||||||
cmd = 'journalctl';
|
cmd = 'journalctl';
|
||||||
|
|
||||||
args.push('--lines=' + (lines === -1 ? 'all' : lines));
|
args.push('--lines=' + (lines === -1 ? 'all' : lines));
|
||||||
args.push(`--unit=${serviceName}`);
|
args.push(`--unit=${name}`);
|
||||||
args.push('--no-pager');
|
args.push('--no-pager');
|
||||||
args.push('--output=short-iso');
|
args.push('--output=short-iso');
|
||||||
|
|
||||||
if (follow) args.push('--follow');
|
if (follow) args.push('--follow');
|
||||||
|
} else if (name === 'nginx') {
|
||||||
|
cmd = '/usr/bin/tail';
|
||||||
|
|
||||||
|
args.push('--lines=' + (lines === -1 ? '+1' : lines));
|
||||||
|
if (follow) args.push('--follow', '--retry', '--quiet'); // same as -F. to make it work if file doesn't exist, --quiet to not output file headers, which are no logs
|
||||||
|
args.push('/var/log/nginx/access.log');
|
||||||
|
args.push('/var/log/nginx/error.log');
|
||||||
} else {
|
} else {
|
||||||
cmd = '/usr/bin/tail';
|
cmd = '/usr/bin/tail';
|
||||||
|
|
||||||
args.push('--lines=' + (lines === -1 ? '+1' : lines));
|
args.push('--lines=' + (lines === -1 ? '+1' : lines));
|
||||||
if (follow) args.push('--follow', '--retry', '--quiet'); // same as -F. to make it work if file doesn't exist, --quiet to not output file headers, which are no logs
|
if (follow) args.push('--follow', '--retry', '--quiet'); // same as -F. to make it work if file doesn't exist, --quiet to not output file headers, which are no logs
|
||||||
args.push(path.join(paths.LOG_DIR, serviceName, 'app.log'));
|
const containerName = APP_SERVICES[name] ? `${name}-${instance}` : name;
|
||||||
|
args.push(path.join(paths.LOG_DIR, containerName, 'app.log'));
|
||||||
}
|
}
|
||||||
|
|
||||||
var cp = spawn(cmd, args);
|
var cp = spawn(cmd, args);
|
||||||
@@ -450,7 +538,7 @@ function getServiceLogs(serviceName, options, callback) {
|
|||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
realtimeTimestamp: timestamp * 1000,
|
realtimeTimestamp: timestamp * 1000,
|
||||||
message: message,
|
message: message,
|
||||||
source: serviceName
|
source: name
|
||||||
}) + '\n';
|
}) + '\n';
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -461,23 +549,67 @@ function getServiceLogs(serviceName, options, callback) {
|
|||||||
callback(null, transformStream);
|
callback(null, transformStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
function restartService(serviceName, callback) {
|
function restartService(id, callback) {
|
||||||
assert.strictEqual(typeof serviceName, 'string');
|
assert.strictEqual(typeof id, 'string');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
if (!KNOWN_SERVICES[serviceName]) return callback(new BoxError(BoxError.NOT_FOUND));
|
const [name, instance ] = id.split(':');
|
||||||
|
|
||||||
KNOWN_SERVICES[serviceName].restart(callback);
|
if (instance) {
|
||||||
|
if (!APP_SERVICES[name]) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||||
|
|
||||||
|
APP_SERVICES[name].restart(instance, callback);
|
||||||
|
} else if (SERVICES[name]) {
|
||||||
|
SERVICES[name].restart(callback);
|
||||||
|
} else {
|
||||||
|
return callback(new BoxError(BoxError.NOT_FOUND));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function waitForService(containerName, tokenEnvName, callback) {
|
// in the future, we can refcount and lazy start global services
|
||||||
|
function startAppServices(app, callback) {
|
||||||
|
assert.strictEqual(typeof app, 'object');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
const instance = app.id;
|
||||||
|
async.eachSeries(Object.keys(app.manifest.addons || {}), function (addon, iteratorDone) {
|
||||||
|
if (!(addon in APP_SERVICES)) return iteratorDone();
|
||||||
|
|
||||||
|
APP_SERVICES[addon].start(instance, function (error) { // assume addons name is service name
|
||||||
|
// error ignored because we don't want "start app" to error. use can fix it from Services
|
||||||
|
if (error) debug(`startAppServices: ${addon}:${instance}`, error);
|
||||||
|
|
||||||
|
iteratorDone();
|
||||||
|
});
|
||||||
|
}, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
// in the future, we can refcount and stop global services as well
|
||||||
|
function stopAppServices(app, callback) {
|
||||||
|
assert.strictEqual(typeof app, 'object');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
const instance = app.id;
|
||||||
|
async.eachSeries(Object.keys(app.manifest.addons || {}), function (addon, iteratorDone) {
|
||||||
|
if (!(addon in APP_SERVICES)) return iteratorDone();
|
||||||
|
|
||||||
|
APP_SERVICES[addon].stop(instance, function (error) { // assume addons name is service name
|
||||||
|
// error ignored because we don't want "start app" to error. use can fix it from Services
|
||||||
|
if (error) debug(`stopAppServices: ${addon}:${instance}`, error);
|
||||||
|
|
||||||
|
iteratorDone();
|
||||||
|
});
|
||||||
|
}, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
function waitForContainer(containerName, tokenEnvName, callback) {
|
||||||
assert.strictEqual(typeof containerName, 'string');
|
assert.strictEqual(typeof containerName, 'string');
|
||||||
assert.strictEqual(typeof tokenEnvName, 'string');
|
assert.strictEqual(typeof tokenEnvName, 'string');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
debug(`Waiting for ${containerName}`);
|
debug(`Waiting for ${containerName}`);
|
||||||
|
|
||||||
getServiceDetails(containerName, tokenEnvName, function (error, result) {
|
getContainerDetails(containerName, tokenEnvName, function (error, result) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
async.retry({ times: 10, interval: 15000 }, function (retryCallback) {
|
async.retry({ times: 10, interval: 15000 }, function (retryCallback) {
|
||||||
@@ -501,11 +633,11 @@ function setupAddons(app, addons, callback) {
|
|||||||
debugApp(app, 'setupAddons: Setting up %j', Object.keys(addons));
|
debugApp(app, 'setupAddons: Setting up %j', Object.keys(addons));
|
||||||
|
|
||||||
async.eachSeries(Object.keys(addons), function iterator(addon, iteratorCallback) {
|
async.eachSeries(Object.keys(addons), function iterator(addon, iteratorCallback) {
|
||||||
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
|
if (!(addon in ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
|
||||||
|
|
||||||
debugApp(app, 'Setting up addon %s with options %j', addon, addons[addon]);
|
debugApp(app, 'Setting up addon %s with options %j', addon, addons[addon]);
|
||||||
|
|
||||||
KNOWN_ADDONS[addon].setup(app, addons[addon], iteratorCallback);
|
ADDONS[addon].setup(app, addons[addon], iteratorCallback);
|
||||||
}, callback);
|
}, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -519,11 +651,11 @@ function teardownAddons(app, addons, callback) {
|
|||||||
debugApp(app, 'teardownAddons: Tearing down %j', Object.keys(addons));
|
debugApp(app, 'teardownAddons: Tearing down %j', Object.keys(addons));
|
||||||
|
|
||||||
async.eachSeries(Object.keys(addons), function iterator(addon, iteratorCallback) {
|
async.eachSeries(Object.keys(addons), function iterator(addon, iteratorCallback) {
|
||||||
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
|
if (!(addon in ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
|
||||||
|
|
||||||
debugApp(app, 'Tearing down addon %s with options %j', addon, addons[addon]);
|
debugApp(app, 'Tearing down addon %s with options %j', addon, addons[addon]);
|
||||||
|
|
||||||
KNOWN_ADDONS[addon].teardown(app, addons[addon], iteratorCallback);
|
ADDONS[addon].teardown(app, addons[addon], iteratorCallback);
|
||||||
}, callback);
|
}, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -539,9 +671,9 @@ function backupAddons(app, addons, callback) {
|
|||||||
debugApp(app, 'backupAddons: Backing up %j', Object.keys(addons));
|
debugApp(app, 'backupAddons: Backing up %j', Object.keys(addons));
|
||||||
|
|
||||||
async.eachSeries(Object.keys(addons), function iterator (addon, iteratorCallback) {
|
async.eachSeries(Object.keys(addons), function iterator (addon, iteratorCallback) {
|
||||||
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
|
if (!(addon in ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
|
||||||
|
|
||||||
KNOWN_ADDONS[addon].backup(app, addons[addon], iteratorCallback);
|
ADDONS[addon].backup(app, addons[addon], iteratorCallback);
|
||||||
}, callback);
|
}, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -557,9 +689,9 @@ function clearAddons(app, addons, callback) {
|
|||||||
debugApp(app, 'clearAddons: clearing %j', Object.keys(addons));
|
debugApp(app, 'clearAddons: clearing %j', Object.keys(addons));
|
||||||
|
|
||||||
async.eachSeries(Object.keys(addons), function iterator (addon, iteratorCallback) {
|
async.eachSeries(Object.keys(addons), function iterator (addon, iteratorCallback) {
|
||||||
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
|
if (!(addon in ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
|
||||||
|
|
||||||
KNOWN_ADDONS[addon].clear(app, addons[addon], iteratorCallback);
|
ADDONS[addon].clear(app, addons[addon], iteratorCallback);
|
||||||
}, callback);
|
}, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -575,9 +707,9 @@ function restoreAddons(app, addons, callback) {
|
|||||||
debugApp(app, 'restoreAddons: restoring %j', Object.keys(addons));
|
debugApp(app, 'restoreAddons: restoring %j', Object.keys(addons));
|
||||||
|
|
||||||
async.eachSeries(Object.keys(addons), function iterator (addon, iteratorCallback) {
|
async.eachSeries(Object.keys(addons), function iterator (addon, iteratorCallback) {
|
||||||
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
|
if (!(addon in ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
|
||||||
|
|
||||||
KNOWN_ADDONS[addon].restore(app, addons[addon], iteratorCallback);
|
ADDONS[addon].restore(app, addons[addon], iteratorCallback);
|
||||||
}, callback);
|
}, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -586,12 +718,12 @@ function importAppDatabase(app, addon, callback) {
|
|||||||
assert.strictEqual(typeof addon, 'string');
|
assert.strictEqual(typeof addon, 'string');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
if (!(addon in KNOWN_ADDONS)) return callback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
|
if (!(addon in ADDONS)) return callback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`));
|
||||||
|
|
||||||
async.series([
|
async.series([
|
||||||
KNOWN_ADDONS[addon].setup.bind(null, app, app.manifest.addons[addon]),
|
ADDONS[addon].setup.bind(null, app, app.manifest.addons[addon]),
|
||||||
KNOWN_ADDONS[addon].clear.bind(null, app, app.manifest.addons[addon]), // clear in case we crashed in a restore
|
ADDONS[addon].clear.bind(null, app, app.manifest.addons[addon]), // clear in case we crashed in a restore
|
||||||
KNOWN_ADDONS[addon].restore.bind(null, app, app.manifest.addons[addon])
|
ADDONS[addon].restore.bind(null, app, app.manifest.addons[addon])
|
||||||
], callback);
|
], callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -601,10 +733,10 @@ function importDatabase(addon, callback) {
|
|||||||
|
|
||||||
debug(`importDatabase: Importing ${addon}`);
|
debug(`importDatabase: Importing ${addon}`);
|
||||||
|
|
||||||
appdb.getAll(function (error, apps) {
|
appdb.getAll(function (error, allApps) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
async.eachSeries(apps, function iterator (app, iteratorCallback) {
|
async.eachSeries(allApps, function iterator (app, iteratorCallback) {
|
||||||
if (!(addon in app.manifest.addons)) return iteratorCallback(); // app doesn't use the addon
|
if (!(addon in app.manifest.addons)) return iteratorCallback(); // app doesn't use the addon
|
||||||
|
|
||||||
debug(`importDatabase: Importing addon ${addon} of app ${app.id}`);
|
debug(`importDatabase: Importing addon ${addon} of app ${app.id}`);
|
||||||
@@ -617,7 +749,51 @@ function importDatabase(addon, callback) {
|
|||||||
// not clear, if repair workflow should be part of addon or per-app
|
// not clear, if repair workflow should be part of addon or per-app
|
||||||
appdb.update(app.id, { installationState: apps.ISTATE_ERROR, error: { message: error.message } }, iteratorCallback);
|
appdb.update(app.id, { installationState: apps.ISTATE_ERROR, error: { message: error.message } }, iteratorCallback);
|
||||||
});
|
});
|
||||||
}, callback);
|
}, function (error) {
|
||||||
|
safe.fs.unlinkSync(path.join(paths.ADDON_CONFIG_DIR, `exported-${addon}`)); // clean up for future migrations
|
||||||
|
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportDatabase(addon, callback) {
|
||||||
|
assert.strictEqual(typeof addon, 'string');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
debug(`exportDatabase: Exporting ${addon}`);
|
||||||
|
|
||||||
|
if (fs.existsSync(path.join(paths.ADDON_CONFIG_DIR, `exported-${addon}`))) {
|
||||||
|
debug(`exportDatabase: Already exported addon ${addon} in previous run`);
|
||||||
|
return callback(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
appdb.getAll(function (error, apps) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
async.eachSeries(apps, function iterator (app, iteratorCallback) {
|
||||||
|
if (!app.manifest.addons || !(addon in app.manifest.addons)) return iteratorCallback(); // app doesn't use the addon
|
||||||
|
|
||||||
|
debug(`exportDatabase: Exporting addon ${addon} of app ${app.id}`);
|
||||||
|
|
||||||
|
ADDONS[addon].backup(app, app.manifest.addons[addon], function (error) {
|
||||||
|
if (error) {
|
||||||
|
debug(`exportDatabase: Error exporting ${addon} of app ${app.id}.`, error);
|
||||||
|
return iteratorCallback(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
iteratorCallback();
|
||||||
|
});
|
||||||
|
}, function (error) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
async.series([
|
||||||
|
(done) => fs.writeFile(path.join(paths.ADDON_CONFIG_DIR, `exported-${addon}`), '', 'utf8', done),
|
||||||
|
// note: after this point, we are restart safe. it's ok if the box code crashes at this point
|
||||||
|
(done) => shell.exec(`exportDatabase - remove${addon}`, `docker rm -f ${addon}`, done), // what if db writes something when quitting ...
|
||||||
|
(done) => shell.sudo(`exportDatabase - removeAddonDir${addon}`, [ RMADDONDIR_CMD, addon ], {}, done) // ready to start afresh
|
||||||
|
], callback);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -633,15 +809,37 @@ function updateServiceConfig(platformConfig, callback) {
|
|||||||
memory = containerConfig.memory;
|
memory = containerConfig.memory;
|
||||||
memorySwap = containerConfig.memorySwap;
|
memorySwap = containerConfig.memorySwap;
|
||||||
} else {
|
} else {
|
||||||
memory = KNOWN_SERVICES[serviceName].defaultMemoryLimit;
|
memory = SERVICES[serviceName].defaultMemoryLimit;
|
||||||
memorySwap = memory * 2;
|
memorySwap = memory * 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
const args = `update --memory ${memory} --memory-swap ${memorySwap} ${serviceName}`.split(' ');
|
const args = `update --memory ${memory} --memory-swap ${memorySwap} ${serviceName}`.split(' ');
|
||||||
shell.spawn(`update${serviceName}`, '/usr/bin/docker', args, { }, iteratorCallback);
|
shell.spawn(`updateServiceConfig(${serviceName})`, '/usr/bin/docker', args, { }, iteratorCallback);
|
||||||
}, callback);
|
}, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateAppServiceConfig(name, instance, servicesConfig, callback) {
|
||||||
|
assert.strictEqual(typeof name, 'string');
|
||||||
|
assert.strictEqual(typeof instance, 'string');
|
||||||
|
assert.strictEqual(typeof servicesConfig, 'object');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
debug(`updateAppServiceConfig: ${name}-${instance} ${JSON.stringify(servicesConfig)}`);
|
||||||
|
|
||||||
|
const serviceConfig = servicesConfig[name];
|
||||||
|
let memory, memorySwap;
|
||||||
|
if (serviceConfig && serviceConfig.memory && serviceConfig.memorySwap) {
|
||||||
|
memory = serviceConfig.memory;
|
||||||
|
memorySwap = serviceConfig.memorySwap;
|
||||||
|
} else {
|
||||||
|
memory = APP_SERVICES[name].defaultMemoryLimit;
|
||||||
|
memorySwap = memory * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = `update --memory ${memory} --memory-swap ${memorySwap} ${name}-${instance}`.split(' ');
|
||||||
|
shell.spawn(`updateAppServiceConfig${name}`, '/usr/bin/docker', args, { }, callback);
|
||||||
|
}
|
||||||
|
|
||||||
function startServices(existingInfra, callback) {
|
function startServices(existingInfra, callback) {
|
||||||
assert.strictEqual(typeof existingInfra, 'object');
|
assert.strictEqual(typeof existingInfra, 'object');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
@@ -661,7 +859,7 @@ function startServices(existingInfra, callback) {
|
|||||||
} else {
|
} else {
|
||||||
assert.strictEqual(typeof existingInfra.images, 'object');
|
assert.strictEqual(typeof existingInfra.images, 'object');
|
||||||
|
|
||||||
if (!existingInfra.images.turn || infra.images.turn.tag !== existingInfra.images.turn.tag) startFuncs.push(startTurn.bind(null, existingInfra));
|
if (infra.images.turn.tag !== existingInfra.images.turn.tag) startFuncs.push(startTurn.bind(null, existingInfra));
|
||||||
if (infra.images.mysql.tag !== existingInfra.images.mysql.tag) startFuncs.push(startMysql.bind(null, existingInfra));
|
if (infra.images.mysql.tag !== existingInfra.images.mysql.tag) startFuncs.push(startMysql.bind(null, existingInfra));
|
||||||
if (infra.images.postgresql.tag !== existingInfra.images.postgresql.tag) startFuncs.push(startPostgresql.bind(null, existingInfra));
|
if (infra.images.postgresql.tag !== existingInfra.images.postgresql.tag) startFuncs.push(startPostgresql.bind(null, existingInfra));
|
||||||
if (infra.images.mongodb.tag !== existingInfra.images.mongodb.tag) startFuncs.push(startMongodb.bind(null, existingInfra));
|
if (infra.images.mongodb.tag !== existingInfra.images.mongodb.tag) startFuncs.push(startMongodb.bind(null, existingInfra));
|
||||||
@@ -744,8 +942,8 @@ function setupLocalStorage(app, options, callback) {
|
|||||||
|
|
||||||
// reomve any existing volume in case it's bound with an old dataDir
|
// reomve any existing volume in case it's bound with an old dataDir
|
||||||
async.series([
|
async.series([
|
||||||
docker.removeVolume.bind(null, app, `${app.id}-localstorage`),
|
docker.removeVolume.bind(null, `${app.id}-localstorage`),
|
||||||
docker.createVolume.bind(null, app, `${app.id}-localstorage`, volumeDataDir)
|
docker.createVolume.bind(null, `${app.id}-localstorage`, volumeDataDir, { fqdn: app.fqdn, appId: app.id })
|
||||||
], callback);
|
], callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -756,7 +954,7 @@ function clearLocalStorage(app, options, callback) {
|
|||||||
|
|
||||||
debugApp(app, 'clearLocalStorage');
|
debugApp(app, 'clearLocalStorage');
|
||||||
|
|
||||||
docker.clearVolume(app, `${app.id}-localstorage`, { removeDirectory: false }, callback);
|
docker.clearVolume(`${app.id}-localstorage`, { removeDirectory: false }, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
function teardownLocalStorage(app, options, callback) {
|
function teardownLocalStorage(app, options, callback) {
|
||||||
@@ -767,8 +965,8 @@ function teardownLocalStorage(app, options, callback) {
|
|||||||
debugApp(app, 'teardownLocalStorage');
|
debugApp(app, 'teardownLocalStorage');
|
||||||
|
|
||||||
async.series([
|
async.series([
|
||||||
docker.clearVolume.bind(null, app, `${app.id}-localstorage`, { removeDirectory: true }),
|
docker.clearVolume.bind(null, `${app.id}-localstorage`, { removeDirectory: true }),
|
||||||
docker.removeVolume.bind(null, app, `${app.id}-localstorage`)
|
docker.removeVolume.bind(null, `${app.id}-localstorage`)
|
||||||
], callback);
|
], callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -778,7 +976,7 @@ function setupTurn(app, options, callback) {
|
|||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
var turnSecret = safe.fs.readFileSync(paths.ADDON_TURN_SECRET_FILE, 'utf8');
|
var turnSecret = safe.fs.readFileSync(paths.ADDON_TURN_SECRET_FILE, 'utf8');
|
||||||
if (!turnSecret) console.error('No turn secret set. Will leave emtpy, but this is a problem!');
|
if (!turnSecret) debug('setupTurn: no turn secret set. Will leave emtpy, but this is a problem!');
|
||||||
|
|
||||||
const env = [
|
const env = [
|
||||||
{ name: 'CLOUDRON_STUN_SERVER', value: settings.adminFqdn() },
|
{ name: 'CLOUDRON_STUN_SERVER', value: settings.adminFqdn() },
|
||||||
@@ -977,7 +1175,7 @@ function startMysql(existingInfra, callback) {
|
|||||||
const upgrading = existingInfra.version !== 'none' && requiresUpgrade(existingInfra.images.mysql.tag, tag);
|
const upgrading = existingInfra.version !== 'none' && requiresUpgrade(existingInfra.images.mysql.tag, tag);
|
||||||
|
|
||||||
if (upgrading) debug('startMysql: mysql will be upgraded');
|
if (upgrading) debug('startMysql: mysql will be upgraded');
|
||||||
const upgradeFunc = upgrading ? shell.sudo.bind(null, 'startMysql', [ RMADDONDIR_CMD, 'mysql' ], {}) : (next) => next();
|
const upgradeFunc = upgrading ? exportDatabase.bind(null, 'mysql') : (next) => next();
|
||||||
|
|
||||||
upgradeFunc(function (error) {
|
upgradeFunc(function (error) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
@@ -1001,10 +1199,14 @@ function startMysql(existingInfra, callback) {
|
|||||||
--label isCloudronManaged=true \
|
--label isCloudronManaged=true \
|
||||||
--read-only -v /tmp -v /run "${tag}"`;
|
--read-only -v /tmp -v /run "${tag}"`;
|
||||||
|
|
||||||
shell.exec('startMysql', cmd, function (error) {
|
async.series([
|
||||||
|
shell.exec.bind(null, 'stopMysql', 'docker stop mysql || true'),
|
||||||
|
shell.exec.bind(null, 'removeMysql', 'docker rm -f mysql || true'),
|
||||||
|
shell.exec.bind(null, 'startMysql', cmd)
|
||||||
|
], function (error) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
waitForService('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error) {
|
waitForContainer('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
if (!upgrading) return callback(null);
|
if (!upgrading) return callback(null);
|
||||||
|
|
||||||
@@ -1033,7 +1235,7 @@ function setupMySql(app, options, callback) {
|
|||||||
password: error ? hat(4 * 48) : existingPassword // see box#362 for password length
|
password: error ? hat(4 * 48) : existingPassword // see box#362 for password length
|
||||||
};
|
};
|
||||||
|
|
||||||
getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
|
getContainerDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
request.post(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `?access_token=${result.token}`, { rejectUnauthorized: false, json: data }, function (error, response) {
|
request.post(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `?access_token=${result.token}`, { rejectUnauthorized: false, json: data }, function (error, response) {
|
||||||
@@ -1072,7 +1274,7 @@ function clearMySql(app, options, callback) {
|
|||||||
|
|
||||||
const database = mysqlDatabaseName(app.id);
|
const database = mysqlDatabaseName(app.id);
|
||||||
|
|
||||||
getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
|
getContainerDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
request.post(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}/clear?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
|
request.post(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}/clear?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
|
||||||
@@ -1092,7 +1294,7 @@ function teardownMySql(app, options, callback) {
|
|||||||
const database = mysqlDatabaseName(app.id);
|
const database = mysqlDatabaseName(app.id);
|
||||||
const username = database;
|
const username = database;
|
||||||
|
|
||||||
getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
|
getContainerDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
request.delete(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}?access_token=${result.token}&username=${username}`, { json: true, rejectUnauthorized: false }, function (error, response) {
|
request.delete(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}?access_token=${result.token}&username=${username}`, { json: true, rejectUnauthorized: false }, function (error, response) {
|
||||||
@@ -1140,7 +1342,7 @@ function backupMySql(app, options, callback) {
|
|||||||
|
|
||||||
debugApp(app, 'Backing up mysql');
|
debugApp(app, 'Backing up mysql');
|
||||||
|
|
||||||
getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
|
getContainerDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
const url = `https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}/backup?access_token=${result.token}`;
|
const url = `https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}/backup?access_token=${result.token}`;
|
||||||
@@ -1159,7 +1361,7 @@ function restoreMySql(app, options, callback) {
|
|||||||
|
|
||||||
callback = once(callback); // protect from multiple returns with streams
|
callback = once(callback); // protect from multiple returns with streams
|
||||||
|
|
||||||
getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
|
getContainerDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
var input = fs.createReadStream(dumpPath('mysql', app.id));
|
var input = fs.createReadStream(dumpPath('mysql', app.id));
|
||||||
@@ -1194,7 +1396,7 @@ function startPostgresql(existingInfra, callback) {
|
|||||||
const upgrading = existingInfra.version !== 'none' && requiresUpgrade(existingInfra.images.postgresql.tag, tag);
|
const upgrading = existingInfra.version !== 'none' && requiresUpgrade(existingInfra.images.postgresql.tag, tag);
|
||||||
|
|
||||||
if (upgrading) debug('startPostgresql: postgresql will be upgraded');
|
if (upgrading) debug('startPostgresql: postgresql will be upgraded');
|
||||||
const upgradeFunc = upgrading ? shell.sudo.bind(null, 'startPostgresql', [ RMADDONDIR_CMD, 'postgresql' ], {}) : (next) => next();
|
const upgradeFunc = upgrading ? exportDatabase.bind(null, 'postgresql') : (next) => next();
|
||||||
|
|
||||||
upgradeFunc(function (error) {
|
upgradeFunc(function (error) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
@@ -1217,10 +1419,14 @@ function startPostgresql(existingInfra, callback) {
|
|||||||
--label isCloudronManaged=true \
|
--label isCloudronManaged=true \
|
||||||
--read-only -v /tmp -v /run "${tag}"`;
|
--read-only -v /tmp -v /run "${tag}"`;
|
||||||
|
|
||||||
shell.exec('startPostgresql', cmd, function (error) {
|
async.series([
|
||||||
|
shell.exec.bind(null, 'stopPostgresql', 'docker stop postgresql || true'),
|
||||||
|
shell.exec.bind(null, 'removePostgresql', 'docker rm -f postgresql || true'),
|
||||||
|
shell.exec.bind(null, 'startPostgresql', cmd)
|
||||||
|
], function (error) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
waitForService('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error) {
|
waitForContainer('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
if (!upgrading) return callback(null);
|
if (!upgrading) return callback(null);
|
||||||
|
|
||||||
@@ -1248,7 +1454,7 @@ function setupPostgreSql(app, options, callback) {
|
|||||||
password: error ? hat(4 * 128) : existingPassword
|
password: error ? hat(4 * 128) : existingPassword
|
||||||
};
|
};
|
||||||
|
|
||||||
getServiceDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
|
getContainerDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
request.post(`https://${result.ip}:3000/databases?access_token=${result.token}`, { rejectUnauthorized: false, json: data }, function (error, response) {
|
request.post(`https://${result.ip}:3000/databases?access_token=${result.token}`, { rejectUnauthorized: false, json: data }, function (error, response) {
|
||||||
@@ -1282,7 +1488,7 @@ function clearPostgreSql(app, options, callback) {
|
|||||||
|
|
||||||
debugApp(app, 'Clearing postgresql');
|
debugApp(app, 'Clearing postgresql');
|
||||||
|
|
||||||
getServiceDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
|
getContainerDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
request.post(`https://${result.ip}:3000/databases/${database}/clear?access_token=${result.token}&username=${username}`, { json: true, rejectUnauthorized: false }, function (error, response) {
|
request.post(`https://${result.ip}:3000/databases/${database}/clear?access_token=${result.token}&username=${username}`, { json: true, rejectUnauthorized: false }, function (error, response) {
|
||||||
@@ -1301,7 +1507,7 @@ function teardownPostgreSql(app, options, callback) {
|
|||||||
|
|
||||||
const { database, username } = postgreSqlNames(app.id);
|
const { database, username } = postgreSqlNames(app.id);
|
||||||
|
|
||||||
getServiceDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
|
getContainerDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
request.delete(`https://${result.ip}:3000/databases/${database}?access_token=${result.token}&username=${username}`, { json: true, rejectUnauthorized: false }, function (error, response) {
|
request.delete(`https://${result.ip}:3000/databases/${database}?access_token=${result.token}&username=${username}`, { json: true, rejectUnauthorized: false }, function (error, response) {
|
||||||
@@ -1322,7 +1528,7 @@ function backupPostgreSql(app, options, callback) {
|
|||||||
|
|
||||||
const { database } = postgreSqlNames(app.id);
|
const { database } = postgreSqlNames(app.id);
|
||||||
|
|
||||||
getServiceDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
|
getContainerDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
const url = `https://${result.ip}:3000/databases/${database}/backup?access_token=${result.token}`;
|
const url = `https://${result.ip}:3000/databases/${database}/backup?access_token=${result.token}`;
|
||||||
@@ -1341,7 +1547,7 @@ function restorePostgreSql(app, options, callback) {
|
|||||||
|
|
||||||
callback = once(callback); // protect from multiple returns with streams
|
callback = once(callback); // protect from multiple returns with streams
|
||||||
|
|
||||||
getServiceDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
|
getContainerDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
var input = fs.createReadStream(dumpPath('postgresql', app.id));
|
var input = fs.createReadStream(dumpPath('postgresql', app.id));
|
||||||
@@ -1373,8 +1579,6 @@ function startTurn(existingInfra, callback) {
|
|||||||
const memoryLimit = 256;
|
const memoryLimit = 256;
|
||||||
const realm = settings.adminFqdn();
|
const realm = settings.adminFqdn();
|
||||||
|
|
||||||
if (existingInfra.version === infra.version && existingInfra.images.turn && infra.images.turn.tag === existingInfra.images.turn.tag) return callback();
|
|
||||||
|
|
||||||
// this exports 3478/tcp, 5349/tls and 50000-51000/udp
|
// this exports 3478/tcp, 5349/tls and 50000-51000/udp
|
||||||
const cmd = `docker run --restart=always -d --name="turn" \
|
const cmd = `docker run --restart=always -d --name="turn" \
|
||||||
--hostname turn \
|
--hostname turn \
|
||||||
@@ -1392,7 +1596,11 @@ function startTurn(existingInfra, callback) {
|
|||||||
--label isCloudronManaged=true \
|
--label isCloudronManaged=true \
|
||||||
--read-only -v /tmp -v /run "${tag}"`;
|
--read-only -v /tmp -v /run "${tag}"`;
|
||||||
|
|
||||||
shell.exec('startTurn', cmd, callback);
|
async.series([
|
||||||
|
shell.exec.bind(null, 'stopTurn', 'docker stop turn || true'),
|
||||||
|
shell.exec.bind(null, 'removeTurn', 'docker rm -f turn || true'),
|
||||||
|
shell.exec.bind(null, 'startTurn', cmd)
|
||||||
|
], callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
function startMongodb(existingInfra, callback) {
|
function startMongodb(existingInfra, callback) {
|
||||||
@@ -1408,7 +1616,7 @@ function startMongodb(existingInfra, callback) {
|
|||||||
const upgrading = existingInfra.version !== 'none' && requiresUpgrade(existingInfra.images.mongodb.tag, tag);
|
const upgrading = existingInfra.version !== 'none' && requiresUpgrade(existingInfra.images.mongodb.tag, tag);
|
||||||
|
|
||||||
if (upgrading) debug('startMongodb: mongodb will be upgraded');
|
if (upgrading) debug('startMongodb: mongodb will be upgraded');
|
||||||
const upgradeFunc = upgrading ? shell.sudo.bind(null, 'startMongodb', [ RMADDONDIR_CMD, 'mongodb' ], {}) : (next) => next();
|
const upgradeFunc = upgrading ? exportDatabase.bind(null, 'mongodb') : (next) => next();
|
||||||
|
|
||||||
upgradeFunc(function (error) {
|
upgradeFunc(function (error) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
@@ -1431,10 +1639,14 @@ function startMongodb(existingInfra, callback) {
|
|||||||
--label isCloudronManaged=true \
|
--label isCloudronManaged=true \
|
||||||
--read-only -v /tmp -v /run "${tag}"`;
|
--read-only -v /tmp -v /run "${tag}"`;
|
||||||
|
|
||||||
shell.exec('startMongodb', cmd, function (error) {
|
async.series([
|
||||||
|
shell.exec.bind(null, 'stopMongodb', 'docker stop mongodb || true'),
|
||||||
|
shell.exec.bind(null, 'removeMongodb', 'docker rm -f mongodb || true'),
|
||||||
|
shell.exec.bind(null, 'startMongodb', cmd)
|
||||||
|
], function (error) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
waitForService('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error) {
|
waitForContainer('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
if (!upgrading) return callback(null);
|
if (!upgrading) return callback(null);
|
||||||
|
|
||||||
@@ -1461,7 +1673,7 @@ function setupMongoDb(app, options, callback) {
|
|||||||
oplog: !!options.oplog
|
oplog: !!options.oplog
|
||||||
};
|
};
|
||||||
|
|
||||||
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
|
getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
request.post(`https://${result.ip}:3000/databases?access_token=${result.token}`, { rejectUnauthorized: false, json: data }, function (error, response) {
|
request.post(`https://${result.ip}:3000/databases?access_token=${result.token}`, { rejectUnauthorized: false, json: data }, function (error, response) {
|
||||||
@@ -1497,7 +1709,7 @@ function clearMongodb(app, options, callback) {
|
|||||||
|
|
||||||
debugApp(app, 'Clearing mongodb');
|
debugApp(app, 'Clearing mongodb');
|
||||||
|
|
||||||
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
|
getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
request.post(`https://${result.ip}:3000/databases/${app.id}/clear?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
|
request.post(`https://${result.ip}:3000/databases/${app.id}/clear?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
|
||||||
@@ -1516,7 +1728,7 @@ function teardownMongoDb(app, options, callback) {
|
|||||||
|
|
||||||
debugApp(app, 'Tearing down mongodb');
|
debugApp(app, 'Tearing down mongodb');
|
||||||
|
|
||||||
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
|
getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
request.delete(`https://${result.ip}:3000/databases/${app.id}?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
|
request.delete(`https://${result.ip}:3000/databases/${app.id}?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
|
||||||
@@ -1535,7 +1747,7 @@ function backupMongoDb(app, options, callback) {
|
|||||||
|
|
||||||
debugApp(app, 'Backing up mongodb');
|
debugApp(app, 'Backing up mongodb');
|
||||||
|
|
||||||
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
|
getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
const url = `https://${result.ip}:3000/databases/${app.id}/backup?access_token=${result.token}`;
|
const url = `https://${result.ip}:3000/databases/${app.id}/backup?access_token=${result.token}`;
|
||||||
@@ -1552,7 +1764,7 @@ function restoreMongoDb(app, options, callback) {
|
|||||||
|
|
||||||
debugApp(app, 'restoreMongoDb');
|
debugApp(app, 'restoreMongoDb');
|
||||||
|
|
||||||
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
|
getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
const readStream = fs.createReadStream(dumpPath('mongodb', app.id));
|
const readStream = fs.createReadStream(dumpPath('mongodb', app.id));
|
||||||
@@ -1576,19 +1788,25 @@ function startRedis(existingInfra, callback) {
|
|||||||
const tag = infra.images.redis.tag;
|
const tag = infra.images.redis.tag;
|
||||||
const upgrading = existingInfra.version !== 'none' && requiresUpgrade(existingInfra.images.redis.tag, tag);
|
const upgrading = existingInfra.version !== 'none' && requiresUpgrade(existingInfra.images.redis.tag, tag);
|
||||||
|
|
||||||
appdb.getAll(function (error, apps) {
|
apps.getAll(function (error, allApps) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
async.eachSeries(apps, function iterator (app, iteratorCallback) {
|
async.eachSeries(allApps, function iterator (app, iteratorCallback) {
|
||||||
if (!('redis' in app.manifest.addons)) return iteratorCallback(); // app doesn't use the addon
|
if (!('redis' in app.manifest.addons)) return iteratorCallback(); // app doesn't use the addon
|
||||||
|
|
||||||
setupRedis(app, app.manifest.addons.redis, iteratorCallback);
|
const redisName = 'redis-' + app.id;
|
||||||
|
|
||||||
|
async.series([
|
||||||
|
shell.exec.bind(null, 'stopRedis', `docker stop ${redisName} || true`), // redis will backup as part of signal handling
|
||||||
|
shell.exec.bind(null, 'removeRedis', `docker rm -f ${redisName} || true`),
|
||||||
|
setupRedis.bind(null, app, app.manifest.addons.redis) // starts the container
|
||||||
|
], iteratorCallback);
|
||||||
}, function (error) {
|
}, function (error) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
if (!upgrading) return callback();
|
if (!upgrading) return callback();
|
||||||
|
|
||||||
importDatabase('redis', callback); // setupRedis currently starts the app container
|
importDatabase('redis', callback);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1608,15 +1826,7 @@ function setupRedis(app, options, callback) {
|
|||||||
const redisServiceToken = hat(4 * 48);
|
const redisServiceToken = hat(4 * 48);
|
||||||
|
|
||||||
// Compute redis memory limit based on app's memory limit (this is arbitrary)
|
// Compute redis memory limit based on app's memory limit (this is arbitrary)
|
||||||
var memoryLimit = app.memoryLimit || app.manifest.memoryLimit || 0;
|
const memoryLimit = app.servicesConfig['redis'] ? app.servicesConfig['redis'].memory : APP_SERVICES['redis'].defaultMemoryLimit;
|
||||||
|
|
||||||
if (memoryLimit === -1) { // unrestricted (debug mode)
|
|
||||||
memoryLimit = 0;
|
|
||||||
} else if (memoryLimit === 0 || memoryLimit <= (2 * 1024 * 1024 * 1024)) { // less than 2G (ram+swap)
|
|
||||||
memoryLimit = 150 * 1024 * 1024; // 150m
|
|
||||||
} else {
|
|
||||||
memoryLimit = 600 * 1024 * 1024; // 600m
|
|
||||||
}
|
|
||||||
|
|
||||||
const tag = infra.images.redis.tag;
|
const tag = infra.images.redis.tag;
|
||||||
const label = app.fqdn;
|
const label = app.fqdn;
|
||||||
@@ -1660,7 +1870,7 @@ function setupRedis(app, options, callback) {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
appdb.setAddonConfig.bind(null, app.id, 'redis', env),
|
appdb.setAddonConfig.bind(null, app.id, 'redis', env),
|
||||||
waitForService.bind(null, 'redis-' + app.id, 'CLOUDRON_REDIS_TOKEN')
|
waitForContainer.bind(null, 'redis-' + app.id, 'CLOUDRON_REDIS_TOKEN')
|
||||||
], function (error) {
|
], function (error) {
|
||||||
if (error) debug('Error setting up redis: ', error);
|
if (error) debug('Error setting up redis: ', error);
|
||||||
callback(error);
|
callback(error);
|
||||||
@@ -1675,7 +1885,7 @@ function clearRedis(app, options, callback) {
|
|||||||
|
|
||||||
debugApp(app, 'Clearing redis');
|
debugApp(app, 'Clearing redis');
|
||||||
|
|
||||||
getServiceDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
|
getContainerDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
request.post(`https://${result.ip}:3000/clear?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
|
request.post(`https://${result.ip}:3000/clear?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
|
||||||
@@ -1714,7 +1924,7 @@ function backupRedis(app, options, callback) {
|
|||||||
|
|
||||||
debugApp(app, 'Backing up redis');
|
debugApp(app, 'Backing up redis');
|
||||||
|
|
||||||
getServiceDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
|
getContainerDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
const url = `https://${result.ip}:3000/backup?access_token=${result.token}`;
|
const url = `https://${result.ip}:3000/backup?access_token=${result.token}`;
|
||||||
@@ -1731,7 +1941,7 @@ function restoreRedis(app, options, callback) {
|
|||||||
|
|
||||||
callback = once(callback); // protect from multiple returns with streams
|
callback = once(callback); // protect from multiple returns with streams
|
||||||
|
|
||||||
getServiceDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
|
getContainerDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
let input;
|
let input;
|
||||||
@@ -1869,3 +2079,13 @@ function statusGraphite(callback) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function teardownOauth(app, options, callback) {
|
||||||
|
assert.strictEqual(typeof app, 'object');
|
||||||
|
assert.strictEqual(typeof options, 'object');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
debugApp(app, 'teardownOauth');
|
||||||
|
|
||||||
|
appdb.unsetAddonConfig(app.id, 'oauth', callback);
|
||||||
|
}
|
||||||
|
|||||||
+10
-2
@@ -41,7 +41,7 @@ var assert = require('assert'),
|
|||||||
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.errorJson', 'apps.runState',
|
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.errorJson', 'apps.runState',
|
||||||
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'subdomains.subdomain AS location', 'subdomains.domain',
|
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'subdomains.subdomain AS location', 'subdomains.domain',
|
||||||
'apps.accessRestrictionJson', 'apps.memoryLimit', 'apps.cpuShares',
|
'apps.accessRestrictionJson', 'apps.memoryLimit', 'apps.cpuShares',
|
||||||
'apps.label', 'apps.tagsJson', 'apps.taskId', 'apps.reverseProxyConfigJson',
|
'apps.label', 'apps.tagsJson', 'apps.taskId', 'apps.reverseProxyConfigJson', 'apps.servicesConfigJson', 'apps.bindsJson',
|
||||||
'apps.sso', 'apps.debugModeJson', 'apps.enableBackup',
|
'apps.sso', 'apps.debugModeJson', 'apps.enableBackup',
|
||||||
'apps.creationTime', 'apps.updateTime', 'apps.mailboxName', 'apps.mailboxDomain', 'apps.enableAutomaticUpdate',
|
'apps.creationTime', 'apps.updateTime', 'apps.mailboxName', 'apps.mailboxDomain', 'apps.enableAutomaticUpdate',
|
||||||
'apps.dataDir', 'apps.ts', 'apps.healthTime' ].join(',');
|
'apps.dataDir', 'apps.ts', 'apps.healthTime' ].join(',');
|
||||||
@@ -94,6 +94,14 @@ function postProcess(result) {
|
|||||||
result.debugMode = safe.JSON.parse(result.debugModeJson);
|
result.debugMode = safe.JSON.parse(result.debugModeJson);
|
||||||
delete result.debugModeJson;
|
delete result.debugModeJson;
|
||||||
|
|
||||||
|
assert(result.servicesConfigJson === null || typeof result.servicesConfigJson === 'string');
|
||||||
|
result.servicesConfig = safe.JSON.parse(result.servicesConfigJson) || {};
|
||||||
|
delete result.servicesConfigJson;
|
||||||
|
|
||||||
|
assert(result.bindsJson === null || typeof result.bindsJson === 'string');
|
||||||
|
result.binds = safe.JSON.parse(result.bindsJson) || {};
|
||||||
|
delete result.bindsJson;
|
||||||
|
|
||||||
result.alternateDomains = result.alternateDomains || [];
|
result.alternateDomains = result.alternateDomains || [];
|
||||||
result.alternateDomains.forEach(function (d) {
|
result.alternateDomains.forEach(function (d) {
|
||||||
delete d.appId;
|
delete d.appId;
|
||||||
@@ -427,7 +435,7 @@ function updateWithConstraints(id, app, constraints, callback) {
|
|||||||
|
|
||||||
var fields = [ ], values = [ ];
|
var fields = [ ], values = [ ];
|
||||||
for (var p in app) {
|
for (var p in app) {
|
||||||
if (p === 'manifest' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode' || p === 'error' || p === 'reverseProxyConfig') {
|
if (p === 'manifest' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode' || p === 'error' || p === 'reverseProxyConfig' || p === 'servicesConfig' || p === 'binds') {
|
||||||
fields.push(`${p}Json = ?`);
|
fields.push(`${p}Json = ?`);
|
||||||
values.push(JSON.stringify(app[p]));
|
values.push(JSON.stringify(app[p]));
|
||||||
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains' && p !== 'env') {
|
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains' && p !== 'env') {
|
||||||
|
|||||||
+6
-13
@@ -73,7 +73,6 @@ function checkAppHealth(app, callback) {
|
|||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
if (app.installationState !== apps.ISTATE_INSTALLED || app.runState !== apps.RSTATE_RUNNING) {
|
if (app.installationState !== apps.ISTATE_INSTALLED || app.runState !== apps.RSTATE_RUNNING) {
|
||||||
debugApp(app, 'skipped. istate:%s rstate:%s', app.installationState, app.runState);
|
|
||||||
return callback(null);
|
return callback(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,10 +102,8 @@ function checkAppHealth(app, callback) {
|
|||||||
.timeout(HEALTHCHECK_INTERVAL)
|
.timeout(HEALTHCHECK_INTERVAL)
|
||||||
.end(function (error, res) {
|
.end(function (error, res) {
|
||||||
if (error && !error.response) {
|
if (error && !error.response) {
|
||||||
debugApp(app, 'not alive (network error): %s', error.message);
|
|
||||||
setHealth(app, apps.HEALTH_UNHEALTHY, callback);
|
setHealth(app, apps.HEALTH_UNHEALTHY, callback);
|
||||||
} else if (res.statusCode >= 400) { // 2xx and 3xx are ok
|
} else if (res.statusCode >= 400) { // 2xx and 3xx are ok
|
||||||
debugApp(app, 'not alive : %s', error || res.status);
|
|
||||||
setHealth(app, apps.HEALTH_UNHEALTHY, callback);
|
setHealth(app, apps.HEALTH_UNHEALTHY, callback);
|
||||||
} else {
|
} else {
|
||||||
setHealth(app, apps.HEALTH_HEALTHY, callback);
|
setHealth(app, apps.HEALTH_HEALTHY, callback);
|
||||||
@@ -180,18 +177,14 @@ function processDockerEvents(intervalSecs, callback) {
|
|||||||
function processApp(callback) {
|
function processApp(callback) {
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
apps.getAll(function (error, result) {
|
apps.getAll(function (error, allApps) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
async.each(result, checkAppHealth, function (error) {
|
async.each(allApps, checkAppHealth, function (error) {
|
||||||
if (error) console.error(error);
|
const alive = allApps
|
||||||
|
.filter(function (a) { return a.installationState === apps.ISTATE_INSTALLED && a.runState === apps.RSTATE_RUNNING && a.health === apps.HEALTH_HEALTHY; });
|
||||||
|
|
||||||
const alive = result
|
debug(`app health: ${alive.length} alive / ${allApps.length - alive.length} dead.` + (error ? ` ${error.reason}` : ''));
|
||||||
.filter(function (a) { return a.installationState === apps.ISTATE_INSTALLED && a.runState === apps.RSTATE_RUNNING && a.health === apps.HEALTH_HEALTHY; })
|
|
||||||
.map(a => a.fqdn)
|
|
||||||
.join(', ');
|
|
||||||
|
|
||||||
debug('apps alive: [%s]', alive);
|
|
||||||
|
|
||||||
callback(null);
|
callback(null);
|
||||||
});
|
});
|
||||||
@@ -206,7 +199,7 @@ function run(intervalSecs, callback) {
|
|||||||
processApp, // this is first because docker.getEvents seems to get 'stuck' sometimes
|
processApp, // this is first because docker.getEvents seems to get 'stuck' sometimes
|
||||||
processDockerEvents.bind(null, intervalSecs)
|
processDockerEvents.bind(null, intervalSecs)
|
||||||
], function (error) {
|
], function (error) {
|
||||||
if (error) debug(error);
|
if (error) debug(`run: could not check app health. ${error.message}`);
|
||||||
|
|
||||||
callback();
|
callback();
|
||||||
});
|
});
|
||||||
|
|||||||
+115
-13
@@ -20,6 +20,7 @@ exports = module.exports = {
|
|||||||
setTags: setTags,
|
setTags: setTags,
|
||||||
setMemoryLimit: setMemoryLimit,
|
setMemoryLimit: setMemoryLimit,
|
||||||
setCpuShares: setCpuShares,
|
setCpuShares: setCpuShares,
|
||||||
|
setBinds: setBinds,
|
||||||
setAutomaticBackup: setAutomaticBackup,
|
setAutomaticBackup: setAutomaticBackup,
|
||||||
setAutomaticUpdate: setAutomaticUpdate,
|
setAutomaticUpdate: setAutomaticUpdate,
|
||||||
setReverseProxyConfig: setReverseProxyConfig,
|
setReverseProxyConfig: setReverseProxyConfig,
|
||||||
@@ -57,6 +58,7 @@ exports = module.exports = {
|
|||||||
restoreInstalledApps: restoreInstalledApps,
|
restoreInstalledApps: restoreInstalledApps,
|
||||||
configureInstalledApps: configureInstalledApps,
|
configureInstalledApps: configureInstalledApps,
|
||||||
schedulePendingTasks: schedulePendingTasks,
|
schedulePendingTasks: schedulePendingTasks,
|
||||||
|
restartAppsUsingAddons: restartAppsUsingAddons,
|
||||||
|
|
||||||
getDataDir: getDataDir,
|
getDataDir: getDataDir,
|
||||||
getIconPath: getIconPath,
|
getIconPath: getIconPath,
|
||||||
@@ -332,6 +334,20 @@ function validateEnv(env) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateBinds(binds) {
|
||||||
|
for (let name of Object.keys(binds)) {
|
||||||
|
// just have friendly characters under /media
|
||||||
|
if (!/^[-0-9a-zA-Z_@$=#.%+]+$/.test(name)) return new BoxError(BoxError.BAD_FIELD, `Invalid bind name: ${name}`);
|
||||||
|
|
||||||
|
const bind = binds[name];
|
||||||
|
|
||||||
|
if (!bind.hostPath.startsWith('/mnt') && !bind.hostPath.startsWith('/media')) return new BoxError(BoxError.BAD_FIELD, 'hostPath must be in /mnt or /media');
|
||||||
|
if (path.normalize(bind.hostPath) !== bind.hostPath) return new BoxError(BoxError.BAD_FIELD, 'hostPath is not normalized');
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function validateDataDir(dataDir) {
|
function validateDataDir(dataDir) {
|
||||||
if (dataDir === null) return null;
|
if (dataDir === null) return null;
|
||||||
|
|
||||||
@@ -404,14 +420,14 @@ function removeInternalFields(app) {
|
|||||||
'location', 'domain', 'fqdn', 'mailboxName', 'mailboxDomain',
|
'location', 'domain', 'fqdn', 'mailboxName', 'mailboxDomain',
|
||||||
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'cpuShares',
|
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'cpuShares',
|
||||||
'sso', 'debugMode', 'reverseProxyConfig', 'enableBackup', 'creationTime', 'updateTime', 'ts', 'tags',
|
'sso', 'debugMode', 'reverseProxyConfig', 'enableBackup', 'creationTime', 'updateTime', 'ts', 'tags',
|
||||||
'label', 'alternateDomains', 'env', 'enableAutomaticUpdate', 'dataDir');
|
'label', 'alternateDomains', 'env', 'enableAutomaticUpdate', 'dataDir', 'binds');
|
||||||
}
|
}
|
||||||
|
|
||||||
// non-admins can only see these
|
// non-admins can only see these
|
||||||
function removeRestrictedFields(app) {
|
function removeRestrictedFields(app) {
|
||||||
return _.pick(app,
|
return _.pick(app,
|
||||||
'id', 'appStoreId', 'installationState', 'error', 'runState', 'health', 'taskId',
|
'id', 'appStoreId', 'installationState', 'error', 'runState', 'health', 'taskId', 'alternateDomains', 'sso',
|
||||||
'location', 'domain', 'fqdn', 'manifest', 'portBindings', 'iconUrl', 'creationTime', 'ts', 'tags', 'label');
|
'location', 'domain', 'fqdn', 'manifest', 'portBindings', 'iconUrl', 'creationTime', 'ts', 'tags', 'label', 'enableBackup');
|
||||||
}
|
}
|
||||||
|
|
||||||
function getIconUrlSync(app) {
|
function getIconUrlSync(app) {
|
||||||
@@ -605,6 +621,8 @@ function downloadManifest(appStoreId, manifest, callback) {
|
|||||||
|
|
||||||
if (result.statusCode !== 200) return callback(new BoxError(BoxError.NOT_FOUND, util.format('Failed to get app info from store.', result.statusCode, result.text)));
|
if (result.statusCode !== 200) return callback(new BoxError(BoxError.NOT_FOUND, util.format('Failed to get app info from store.', result.statusCode, result.text)));
|
||||||
|
|
||||||
|
if (!result.body.manifest || typeof result.body.manifest !== 'object') return callback(new BoxError(BoxError.NOT_FOUND, util.format('Missing manifest. Failed to get app info from store.', result.statusCode, result.text)));
|
||||||
|
|
||||||
callback(null, parts[0], result.body.manifest);
|
callback(null, parts[0], result.body.manifest);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -622,7 +640,7 @@ function scheduleTask(appId, installationState, taskId, callback) {
|
|||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
appTaskManager.scheduleTask(appId, taskId, function (error) {
|
appTaskManager.scheduleTask(appId, taskId, function (error) {
|
||||||
debug(`scheduleTask: task ${taskId} of $${appId} completed`);
|
debug(`scheduleTask: task ${taskId} of ${appId} completed`);
|
||||||
if (error && (error.code === tasks.ECRASHED || error.code === tasks.ESTOPPED)) { // if task crashed, update the error
|
if (error && (error.code === tasks.ECRASHED || error.code === tasks.ESTOPPED)) { // if task crashed, update the error
|
||||||
debug(`Apptask crashed/stopped: ${error.message}`);
|
debug(`Apptask crashed/stopped: ${error.message}`);
|
||||||
let boxError = new BoxError(BoxError.TASK_ERROR, error.message);
|
let boxError = new BoxError(BoxError.TASK_ERROR, error.message);
|
||||||
@@ -678,6 +696,11 @@ function checkAppState(app, state) {
|
|||||||
if (state !== exports.ISTATE_PENDING_UNINSTALL && state !== exports.ISTATE_PENDING_RESTORE) return new BoxError(BoxError.BAD_STATE, 'Not allowed in error state');
|
if (state !== exports.ISTATE_PENDING_UNINSTALL && state !== exports.ISTATE_PENDING_RESTORE) return new BoxError(BoxError.BAD_STATE, 'Not allowed in error state');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (app.runState === exports.RSTATE_STOPPED) {
|
||||||
|
// can't backup or restore since app addons are down. can't update because migration scripts won't run
|
||||||
|
if (state === exports.ISTATE_PENDING_UPDATE || state === exports.ISTATE_PENDING_BACKUP || state === exports.ISTATE_PENDING_RESTORE) return new BoxError(BoxError.BAD_STATE, 'Not allowed in stopped state');
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -761,6 +784,8 @@ function install(data, auditSource, callback) {
|
|||||||
error = validateEnv(env);
|
error = validateEnv(env);
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
if (settings.isDemo() && constants.DEMO_BLACKLISTED_APPS.includes(appStoreId)) return callback(new BoxError(BoxError.BAD_FIELD, 'This app is blacklisted in the demo'));
|
||||||
|
|
||||||
const mailboxName = hasMailAddon(manifest) ? mailboxNameForLocation(location, manifest) : null;
|
const mailboxName = hasMailAddon(manifest) ? mailboxNameForLocation(location, manifest) : null;
|
||||||
const mailboxDomain = hasMailAddon(manifest) ? domain : null;
|
const mailboxDomain = hasMailAddon(manifest) ? domain : null;
|
||||||
const appId = uuid.v4();
|
const appId = uuid.v4();
|
||||||
@@ -968,6 +993,32 @@ function setCpuShares(app, cpuShares, auditSource, callback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setBinds(app, binds, auditSource, callback) {
|
||||||
|
assert.strictEqual(typeof app, 'object');
|
||||||
|
assert(binds && typeof binds === 'object');
|
||||||
|
assert.strictEqual(typeof auditSource, 'object');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
const appId = app.id;
|
||||||
|
let error = checkAppState(app, exports.ISTATE_PENDING_RECREATE_CONTAINER);
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
error = validateBinds(binds);
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
const task = {
|
||||||
|
args: {},
|
||||||
|
values: { binds }
|
||||||
|
};
|
||||||
|
addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task, function (error, result) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, binds, taskId: result.taskId });
|
||||||
|
|
||||||
|
callback(null, { taskId: result.taskId });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function setEnvironment(app, env, auditSource, callback) {
|
function setEnvironment(app, env, auditSource, callback) {
|
||||||
assert.strictEqual(typeof app, 'object');
|
assert.strictEqual(typeof app, 'object');
|
||||||
assert.strictEqual(typeof env, 'object');
|
assert.strictEqual(typeof env, 'object');
|
||||||
@@ -1435,7 +1486,8 @@ function restore(app, backupId, auditSource, callback) {
|
|||||||
func(function (error, backupInfo) {
|
func(function (error, backupInfo) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
if (!backupInfo.manifest) callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Could not get restore manifest'));
|
if (!backupInfo.manifest) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Could not get restore manifest'));
|
||||||
|
if (backupInfo.encryptionVersion === 1) return callback(new BoxError(BoxError.BAD_FIELD, 'This encrypted backup was created with an older Cloudron version and has to be restored using the CLI tool'));
|
||||||
|
|
||||||
// re-validate because this new box version may not accept old configs
|
// re-validate because this new box version may not accept old configs
|
||||||
error = checkManifestConstraints(backupInfo.manifest);
|
error = checkManifestConstraints(backupInfo.manifest);
|
||||||
@@ -1495,6 +1547,15 @@ function importApp(app, data, auditSource, callback) {
|
|||||||
testBackupConfig(function (error) {
|
testBackupConfig(function (error) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
if (backupConfig) {
|
||||||
|
if ('password' in backupConfig) {
|
||||||
|
backupConfig.encryption = backups.generateEncryptionKeysSync(backupConfig.password);
|
||||||
|
delete backupConfig.password;
|
||||||
|
} else {
|
||||||
|
backupConfig.encryption = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const restoreConfig = { backupId, backupFormat, backupConfig };
|
const restoreConfig = { backupId, backupFormat, backupConfig };
|
||||||
|
|
||||||
const task = {
|
const task = {
|
||||||
@@ -1553,7 +1614,8 @@ function clone(app, data, user, auditSource, callback) {
|
|||||||
backups.get(backupId, function (error, backupInfo) {
|
backups.get(backupId, function (error, backupInfo) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
if (!backupInfo.manifest) callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Could not get restore config'));
|
if (!backupInfo.manifest) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Could not get restore config'));
|
||||||
|
if (backupInfo.encryptionVersion === 1) return callback(new BoxError(BoxError.BAD_FIELD, 'This encrypted backup was created with an older Cloudron version and cannot be cloned'));
|
||||||
|
|
||||||
const manifest = backupInfo.manifest, appStoreId = app.appStoreId;
|
const manifest = backupInfo.manifest, appStoreId = app.appStoreId;
|
||||||
|
|
||||||
@@ -1775,14 +1837,25 @@ function exec(app, options, callback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function canAutoupdateApp(app, newManifest) {
|
function canAutoupdateApp(app, updateInfo) {
|
||||||
|
assert.strictEqual(typeof app, 'object');
|
||||||
|
assert.strictEqual(typeof updateInfo, 'object');
|
||||||
|
|
||||||
|
const manifest = updateInfo.manifest;
|
||||||
|
|
||||||
if (!app.enableAutomaticUpdate) return false;
|
if (!app.enableAutomaticUpdate) return false;
|
||||||
if ((semver.major(app.manifest.version) !== 0) && (semver.major(app.manifest.version) !== semver.major(newManifest.version))) return false; // major changes are blocking
|
|
||||||
|
// for invalid subscriptions the appstore does not return a dockerImage
|
||||||
|
if (!manifest.dockerImage) return false;
|
||||||
|
|
||||||
|
if (updateInfo.unstable) return false; // only manual update allowed for unstable updates
|
||||||
|
|
||||||
|
if ((semver.major(app.manifest.version) !== 0) && (semver.major(app.manifest.version) !== semver.major(manifest.version))) return false; // major changes are blocking
|
||||||
|
|
||||||
if (app.runState === exports.RSTATE_STOPPED) return false; // stopped apps won't run migration scripts and shouldn't be updated
|
if (app.runState === exports.RSTATE_STOPPED) return false; // stopped apps won't run migration scripts and shouldn't be updated
|
||||||
|
|
||||||
const newTcpPorts = newManifest.tcpPorts || { };
|
const newTcpPorts = manifest.tcpPorts || { };
|
||||||
const newUdpPorts = newManifest.udpPorts || { };
|
const newUdpPorts = manifest.udpPorts || { };
|
||||||
const portBindings = app.portBindings; // this is never null
|
const portBindings = app.portBindings; // this is never null
|
||||||
|
|
||||||
for (let portName in portBindings) {
|
for (let portName in portBindings) {
|
||||||
@@ -1807,7 +1880,7 @@ function autoupdateApps(updateInfo, auditSource, callback) { // updateInfo is {
|
|||||||
return iteratorDone();
|
return iteratorDone();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!canAutoupdateApp(app, updateInfo[appId].manifest)) {
|
if (!canAutoupdateApp(app, updateInfo[appId])) {
|
||||||
debug(`app ${app.fqdn} requires manual update`);
|
debug(`app ${app.fqdn} requires manual update`);
|
||||||
return iteratorDone();
|
return iteratorDone();
|
||||||
}
|
}
|
||||||
@@ -1852,7 +1925,7 @@ function listBackups(app, page, perPage, callback) {
|
|||||||
assert(typeof perPage === 'number' && perPage > 0);
|
assert(typeof perPage === 'number' && perPage > 0);
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
backups.getByAppIdPaged(page, perPage, app.id, function (error, results) {
|
backups.getByIdentifierAndStatePaged(app.id, backups.BACKUP_STATE_NORMAL, page, perPage, function (error, results) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
callback(null, results);
|
callback(null, results);
|
||||||
@@ -1869,7 +1942,7 @@ function restoreInstalledApps(callback) {
|
|||||||
apps = apps.filter(app => app.installationState !== exports.ISTATE_PENDING_RESTORE); // safeguard against tasks being created non-stop if we crash on startup
|
apps = apps.filter(app => app.installationState !== exports.ISTATE_PENDING_RESTORE); // safeguard against tasks being created non-stop if we crash on startup
|
||||||
|
|
||||||
async.eachSeries(apps, function (app, iteratorDone) {
|
async.eachSeries(apps, function (app, iteratorDone) {
|
||||||
backups.getByAppIdPaged(1, 1, app.id, function (error, results) {
|
backups.getByIdentifierAndStatePaged(app.id, backups.BACKUP_STATE_NORMAL, 1, 1, function (error, results) {
|
||||||
let installationState, restoreConfig, oldManifest;
|
let installationState, restoreConfig, oldManifest;
|
||||||
if (!error && results.length) {
|
if (!error && results.length) {
|
||||||
installationState = exports.ISTATE_PENDING_RESTORE;
|
installationState = exports.ISTATE_PENDING_RESTORE;
|
||||||
@@ -1930,6 +2003,35 @@ function configureInstalledApps(callback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function restartAppsUsingAddons(changedAddons, callback) {
|
||||||
|
assert(Array.isArray(changedAddons));
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
getAll(function (error, apps) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
apps = apps.filter(app => app.manifest.addons && _.intersection(Object.keys(app.manifest.addons), changedAddons).length !== 0);
|
||||||
|
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_RESTART); // safeguard against tasks being created non-stop restart if we crash on startup
|
||||||
|
apps = apps.filter(app => app.runState !== exports.RSTATE_STOPPED); // don't start stopped apps
|
||||||
|
|
||||||
|
async.eachSeries(apps, function (app, iteratorDone) {
|
||||||
|
debug(`restartAppsUsingAddons: marking ${app.fqdn} for restart`);
|
||||||
|
|
||||||
|
const task = {
|
||||||
|
args: {},
|
||||||
|
values: { runState: exports.RSTATE_RUNNING }
|
||||||
|
};
|
||||||
|
addTask(app.id, exports.ISTATE_PENDING_RESTART, task, function (error, result) {
|
||||||
|
if (error) debug(`restartAppsUsingAddons: error marking ${app.fqdn} for restart: ${JSON.stringify(error)}`);
|
||||||
|
else debug(`restartAppsUsingAddons: marked ${app.id} for restart with taskId ${result.taskId}`);
|
||||||
|
|
||||||
|
iteratorDone(); // ignore error
|
||||||
|
});
|
||||||
|
}, callback);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// auto-restart app tasks after a crash
|
// auto-restart app tasks after a crash
|
||||||
function schedulePendingTasks(callback) {
|
function schedulePendingTasks(callback) {
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|||||||
+42
-156
@@ -11,7 +11,6 @@ exports = module.exports = {
|
|||||||
trackFinishedSetup: trackFinishedSetup,
|
trackFinishedSetup: trackFinishedSetup,
|
||||||
|
|
||||||
registerWithLoginCredentials: registerWithLoginCredentials,
|
registerWithLoginCredentials: registerWithLoginCredentials,
|
||||||
registerWithLicense: registerWithLicense,
|
|
||||||
|
|
||||||
purchaseApp: purchaseApp,
|
purchaseApp: purchaseApp,
|
||||||
unpurchaseApp: unpurchaseApp,
|
unpurchaseApp: unpurchaseApp,
|
||||||
@@ -20,8 +19,6 @@ exports = module.exports = {
|
|||||||
getSubscription: getSubscription,
|
getSubscription: getSubscription,
|
||||||
isFreePlan: isFreePlan,
|
isFreePlan: isFreePlan,
|
||||||
|
|
||||||
sendAliveStatus: sendAliveStatus,
|
|
||||||
|
|
||||||
getAppUpdate: getAppUpdate,
|
getAppUpdate: getAppUpdate,
|
||||||
getBoxUpdate: getBoxUpdate,
|
getBoxUpdate: getBoxUpdate,
|
||||||
|
|
||||||
@@ -34,32 +31,26 @@ var apps = require('./apps.js'),
|
|||||||
BoxError = require('./boxerror.js'),
|
BoxError = require('./boxerror.js'),
|
||||||
constants = require('./constants.js'),
|
constants = require('./constants.js'),
|
||||||
debug = require('debug')('box:appstore'),
|
debug = require('debug')('box:appstore'),
|
||||||
domains = require('./domains.js'),
|
|
||||||
eventlog = require('./eventlog.js'),
|
eventlog = require('./eventlog.js'),
|
||||||
groups = require('./groups.js'),
|
|
||||||
mail = require('./mail.js'),
|
|
||||||
os = require('os'),
|
|
||||||
paths = require('./paths.js'),
|
paths = require('./paths.js'),
|
||||||
safe = require('safetydance'),
|
safe = require('safetydance'),
|
||||||
semver = require('semver'),
|
semver = require('semver'),
|
||||||
settings = require('./settings.js'),
|
settings = require('./settings.js'),
|
||||||
superagent = require('superagent'),
|
superagent = require('superagent'),
|
||||||
users = require('./users.js'),
|
|
||||||
util = require('util');
|
util = require('util');
|
||||||
|
|
||||||
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
|
||||||
|
|
||||||
// These are the default options and will be adjusted once a subscription state is obtained
|
// These are the default options and will be adjusted once a subscription state is obtained
|
||||||
// Keep in sync with appstore/routes/cloudrons.js
|
// Keep in sync with appstore/routes/cloudrons.js
|
||||||
let gFeatures = {
|
let gFeatures = {
|
||||||
userMaxCount: null,
|
userMaxCount: 5,
|
||||||
externalLdap: true,
|
domainMaxCount: 1,
|
||||||
eventLog: true,
|
externalLdap: false,
|
||||||
privateDockerRegistry: true,
|
privateDockerRegistry: false,
|
||||||
branding: true,
|
branding: false,
|
||||||
userManager: true,
|
support: false,
|
||||||
multiAdmin: true,
|
directoryConfig: false,
|
||||||
support: true
|
mailboxMaxCount: 5,
|
||||||
|
emailPremium: false
|
||||||
};
|
};
|
||||||
|
|
||||||
// attempt to load feature cache in case appstore would be down
|
// attempt to load feature cache in case appstore would be down
|
||||||
@@ -127,7 +118,7 @@ function registerUser(email, password, callback) {
|
|||||||
const url = settings.apiServerOrigin() + '/api/v1/register_user';
|
const url = settings.apiServerOrigin() + '/api/v1/register_user';
|
||||||
superagent.post(url).send(data).timeout(30 * 1000).end(function (error, result) {
|
superagent.post(url).send(data).timeout(30 * 1000).end(function (error, result) {
|
||||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||||
if (result.statusCode === 409) return callback(new BoxError(BoxError.ALREADY_EXISTS));
|
if (result.statusCode === 409) return callback(new BoxError(BoxError.ALREADY_EXISTS, error.message));
|
||||||
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `register status code: ${result.statusCode}`));
|
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `register status code: ${result.statusCode}`));
|
||||||
|
|
||||||
callback(null);
|
callback(null);
|
||||||
@@ -235,112 +226,8 @@ function unpurchaseApp(appId, data, callback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendAliveStatus(callback) {
|
function getBoxUpdate(options, callback) {
|
||||||
callback = callback || NOOP_CALLBACK;
|
assert.strictEqual(typeof options, 'object');
|
||||||
|
|
||||||
let allSettings, allDomains, mailDomains, loginEvents, userCount, groupCount;
|
|
||||||
|
|
||||||
async.series([
|
|
||||||
function (callback) {
|
|
||||||
settings.getAll(function (error, result) {
|
|
||||||
if (error) return callback(error);
|
|
||||||
allSettings = result;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
function (callback) {
|
|
||||||
domains.getAll(function (error, result) {
|
|
||||||
if (error) return callback(error);
|
|
||||||
allDomains = result;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
function (callback) {
|
|
||||||
mail.getDomains(function (error, result) {
|
|
||||||
if (error) return callback(error);
|
|
||||||
mailDomains = result;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
function (callback) {
|
|
||||||
eventlog.getAllPaged([ eventlog.ACTION_USER_LOGIN ], null, 1, 1, function (error, result) {
|
|
||||||
if (error) return callback(error);
|
|
||||||
loginEvents = result;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
function (callback) {
|
|
||||||
users.count(function (error, result) {
|
|
||||||
if (error) return callback(error);
|
|
||||||
userCount = result;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
function (callback) {
|
|
||||||
groups.count(function (error, result) {
|
|
||||||
if (error) return callback(error);
|
|
||||||
groupCount = result;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
], function (error) {
|
|
||||||
if (error) return callback(error);
|
|
||||||
|
|
||||||
var backendSettings = {
|
|
||||||
backupConfig: {
|
|
||||||
provider: allSettings[settings.BACKUP_CONFIG_KEY].provider,
|
|
||||||
hardlinks: !allSettings[settings.BACKUP_CONFIG_KEY].noHardlinks
|
|
||||||
},
|
|
||||||
domainConfig: {
|
|
||||||
count: allDomains.length,
|
|
||||||
domains: Array.from(new Set(allDomains.map(function (d) { return { domain: d.domain, provider: d.provider }; })))
|
|
||||||
},
|
|
||||||
mailConfig: {
|
|
||||||
outboundCount: mailDomains.length,
|
|
||||||
inboundCount: mailDomains.filter(function (d) { return d.enabled; }).length,
|
|
||||||
catchAllCount: mailDomains.filter(function (d) { return d.catchAll.length !== 0; }).length,
|
|
||||||
relayProviders: Array.from(new Set(mailDomains.map(function (d) { return d.relay.provider; })))
|
|
||||||
},
|
|
||||||
userCount: userCount,
|
|
||||||
groupCount: groupCount,
|
|
||||||
appAutoupdatePattern: allSettings[settings.APP_AUTOUPDATE_PATTERN_KEY],
|
|
||||||
boxAutoupdatePattern: allSettings[settings.BOX_AUTOUPDATE_PATTERN_KEY],
|
|
||||||
timeZone: allSettings[settings.TIME_ZONE_KEY],
|
|
||||||
sysinfoProvider: allSettings[settings.SYSINFO_CONFIG_KEY].provider
|
|
||||||
};
|
|
||||||
|
|
||||||
var data = {
|
|
||||||
version: constants.VERSION,
|
|
||||||
adminFqdn: settings.adminFqdn(),
|
|
||||||
provider: settings.provider(),
|
|
||||||
backendSettings: backendSettings,
|
|
||||||
machine: {
|
|
||||||
cpus: os.cpus(),
|
|
||||||
totalmem: os.totalmem()
|
|
||||||
},
|
|
||||||
events: {
|
|
||||||
lastLogin: loginEvents[0] ? (new Date(loginEvents[0].creationTime).getTime()) : 0
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
getCloudronToken(function (error, token) {
|
|
||||||
if (error) return callback(error);
|
|
||||||
|
|
||||||
const url = `${settings.apiServerOrigin()}/api/v1/alive`;
|
|
||||||
superagent.post(url).send(data).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
|
|
||||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error));
|
|
||||||
if (result.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
|
|
||||||
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
|
||||||
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
|
|
||||||
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Sending alive status failed. %s %j', result.status, result.body)));
|
|
||||||
|
|
||||||
callback(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getBoxUpdate(callback) {
|
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
getCloudronToken(function (error, token) {
|
getCloudronToken(function (error, token) {
|
||||||
@@ -348,7 +235,13 @@ function getBoxUpdate(callback) {
|
|||||||
|
|
||||||
const url = `${settings.apiServerOrigin()}/api/v1/boxupdate`;
|
const url = `${settings.apiServerOrigin()}/api/v1/boxupdate`;
|
||||||
|
|
||||||
superagent.get(url).query({ accessToken: token, boxVersion: constants.VERSION }).timeout(10 * 1000).end(function (error, result) {
|
const query = {
|
||||||
|
accessToken: token,
|
||||||
|
boxVersion: constants.VERSION,
|
||||||
|
automatic: options.automatic
|
||||||
|
};
|
||||||
|
|
||||||
|
superagent.get(url).query(query).timeout(10 * 1000).end(function (error, result) {
|
||||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||||
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||||
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
|
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
|
||||||
@@ -374,16 +267,24 @@ function getBoxUpdate(callback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAppUpdate(app, callback) {
|
function getAppUpdate(app, options, callback) {
|
||||||
assert.strictEqual(typeof app, 'object');
|
assert.strictEqual(typeof app, 'object');
|
||||||
|
assert.strictEqual(typeof options, 'object');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
getCloudronToken(function (error, token) {
|
getCloudronToken(function (error, token) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
const url = `${settings.apiServerOrigin()}/api/v1/appupdate`;
|
const url = `${settings.apiServerOrigin()}/api/v1/appupdate`;
|
||||||
|
const query = {
|
||||||
|
accessToken: token,
|
||||||
|
boxVersion: constants.VERSION,
|
||||||
|
appId: app.appStoreId,
|
||||||
|
appVersion: app.manifest.version,
|
||||||
|
automatic: options.automatic
|
||||||
|
};
|
||||||
|
|
||||||
superagent.get(url).query({ accessToken: token, boxVersion: constants.VERSION, appId: app.appStoreId, appVersion: app.manifest.version }).timeout(10 * 1000).end(function (error, result) {
|
superagent.get(url).query(query).timeout(10 * 1000).end(function (error, result) {
|
||||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error));
|
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error));
|
||||||
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||||
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
|
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
|
||||||
@@ -401,7 +302,9 @@ function getAppUpdate(app, callback) {
|
|||||||
return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Malformed update: %s %s', result.statusCode, result.text)));
|
return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Malformed update: %s %s', result.statusCode, result.text)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// { id, creationDate, manifest }
|
updateInfo.unstable = !!updateInfo.unstable;
|
||||||
|
|
||||||
|
// { id, creationDate, manifest, unstable }
|
||||||
callback(null, updateInfo);
|
callback(null, updateInfo);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -415,7 +318,7 @@ function registerCloudron(data, callback) {
|
|||||||
|
|
||||||
superagent.post(url).send(data).timeout(30 * 1000).end(function (error, result) {
|
superagent.post(url).send(data).timeout(30 * 1000).end(function (error, result) {
|
||||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||||
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Unable to register cloudron: ${error.message}`));
|
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Unable to register cloudron: ${result.statusCode} ${error.message}`));
|
||||||
|
|
||||||
// cloudronId, token, licenseKey
|
// cloudronId, token, licenseKey
|
||||||
if (!result.body.cloudronId) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response - no cloudron id'));
|
if (!result.body.cloudronId) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response - no cloudron id'));
|
||||||
@@ -438,18 +341,16 @@ function registerCloudron(data, callback) {
|
|||||||
|
|
||||||
// This works without a Cloudron token as this Cloudron was not yet registered
|
// This works without a Cloudron token as this Cloudron was not yet registered
|
||||||
let gBeginSetupAlreadyTracked = false;
|
let gBeginSetupAlreadyTracked = false;
|
||||||
function trackBeginSetup(provider) {
|
function trackBeginSetup() {
|
||||||
assert.strictEqual(typeof provider, 'string');
|
|
||||||
|
|
||||||
// avoid browser reload double tracking, not perfect since box might restart, but covers most cases and is simple
|
// avoid browser reload double tracking, not perfect since box might restart, but covers most cases and is simple
|
||||||
if (gBeginSetupAlreadyTracked) return;
|
if (gBeginSetupAlreadyTracked) return;
|
||||||
gBeginSetupAlreadyTracked = true;
|
gBeginSetupAlreadyTracked = true;
|
||||||
|
|
||||||
const url = `${settings.apiServerOrigin()}/api/v1/helper/setup_begin`;
|
const url = `${settings.apiServerOrigin()}/api/v1/helper/setup_begin`;
|
||||||
|
|
||||||
superagent.post(url).send({ provider }).timeout(30 * 1000).end(function (error, result) {
|
superagent.post(url).send({}).timeout(30 * 1000).end(function (error, result) {
|
||||||
if (error && !error.response) return console.error(error.message);
|
if (error && !error.response) return debug(`trackBeginSetup: ${error.message}`);
|
||||||
if (result.statusCode !== 200) return console.error(error.message);
|
if (result.statusCode !== 200) return debug(`trackBeginSetup: ${result.statusCode} ${error.message}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,23 +361,8 @@ function trackFinishedSetup(domain) {
|
|||||||
const url = `${settings.apiServerOrigin()}/api/v1/helper/setup_finished`;
|
const url = `${settings.apiServerOrigin()}/api/v1/helper/setup_finished`;
|
||||||
|
|
||||||
superagent.post(url).send({ domain }).timeout(30 * 1000).end(function (error, result) {
|
superagent.post(url).send({ domain }).timeout(30 * 1000).end(function (error, result) {
|
||||||
if (error && !error.response) return console.error(error.message);
|
if (error && !error.response) return debug(`trackFinishedSetup: ${error.message}`);
|
||||||
if (result.statusCode !== 200) return console.error(error.message);
|
if (result.statusCode !== 200) return debug(`trackFinishedSetup: ${result.statusCode} ${error.message}`);
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function registerWithLicense(license, domain, callback) {
|
|
||||||
assert.strictEqual(typeof license, 'string');
|
|
||||||
assert.strictEqual(typeof domain, 'string');
|
|
||||||
assert.strictEqual(typeof callback, 'function');
|
|
||||||
|
|
||||||
getCloudronToken(function (error, token) {
|
|
||||||
if (token) return callback(new BoxError(BoxError.CONFLICT));
|
|
||||||
|
|
||||||
const provider = settings.provider();
|
|
||||||
const version = constants.VERSION;
|
|
||||||
|
|
||||||
registerCloudron({ license, domain, provider, version }, callback);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -491,7 +377,7 @@ function registerWithLoginCredentials(options, callback) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getCloudronToken(function (error, token) {
|
getCloudronToken(function (error, token) {
|
||||||
if (token) return callback(new BoxError(BoxError.CONFLICT));
|
if (token) return callback(new BoxError(BoxError.CONFLICT, 'Cloudron is already registered'));
|
||||||
|
|
||||||
maybeSignup(function (error) {
|
maybeSignup(function (error) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
@@ -499,7 +385,7 @@ function registerWithLoginCredentials(options, callback) {
|
|||||||
login(options.email, options.password, options.totpToken || '', function (error, result) {
|
login(options.email, options.password, options.totpToken || '', function (error, result) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
registerCloudron({ domain: settings.adminDomain(), accessToken: result.accessToken, provider: settings.provider(), version: constants.VERSION, purpose: options.purpose || '' }, callback);
|
registerCloudron({ domain: settings.adminDomain(), accessToken: result.accessToken, version: constants.VERSION, purpose: options.purpose || '' }, callback);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -524,7 +410,7 @@ function createTicket(info, auditSource, callback) {
|
|||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
collectAppInfoIfNeeded(function (error, result) {
|
collectAppInfoIfNeeded(function (error, result) {
|
||||||
if (error) console.error('Unable to get app info', error);
|
if (error) return callback(error);
|
||||||
if (result) info.app = result;
|
if (result) info.app = result;
|
||||||
|
|
||||||
let url = settings.apiServerOrigin() + '/api/v1/ticket';
|
let url = settings.apiServerOrigin() + '/api/v1/ticket';
|
||||||
|
|||||||
+41
-14
@@ -17,8 +17,6 @@ exports = module.exports = {
|
|||||||
_waitForDnsPropagation: waitForDnsPropagation
|
_waitForDnsPropagation: waitForDnsPropagation
|
||||||
};
|
};
|
||||||
|
|
||||||
require('supererror')({ splatchError: true });
|
|
||||||
|
|
||||||
var addons = require('./addons.js'),
|
var addons = require('./addons.js'),
|
||||||
appdb = require('./appdb.js'),
|
appdb = require('./appdb.js'),
|
||||||
apps = require('./apps.js'),
|
apps = require('./apps.js'),
|
||||||
@@ -37,7 +35,6 @@ var addons = require('./addons.js'),
|
|||||||
eventlog = require('./eventlog.js'),
|
eventlog = require('./eventlog.js'),
|
||||||
fs = require('fs'),
|
fs = require('fs'),
|
||||||
manifestFormat = require('cloudron-manifestformat'),
|
manifestFormat = require('cloudron-manifestformat'),
|
||||||
mkdirp = require('mkdirp'),
|
|
||||||
net = require('net'),
|
net = require('net'),
|
||||||
os = require('os'),
|
os = require('os'),
|
||||||
path = require('path'),
|
path = require('path'),
|
||||||
@@ -46,6 +43,7 @@ var addons = require('./addons.js'),
|
|||||||
rimraf = require('rimraf'),
|
rimraf = require('rimraf'),
|
||||||
safe = require('safetydance'),
|
safe = require('safetydance'),
|
||||||
settings = require('./settings.js'),
|
settings = require('./settings.js'),
|
||||||
|
sftp = require('./sftp.js'),
|
||||||
shell = require('./shell.js'),
|
shell = require('./shell.js'),
|
||||||
superagent = require('superagent'),
|
superagent = require('superagent'),
|
||||||
sysinfo = require('./sysinfo.js'),
|
sysinfo = require('./sysinfo.js'),
|
||||||
@@ -176,7 +174,7 @@ function createAppDir(app, callback) {
|
|||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
const appDir = path.join(paths.APPS_DATA_DIR, app.id);
|
const appDir = path.join(paths.APPS_DATA_DIR, app.id);
|
||||||
mkdirp(appDir, function (error) {
|
fs.mkdir(appDir, { recursive: true }, function (error) {
|
||||||
if (error) return callback(new BoxError(BoxError.FS_ERROR, `Error creating directory: ${error.message}`, { appDir }));
|
if (error) return callback(new BoxError(BoxError.FS_ERROR, `Error creating directory: ${error.message}`, { appDir }));
|
||||||
|
|
||||||
callback(null);
|
callback(null);
|
||||||
@@ -580,6 +578,9 @@ function install(app, args, progressCallback, callback) {
|
|||||||
|
|
||||||
startApp.bind(null, app),
|
startApp.bind(null, app),
|
||||||
|
|
||||||
|
progressCallback.bind(null, { percent: 80, message: 'Configuring file manager' }),
|
||||||
|
sftp.rebuild.bind(null, {}),
|
||||||
|
|
||||||
progressCallback.bind(null, { percent: 85, message: 'Waiting for DNS propagation' }),
|
progressCallback.bind(null, { percent: 85, message: 'Waiting for DNS propagation' }),
|
||||||
exports._waitForDnsPropagation.bind(null, app),
|
exports._waitForDnsPropagation.bind(null, app),
|
||||||
|
|
||||||
@@ -741,7 +742,12 @@ function migrateDataDir(app, args, progressCallback, callback) {
|
|||||||
debugApp(app, 'error migrating data dir : %s', error);
|
debugApp(app, 'error migrating data dir : %s', error);
|
||||||
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
|
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
|
||||||
}
|
}
|
||||||
callback(null);
|
|
||||||
|
// We do this after the app has the new data commited to the database
|
||||||
|
sftp.rebuild({}, function (error) {
|
||||||
|
if (error) debug('migrateDataDir: failed to rebuild sftp addon:', error);
|
||||||
|
callback();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -776,6 +782,9 @@ function configure(app, args, progressCallback, callback) {
|
|||||||
|
|
||||||
startApp.bind(null, app),
|
startApp.bind(null, app),
|
||||||
|
|
||||||
|
progressCallback.bind(null, { percent: 80, message: 'Configuring file manager' }),
|
||||||
|
sftp.rebuild.bind(null, {}),
|
||||||
|
|
||||||
progressCallback.bind(null, { percent: 90, message: 'Configuring reverse proxy' }),
|
progressCallback.bind(null, { percent: 90, message: 'Configuring reverse proxy' }),
|
||||||
configureReverseProxy.bind(null, app),
|
configureReverseProxy.bind(null, app),
|
||||||
|
|
||||||
@@ -786,7 +795,8 @@ function configure(app, args, progressCallback, callback) {
|
|||||||
debugApp(app, 'error reconfiguring : %s', error);
|
debugApp(app, 'error reconfiguring : %s', error);
|
||||||
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
|
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
|
||||||
}
|
}
|
||||||
callback(null);
|
|
||||||
|
callback();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -853,7 +863,7 @@ function update(app, args, progressCallback, callback) {
|
|||||||
if (newTcpPorts[portName] || newUdpPorts[portName]) return callback(null); // port still in use
|
if (newTcpPorts[portName] || newUdpPorts[portName]) return callback(null); // port still in use
|
||||||
|
|
||||||
appdb.delPortBinding(currentPorts[portName], apps.PORT_TYPE_TCP, function (error) {
|
appdb.delPortBinding(currentPorts[portName], apps.PORT_TYPE_TCP, function (error) {
|
||||||
if (error && error.reason === BoxError.NOT_FOUND) console.error('Portbinding does not exist in database.');
|
if (error && error.reason === BoxError.NOT_FOUND) debug('update: portbinding does not exist in database', error);
|
||||||
else if (error) return next(error);
|
else if (error) return next(error);
|
||||||
|
|
||||||
// also delete from app object for further processing (the db is updated in the next step)
|
// also delete from app object for further processing (the db is updated in the next step)
|
||||||
@@ -869,14 +879,17 @@ function update(app, args, progressCallback, callback) {
|
|||||||
progressCallback.bind(null, { percent: 45, message: 'Downloading icon' }),
|
progressCallback.bind(null, { percent: 45, message: 'Downloading icon' }),
|
||||||
downloadIcon.bind(null, app),
|
downloadIcon.bind(null, app),
|
||||||
|
|
||||||
progressCallback.bind(null, { percent: 70, message: 'Updating addons' }),
|
progressCallback.bind(null, { percent: 60, message: 'Updating addons' }),
|
||||||
addons.setupAddons.bind(null, app, updateConfig.manifest.addons),
|
addons.setupAddons.bind(null, app, updateConfig.manifest.addons),
|
||||||
|
|
||||||
progressCallback.bind(null, { percent: 80, message: 'Creating container' }),
|
progressCallback.bind(null, { percent: 70, message: 'Creating container' }),
|
||||||
createContainer.bind(null, app),
|
createContainer.bind(null, app),
|
||||||
|
|
||||||
startApp.bind(null, app),
|
startApp.bind(null, app),
|
||||||
|
|
||||||
|
progressCallback.bind(null, { percent: 80, message: 'Configuring file manager' }),
|
||||||
|
sftp.rebuild.bind(null, {}),
|
||||||
|
|
||||||
progressCallback.bind(null, { percent: 100, message: 'Done' }),
|
progressCallback.bind(null, { percent: 100, message: 'Done' }),
|
||||||
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null, updateTime: new Date() })
|
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null, updateTime: new Date() })
|
||||||
], function seriesDone(error) {
|
], function seriesDone(error) {
|
||||||
@@ -899,7 +912,10 @@ function start(app, args, progressCallback, callback) {
|
|||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
async.series([
|
async.series([
|
||||||
progressCallback.bind(null, { percent: 20, message: 'Starting container' }),
|
progressCallback.bind(null, { percent: 10, message: 'Starting app services' }),
|
||||||
|
addons.startAppServices.bind(null, app),
|
||||||
|
|
||||||
|
progressCallback.bind(null, { percent: 35, message: 'Starting container' }),
|
||||||
docker.startContainer.bind(null, app.id),
|
docker.startContainer.bind(null, app.id),
|
||||||
|
|
||||||
// stopped apps do not renew certs. currently, we don't do DNS to not overwrite existing user settings
|
// stopped apps do not renew certs. currently, we don't do DNS to not overwrite existing user settings
|
||||||
@@ -927,6 +943,9 @@ function stop(app, args, progressCallback, callback) {
|
|||||||
progressCallback.bind(null, { percent: 20, message: 'Stopping container' }),
|
progressCallback.bind(null, { percent: 20, message: 'Stopping container' }),
|
||||||
docker.stopContainers.bind(null, app.id),
|
docker.stopContainers.bind(null, app.id),
|
||||||
|
|
||||||
|
progressCallback.bind(null, { percent: 50, message: 'Stopping app services' }),
|
||||||
|
addons.stopAppServices.bind(null, app),
|
||||||
|
|
||||||
progressCallback.bind(null, { percent: 100, message: 'Done' }),
|
progressCallback.bind(null, { percent: 100, message: 'Done' }),
|
||||||
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
|
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
|
||||||
], function seriesDone(error) {
|
], function seriesDone(error) {
|
||||||
@@ -973,16 +992,22 @@ function uninstall(app, args, progressCallback, callback) {
|
|||||||
progressCallback.bind(null, { percent: 30, message: 'Teardown addons' }),
|
progressCallback.bind(null, { percent: 30, message: 'Teardown addons' }),
|
||||||
addons.teardownAddons.bind(null, app, app.manifest.addons),
|
addons.teardownAddons.bind(null, app, app.manifest.addons),
|
||||||
|
|
||||||
progressCallback.bind(null, { percent: 40, message: 'Deleting app data directory' }),
|
progressCallback.bind(null, { percent: 40, message: 'Cleanup file manager' }),
|
||||||
|
function (callback) {
|
||||||
|
if (!app.dataDir) return callback();
|
||||||
|
sftp.rebuild({ ignoredApps: [ app.id ] }, callback);
|
||||||
|
},
|
||||||
|
|
||||||
|
progressCallback.bind(null, { percent: 50, message: 'Deleting app data directory' }),
|
||||||
deleteAppDir.bind(null, app, { removeDirectory: true }),
|
deleteAppDir.bind(null, app, { removeDirectory: true }),
|
||||||
|
|
||||||
progressCallback.bind(null, { percent: 50, message: 'Deleting image' }),
|
progressCallback.bind(null, { percent: 60, message: 'Deleting image' }),
|
||||||
docker.deleteImage.bind(null, app.manifest),
|
docker.deleteImage.bind(null, app.manifest),
|
||||||
|
|
||||||
progressCallback.bind(null, { percent: 60, message: 'Unregistering domains' }),
|
progressCallback.bind(null, { percent: 70, message: 'Unregistering domains' }),
|
||||||
unregisterSubdomains.bind(null, app, [ { subdomain: app.location, domain: app.domain } ].concat(app.alternateDomains)),
|
unregisterSubdomains.bind(null, app, [ { subdomain: app.location, domain: app.domain } ].concat(app.alternateDomains)),
|
||||||
|
|
||||||
progressCallback.bind(null, { percent: 70, message: 'Cleanup icon' }),
|
progressCallback.bind(null, { percent: 80, message: 'Cleanup icon' }),
|
||||||
removeIcon.bind(null, app),
|
removeIcon.bind(null, app),
|
||||||
|
|
||||||
progressCallback.bind(null, { percent: 90, message: 'Cleanup logs' }),
|
progressCallback.bind(null, { percent: 90, message: 'Cleanup logs' }),
|
||||||
@@ -1038,6 +1063,8 @@ function run(appId, args, progressCallback, callback) {
|
|||||||
return stop(app, args, progressCallback, callback);
|
return stop(app, args, progressCallback, callback);
|
||||||
case apps.ISTATE_PENDING_RESTART:
|
case apps.ISTATE_PENDING_RESTART:
|
||||||
return restart(app, args, progressCallback, callback);
|
return restart(app, args, progressCallback, callback);
|
||||||
|
case apps.ISTATE_INSTALLED: // can only happen when we have a bug in our code while testing/development
|
||||||
|
return updateApp(app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null }, callback);
|
||||||
default:
|
default:
|
||||||
debugApp(app, 'apptask launched with invalid command');
|
debugApp(app, 'apptask launched with invalid command');
|
||||||
return callback(new BoxError(BoxError.INTERNAL_ERROR, 'Unknown install command in apptask:' + app.installationState));
|
return callback(new BoxError(BoxError.INTERNAL_ERROR, 'Unknown install command in apptask:' + app.installationState));
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ function scheduleTask(appId, taskId, callback) {
|
|||||||
|
|
||||||
if (!fs.existsSync(path.dirname(logFile))) safe.fs.mkdirSync(path.dirname(logFile)); // ensure directory
|
if (!fs.existsSync(path.dirname(logFile))) safe.fs.mkdirSync(path.dirname(logFile)); // ensure directory
|
||||||
|
|
||||||
tasks.startTask(taskId, { logFile, timeout: 20 * 60 * 60 * 1000 /* 20 hours */ }, function (error, result) {
|
// TODO: set memory limit for app backup task
|
||||||
|
tasks.startTask(taskId, { logFile, timeout: 20 * 60 * 60 * 1000 /* 20 hours */, nice: 15 }, function (error, result) {
|
||||||
callback(error, result);
|
callback(error, result);
|
||||||
|
|
||||||
delete gActiveTasks[appId];
|
delete gActiveTasks[appId];
|
||||||
|
|||||||
+25
-30
@@ -6,27 +6,20 @@ var assert = require('assert'),
|
|||||||
safe = require('safetydance'),
|
safe = require('safetydance'),
|
||||||
util = require('util');
|
util = require('util');
|
||||||
|
|
||||||
var BACKUPS_FIELDS = [ 'id', 'creationTime', 'version', 'type', 'dependsOn', 'state', 'manifestJson', 'format', 'preserveSecs' ];
|
var BACKUPS_FIELDS = [ 'id', 'identifier', 'creationTime', 'packageVersion', 'type', 'dependsOn', 'state', 'manifestJson', 'format', 'preserveSecs', 'encryptionVersion' ];
|
||||||
|
|
||||||
exports = module.exports = {
|
exports = module.exports = {
|
||||||
add: add,
|
add,
|
||||||
|
|
||||||
getByTypeAndStatePaged: getByTypeAndStatePaged,
|
getByTypePaged,
|
||||||
getByTypePaged: getByTypePaged,
|
getByIdentifierPaged,
|
||||||
|
getByIdentifierAndStatePaged,
|
||||||
|
|
||||||
get: get,
|
get,
|
||||||
del: del,
|
del,
|
||||||
update: update,
|
update,
|
||||||
getByAppIdPaged: getByAppIdPaged,
|
|
||||||
|
|
||||||
_clear: clear,
|
_clear: clear
|
||||||
|
|
||||||
BACKUP_TYPE_APP: 'app',
|
|
||||||
BACKUP_TYPE_BOX: 'box',
|
|
||||||
|
|
||||||
BACKUP_STATE_NORMAL: 'normal', // should rename to created to avoid listing in UI?
|
|
||||||
BACKUP_STATE_CREATING: 'creating',
|
|
||||||
BACKUP_STATE_ERROR: 'error'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function postProcess(result) {
|
function postProcess(result) {
|
||||||
@@ -38,15 +31,15 @@ function postProcess(result) {
|
|||||||
delete result.manifestJson;
|
delete result.manifestJson;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getByTypeAndStatePaged(type, state, page, perPage, callback) {
|
function getByIdentifierAndStatePaged(identifier, state, page, perPage, callback) {
|
||||||
assert(type === exports.BACKUP_TYPE_APP || type === exports.BACKUP_TYPE_BOX);
|
assert.strictEqual(typeof identifier, 'string');
|
||||||
assert.strictEqual(typeof state, 'string');
|
assert.strictEqual(typeof state, 'string');
|
||||||
assert(typeof page === 'number' && page > 0);
|
assert(typeof page === 'number' && page > 0);
|
||||||
assert(typeof perPage === 'number' && perPage > 0);
|
assert(typeof perPage === 'number' && perPage > 0);
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE type = ? AND state = ? ORDER BY creationTime DESC LIMIT ?,?',
|
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE identifier = ? AND state = ? ORDER BY creationTime DESC LIMIT ?,?',
|
||||||
[ type, state, (page-1)*perPage, perPage ], function (error, results) {
|
[ identifier, state, (page-1)*perPage, perPage ], function (error, results) {
|
||||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||||
|
|
||||||
results.forEach(function (result) { postProcess(result); });
|
results.forEach(function (result) { postProcess(result); });
|
||||||
@@ -56,7 +49,7 @@ function getByTypeAndStatePaged(type, state, page, perPage, callback) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getByTypePaged(type, page, perPage, callback) {
|
function getByTypePaged(type, page, perPage, callback) {
|
||||||
assert(type === exports.BACKUP_TYPE_APP || type === exports.BACKUP_TYPE_BOX);
|
assert.strictEqual(typeof type, 'string');
|
||||||
assert(typeof page === 'number' && page > 0);
|
assert(typeof page === 'number' && page > 0);
|
||||||
assert(typeof perPage === 'number' && perPage > 0);
|
assert(typeof perPage === 'number' && perPage > 0);
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
@@ -71,15 +64,14 @@ function getByTypePaged(type, page, perPage, callback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getByAppIdPaged(page, perPage, appId, callback) {
|
function getByIdentifierPaged(identifier, page, perPage, callback) {
|
||||||
|
assert.strictEqual(typeof identifier, 'string');
|
||||||
assert(typeof page === 'number' && page > 0);
|
assert(typeof page === 'number' && page > 0);
|
||||||
assert(typeof perPage === 'number' && perPage > 0);
|
assert(typeof perPage === 'number' && perPage > 0);
|
||||||
assert.strictEqual(typeof appId, 'string');
|
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
// box versions (0.93.x and below) used to use appbackup_ prefix
|
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE identifier = ? ORDER BY creationTime DESC LIMIT ?,?',
|
||||||
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE type = ? AND state = ? AND id LIKE ? ORDER BY creationTime DESC LIMIT ?,?',
|
[ identifier, (page-1)*perPage, perPage ], function (error, results) {
|
||||||
[ exports.BACKUP_TYPE_APP, exports.BACKUP_STATE_NORMAL, '%app%\\_' + appId + '\\_%', (page-1)*perPage, perPage ], function (error, results) {
|
|
||||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||||
|
|
||||||
results.forEach(function (result) { postProcess(result); });
|
results.forEach(function (result) { postProcess(result); });
|
||||||
@@ -106,8 +98,11 @@ function get(id, callback) {
|
|||||||
function add(id, data, callback) {
|
function add(id, data, callback) {
|
||||||
assert(data && typeof data === 'object');
|
assert(data && typeof data === 'object');
|
||||||
assert.strictEqual(typeof id, 'string');
|
assert.strictEqual(typeof id, 'string');
|
||||||
assert.strictEqual(typeof data.version, 'string');
|
assert(data.encryptionVersion === null || typeof data.encryptionVersion === 'number');
|
||||||
assert(data.type === exports.BACKUP_TYPE_APP || data.type === exports.BACKUP_TYPE_BOX);
|
assert.strictEqual(typeof data.packageVersion, 'string');
|
||||||
|
assert.strictEqual(typeof data.type, 'string');
|
||||||
|
assert.strictEqual(typeof data.identifier, 'string');
|
||||||
|
assert.strictEqual(typeof data.state, 'string');
|
||||||
assert(util.isArray(data.dependsOn));
|
assert(util.isArray(data.dependsOn));
|
||||||
assert.strictEqual(typeof data.manifest, 'object');
|
assert.strictEqual(typeof data.manifest, 'object');
|
||||||
assert.strictEqual(typeof data.format, 'string');
|
assert.strictEqual(typeof data.format, 'string');
|
||||||
@@ -116,8 +111,8 @@ function add(id, data, callback) {
|
|||||||
var creationTime = data.creationTime || new Date(); // allow tests to set the time
|
var creationTime = data.creationTime || new Date(); // allow tests to set the time
|
||||||
var manifestJson = JSON.stringify(data.manifest);
|
var manifestJson = JSON.stringify(data.manifest);
|
||||||
|
|
||||||
database.query('INSERT INTO backups (id, version, type, creationTime, state, dependsOn, manifestJson, format) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
database.query('INSERT INTO backups (id, identifier, encryptionVersion, packageVersion, type, creationTime, state, dependsOn, manifestJson, format) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||||
[ id, data.version, data.type, creationTime, exports.BACKUP_STATE_NORMAL, data.dependsOn.join(','), manifestJson, data.format ],
|
[ id, data.identifier, data.encryptionVersion, data.packageVersion, data.type, creationTime, data.state, data.dependsOn.join(','), manifestJson, data.format ],
|
||||||
function (error) {
|
function (error) {
|
||||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS));
|
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS));
|
||||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||||
|
|||||||
+467
-247
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -332,7 +332,7 @@ Acme2.prototype.createKeyAndCsr = function (hostname, callback) {
|
|||||||
// in some old releases, csr file was corrupt. so always regenerate it
|
// in some old releases, csr file was corrupt. so always regenerate it
|
||||||
debug('createKeyAndCsr: reuse the key for renewal at %s', privateKeyFile);
|
debug('createKeyAndCsr: reuse the key for renewal at %s', privateKeyFile);
|
||||||
} else {
|
} else {
|
||||||
var key = safe.child_process.execSync('openssl genrsa 4096');
|
var key = safe.child_process.execSync('openssl ecparam -genkey -name secp384r1'); // openssl ecparam -list_curves
|
||||||
if (!key) return callback(new BoxError(BoxError.OPENSSL_ERROR, safe.error));
|
if (!key) return callback(new BoxError(BoxError.OPENSSL_ERROR, safe.error));
|
||||||
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new BoxError(BoxError.FS_ERROR, safe.error));
|
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new BoxError(BoxError.FS_ERROR, safe.error));
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
exports = module.exports = {
|
|
||||||
getCertificate: getCertificate,
|
|
||||||
|
|
||||||
// testing
|
|
||||||
_name: 'caas'
|
|
||||||
};
|
|
||||||
|
|
||||||
var assert = require('assert'),
|
|
||||||
debug = require('debug')('box:cert/caas.js');
|
|
||||||
|
|
||||||
function getCertificate(hostname, domain, options, callback) {
|
|
||||||
assert.strictEqual(typeof hostname, 'string');
|
|
||||||
assert.strictEqual(typeof domain, 'string');
|
|
||||||
assert.strictEqual(typeof options, 'object');
|
|
||||||
assert.strictEqual(typeof callback, 'function');
|
|
||||||
|
|
||||||
debug('getCertificate: using fallback certificate', hostname);
|
|
||||||
|
|
||||||
return callback(null, '', '');
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
exports = module.exports = {
|
|
||||||
getCertificate: getCertificate,
|
|
||||||
|
|
||||||
// testing
|
|
||||||
_name: 'fallback'
|
|
||||||
};
|
|
||||||
|
|
||||||
var assert = require('assert'),
|
|
||||||
debug = require('debug')('box:cert/fallback.js');
|
|
||||||
|
|
||||||
function getCertificate(hostname, domain, options, callback) {
|
|
||||||
assert.strictEqual(typeof hostname, 'string');
|
|
||||||
assert.strictEqual(typeof domain, 'string');
|
|
||||||
assert.strictEqual(typeof options, 'object');
|
|
||||||
assert.strictEqual(typeof callback, 'function');
|
|
||||||
|
|
||||||
debug('getCertificate: using fallback certificate', hostname);
|
|
||||||
|
|
||||||
return callback(null, '', '');
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
// -------------------------------------------
|
|
||||||
// This file just describes the interface
|
|
||||||
//
|
|
||||||
// New backends can start from here
|
|
||||||
// -------------------------------------------
|
|
||||||
|
|
||||||
exports = module.exports = {
|
|
||||||
getCertificate: getCertificate
|
|
||||||
};
|
|
||||||
|
|
||||||
var assert = require('assert'),
|
|
||||||
BoxError = require('../boxerror.js');
|
|
||||||
|
|
||||||
function getCertificate(hostname, domain, options, callback) {
|
|
||||||
assert.strictEqual(typeof hostname, 'string');
|
|
||||||
assert.strictEqual(typeof domain, 'string');
|
|
||||||
assert.strictEqual(typeof options, 'object');
|
|
||||||
assert.strictEqual(typeof callback, 'function');
|
|
||||||
|
|
||||||
return callback(new BoxError(BoxError.NOT_IMPLEMENTED, 'getCertificate is not implemented'));
|
|
||||||
}
|
|
||||||
|
|
||||||
+17
-20
@@ -18,7 +18,7 @@ exports = module.exports = {
|
|||||||
|
|
||||||
setupDashboard: setupDashboard,
|
setupDashboard: setupDashboard,
|
||||||
|
|
||||||
runSystemChecks: runSystemChecks,
|
runSystemChecks: runSystemChecks
|
||||||
};
|
};
|
||||||
|
|
||||||
var addons = require('./addons.js'),
|
var addons = require('./addons.js'),
|
||||||
@@ -66,7 +66,7 @@ function uninitialize(callback) {
|
|||||||
|
|
||||||
async.series([
|
async.series([
|
||||||
cron.stopJobs,
|
cron.stopJobs,
|
||||||
platform.stop
|
platform.stopAllTasks
|
||||||
], callback);
|
], callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +78,13 @@ function onActivated(callback) {
|
|||||||
// 2. the restore code path can run without sudo (since mail/ is non-root)
|
// 2. the restore code path can run without sudo (since mail/ is non-root)
|
||||||
async.series([
|
async.series([
|
||||||
platform.start,
|
platform.start,
|
||||||
cron.startJobs
|
cron.startJobs,
|
||||||
|
function checkBackupConfiguration(callback) {
|
||||||
|
backups.checkConfiguration(function (error, message) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
notifications.alert(notifications.ALERT_BACKUP_CONFIG, 'Backup configuration is unsafe', message, callback);
|
||||||
|
});
|
||||||
|
}
|
||||||
], callback);
|
], callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,12 +109,15 @@ function notifyUpdate(callback) {
|
|||||||
|
|
||||||
// each of these tasks can fail. we will add some routes to fix/re-run them
|
// each of these tasks can fail. we will add some routes to fix/re-run them
|
||||||
function runStartupTasks() {
|
function runStartupTasks() {
|
||||||
|
// stop all the systemd tasks
|
||||||
|
platform.stopAllTasks(NOOP_CALLBACK);
|
||||||
|
|
||||||
// configure nginx to be reachable by IP
|
// configure nginx to be reachable by IP
|
||||||
reverseProxy.writeDefaultConfig(NOOP_CALLBACK);
|
reverseProxy.writeDefaultConfig(NOOP_CALLBACK);
|
||||||
|
|
||||||
// this configures collectd to collect backup storage metrics if filesystem is used. This is also triggerd when the settings change with the rest api
|
// this configures collectd to collect backup storage metrics if filesystem is used. This is also triggerd when the settings change with the rest api
|
||||||
settings.getBackupConfig(function (error, backupConfig) {
|
settings.getBackupConfig(function (error, backupConfig) {
|
||||||
if (error) return console.error('Failed to read backup config.', error);
|
if (error) return debug('runStartupTasks: failed to get backup config.', error);
|
||||||
backups.configureCollectd(backupConfig, NOOP_CALLBACK);
|
backups.configureCollectd(backupConfig, NOOP_CALLBACK);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -139,17 +148,18 @@ function getConfig(callback) {
|
|||||||
mailFqdn: settings.mailFqdn(),
|
mailFqdn: settings.mailFqdn(),
|
||||||
version: constants.VERSION,
|
version: constants.VERSION,
|
||||||
isDemo: settings.isDemo(),
|
isDemo: settings.isDemo(),
|
||||||
provider: settings.provider(),
|
|
||||||
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
|
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
|
||||||
footer: allSettings[settings.FOOTER_KEY] || constants.FOOTER,
|
footer: allSettings[settings.FOOTER_KEY] || constants.FOOTER,
|
||||||
features: appstore.getFeatures()
|
features: appstore.getFeatures(),
|
||||||
|
profileLocked: allSettings[settings.DIRECTORY_CONFIG_KEY].lockUserProfiles,
|
||||||
|
mandatory2FA: allSettings[settings.DIRECTORY_CONFIG_KEY].mandatory2FA
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function reboot(callback) {
|
function reboot(callback) {
|
||||||
notifications.alert(notifications.ALERT_REBOOT, 'Reboot Required', '', function (error) {
|
notifications.alert(notifications.ALERT_REBOOT, 'Reboot Required', '', function (error) {
|
||||||
if (error) console.error('Failed to clear reboot notification.', error);
|
if (error) debug('reboot: failed to clear reboot notification.', error);
|
||||||
|
|
||||||
shell.sudo('reboot', [ REBOOT_CMD ], {}, callback);
|
shell.sudo('reboot', [ REBOOT_CMD ], {}, callback);
|
||||||
});
|
});
|
||||||
@@ -167,24 +177,11 @@ function runSystemChecks(callback) {
|
|||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
async.parallel([
|
async.parallel([
|
||||||
checkBackupConfiguration,
|
|
||||||
checkMailStatus,
|
checkMailStatus,
|
||||||
checkRebootRequired
|
checkRebootRequired
|
||||||
], callback);
|
], callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkBackupConfiguration(callback) {
|
|
||||||
assert.strictEqual(typeof callback, 'function');
|
|
||||||
|
|
||||||
debug('checking backup configuration');
|
|
||||||
|
|
||||||
backups.checkConfiguration(function (error, message) {
|
|
||||||
if (error) return callback(error);
|
|
||||||
|
|
||||||
notifications.alert(notifications.ALERT_BACKUP_CONFIG, 'Backup configuration is unsafe', message, callback);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkMailStatus(callback) {
|
function checkMailStatus(callback) {
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -37,10 +37,11 @@ exports = module.exports = {
|
|||||||
DEFAULT_MEMORY_LIMIT: (256 * 1024 * 1024), // see also client.js
|
DEFAULT_MEMORY_LIMIT: (256 * 1024 * 1024), // see also client.js
|
||||||
|
|
||||||
DEMO_USERNAME: 'cloudron',
|
DEMO_USERNAME: 'cloudron',
|
||||||
|
DEMO_BLACKLISTED_APPS: [ 'com.github.cloudtorrent' ],
|
||||||
|
|
||||||
AUTOUPDATE_PATTERN_NEVER: 'never',
|
AUTOUPDATE_PATTERN_NEVER: 'never',
|
||||||
|
|
||||||
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8),
|
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8), // also used in dashboard client.js
|
||||||
|
|
||||||
CLOUDRON: CLOUDRON,
|
CLOUDRON: CLOUDRON,
|
||||||
TEST: TEST,
|
TEST: TEST,
|
||||||
|
|||||||
+23
-26
@@ -1,16 +1,25 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
// IMPORTANT: These patterns are together because they spin tasks which acquire a lock
|
||||||
|
// If the patterns overlap all the time, then the task may not ever get a chance to run!
|
||||||
|
// If you change this change dashboard patterns in settings.html
|
||||||
|
const DEFAULT_CLEANUP_BACKUPS_PATTERN = '00 30 1,3,5,23 * * *',
|
||||||
|
DEFAULT_BOX_AUTOUPDATE_PATTERN = '00 00 1,3,5,23 * * *',
|
||||||
|
DEFAULT_APP_AUTOUPDATE_PATTERN = '00 15 1,3,5,23 * * *';
|
||||||
|
|
||||||
exports = module.exports = {
|
exports = module.exports = {
|
||||||
startJobs: startJobs,
|
startJobs,
|
||||||
|
|
||||||
stopJobs: stopJobs,
|
stopJobs,
|
||||||
|
|
||||||
handleSettingsChanged: handleSettingsChanged
|
handleSettingsChanged,
|
||||||
|
|
||||||
|
DEFAULT_BOX_AUTOUPDATE_PATTERN,
|
||||||
|
DEFAULT_APP_AUTOUPDATE_PATTERN
|
||||||
};
|
};
|
||||||
|
|
||||||
var appHealthMonitor = require('./apphealthmonitor.js'),
|
var appHealthMonitor = require('./apphealthmonitor.js'),
|
||||||
apps = require('./apps.js'),
|
apps = require('./apps.js'),
|
||||||
appstore = require('./appstore.js'),
|
|
||||||
assert = require('assert'),
|
assert = require('assert'),
|
||||||
async = require('async'),
|
async = require('async'),
|
||||||
auditSource = require('./auditsource.js'),
|
auditSource = require('./auditsource.js'),
|
||||||
@@ -29,7 +38,6 @@ var appHealthMonitor = require('./apphealthmonitor.js'),
|
|||||||
updateChecker = require('./updatechecker.js');
|
updateChecker = require('./updatechecker.js');
|
||||||
|
|
||||||
var gJobs = {
|
var gJobs = {
|
||||||
alive: null, // send periodic stats
|
|
||||||
appAutoUpdater: null,
|
appAutoUpdater: null,
|
||||||
boxAutoUpdater: null,
|
boxAutoUpdater: null,
|
||||||
appUpdateChecker: null,
|
appUpdateChecker: null,
|
||||||
@@ -61,14 +69,8 @@ function startJobs(callback) {
|
|||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
const randomMinute = Math.floor(60*Math.random());
|
const randomMinute = Math.floor(60*Math.random());
|
||||||
gJobs.alive = new CronJob({
|
|
||||||
cronTime: '00 ' + randomMinute + ' * * * *', // every hour on a random minute
|
|
||||||
onTick: appstore.sendAliveStatus,
|
|
||||||
start: true
|
|
||||||
});
|
|
||||||
|
|
||||||
gJobs.systemChecks = new CronJob({
|
gJobs.systemChecks = new CronJob({
|
||||||
cronTime: '00 30 * * * *', // every 30 minutes. if you change this interval, change the notification messages with correct duration
|
cronTime: '00 30 2 * * *', // once a day. if you change this interval, change the notification messages with correct duration
|
||||||
onTick: () => cloudron.runSystemChecks(NOOP_CALLBACK),
|
onTick: () => cloudron.runSystemChecks(NOOP_CALLBACK),
|
||||||
start: true
|
start: true
|
||||||
});
|
});
|
||||||
@@ -80,14 +82,15 @@ function startJobs(callback) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
gJobs.boxUpdateCheckerJob = new CronJob({
|
gJobs.boxUpdateCheckerJob = new CronJob({
|
||||||
cronTime: '00 ' + randomMinute + ' * * * *', // once an hour
|
cronTime: '00 ' + randomMinute + ' 1,3,5,21,23 * * *', // 5 times
|
||||||
onTick: () => updateChecker.checkBoxUpdates(NOOP_CALLBACK),
|
onTick: () => updateChecker.checkBoxUpdates({ automatic: true }, NOOP_CALLBACK),
|
||||||
start: true
|
start: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// this is run separately from the update itself so that the user can disable automatic updates but can still get a notification
|
||||||
gJobs.appUpdateChecker = new CronJob({
|
gJobs.appUpdateChecker = new CronJob({
|
||||||
cronTime: '00 ' + randomMinute + ' * * * *', // once an hour
|
cronTime: '00 ' + randomMinute + ' 2,4,6,20,22 * * *', // 5 times
|
||||||
onTick: () => updateChecker.checkAppUpdates(NOOP_CALLBACK),
|
onTick: () => updateChecker.checkAppUpdates({ automatic: true }, NOOP_CALLBACK),
|
||||||
start: true
|
start: true
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -98,7 +101,7 @@ function startJobs(callback) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
gJobs.cleanupBackups = new CronJob({
|
gJobs.cleanupBackups = new CronJob({
|
||||||
cronTime: '00 45 1,3,5,23 * * *', // every 6 hours. try not to overlap with ensureBackup job
|
cronTime: DEFAULT_CLEANUP_BACKUPS_PATTERN,
|
||||||
onTick: backups.startCleanupTask.bind(null, auditSource.CRON, NOOP_CALLBACK),
|
onTick: backups.startCleanupTask.bind(null, auditSource.CRON, NOOP_CALLBACK),
|
||||||
start: true
|
start: true
|
||||||
});
|
});
|
||||||
@@ -172,19 +175,13 @@ function backupConfigChanged(value, tz) {
|
|||||||
assert.strictEqual(typeof value, 'object');
|
assert.strictEqual(typeof value, 'object');
|
||||||
assert.strictEqual(typeof tz, 'string');
|
assert.strictEqual(typeof tz, 'string');
|
||||||
|
|
||||||
debug(`backupConfigChanged: interval ${value.intervalSecs} (${tz})`);
|
debug(`backupConfigChanged: schedule ${value.schedulePattern} (${tz})`);
|
||||||
|
|
||||||
if (gJobs.backup) gJobs.backup.stop();
|
if (gJobs.backup) gJobs.backup.stop();
|
||||||
let pattern;
|
|
||||||
if (value.intervalSecs <= 6 * 60 * 60) {
|
|
||||||
pattern = '00 00 1,7,13,19 * * *'; // no option but to backup in the middle of the day
|
|
||||||
} else {
|
|
||||||
pattern = '00 00 1,3,5,23 * * *'; // avoid middle of the day backups
|
|
||||||
}
|
|
||||||
|
|
||||||
gJobs.backup = new CronJob({
|
gJobs.backup = new CronJob({
|
||||||
cronTime: pattern,
|
cronTime: value.schedulePattern,
|
||||||
onTick: backups.ensureBackup.bind(null, auditSource.CRON, NOOP_CALLBACK),
|
onTick: backups.startBackupTask.bind(null, auditSource.CRON, NOOP_CALLBACK),
|
||||||
start: true,
|
start: true,
|
||||||
timeZone: tz
|
timeZone: tz
|
||||||
});
|
});
|
||||||
|
|||||||
+38
-97
@@ -17,12 +17,12 @@ var assert = require('assert'),
|
|||||||
BoxError = require('./boxerror.js'),
|
BoxError = require('./boxerror.js'),
|
||||||
child_process = require('child_process'),
|
child_process = require('child_process'),
|
||||||
constants = require('./constants.js'),
|
constants = require('./constants.js'),
|
||||||
|
debug = require('debug')('box:database'),
|
||||||
mysql = require('mysql'),
|
mysql = require('mysql'),
|
||||||
once = require('once'),
|
once = require('once'),
|
||||||
util = require('util');
|
util = require('util');
|
||||||
|
|
||||||
var gConnectionPool = null,
|
var gConnectionPool = null;
|
||||||
gDefaultConnection = null;
|
|
||||||
|
|
||||||
const gDatabase = {
|
const gDatabase = {
|
||||||
hostname: '127.0.0.1',
|
hostname: '127.0.0.1',
|
||||||
@@ -42,59 +42,37 @@ function initialize(callback) {
|
|||||||
gDatabase.hostname = require('child_process').execSync('docker inspect -f "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" mysql-server').toString().trim();
|
gDatabase.hostname = require('child_process').execSync('docker inspect -f "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" mysql-server').toString().trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://github.com/mysqljs/mysql#pool-options
|
||||||
gConnectionPool = mysql.createPool({
|
gConnectionPool = mysql.createPool({
|
||||||
connectionLimit: 5, // this has to be > 1 since we store one connection as 'default'. the rest for transactions
|
connectionLimit: 5,
|
||||||
host: gDatabase.hostname,
|
host: gDatabase.hostname,
|
||||||
user: gDatabase.username,
|
user: gDatabase.username,
|
||||||
password: gDatabase.password,
|
password: gDatabase.password,
|
||||||
port: gDatabase.port,
|
port: gDatabase.port,
|
||||||
database: gDatabase.name,
|
database: gDatabase.name,
|
||||||
multipleStatements: false,
|
multipleStatements: false,
|
||||||
|
waitForConnections: true, // getConnection() will wait until a connection is avaiable
|
||||||
ssl: false,
|
ssl: false,
|
||||||
timezone: 'Z' // mysql follows the SYSTEM timezone. on Cloudron, this is UTC
|
timezone: 'Z' // mysql follows the SYSTEM timezone. on Cloudron, this is UTC
|
||||||
});
|
});
|
||||||
|
|
||||||
gConnectionPool.on('connection', function (connection) {
|
gConnectionPool.on('connection', function (connection) {
|
||||||
|
// connection objects are re-used. so we have to attach to the event here (once) to prevent crash
|
||||||
|
// note the pool also has an 'acquire' event but that is called whenever we do a getConnection()
|
||||||
|
connection.on('error', (error) => debug(`Connection ${connection.threadId} error: ${error.message} ${error.code}`));
|
||||||
|
|
||||||
connection.query('USE ' + gDatabase.name);
|
connection.query('USE ' + gDatabase.name);
|
||||||
connection.query('SET SESSION sql_mode = \'strict_all_tables\'');
|
connection.query('SET SESSION sql_mode = \'strict_all_tables\'');
|
||||||
});
|
});
|
||||||
|
|
||||||
reconnect(callback);
|
callback(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function uninitialize(callback) {
|
function uninitialize(callback) {
|
||||||
if (gConnectionPool) {
|
if (!gConnectionPool) return callback(null);
|
||||||
gConnectionPool.end(callback);
|
|
||||||
gConnectionPool = null;
|
|
||||||
} else {
|
|
||||||
callback(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function reconnect(callback) {
|
gConnectionPool.end(callback);
|
||||||
callback = callback ? once(callback) : function () {};
|
gConnectionPool = null;
|
||||||
|
|
||||||
gConnectionPool.getConnection(function (error, connection) {
|
|
||||||
if (error) {
|
|
||||||
console.error('Unable to reestablish connection to database. Try again in a bit.', error.message);
|
|
||||||
return setTimeout(reconnect.bind(null, callback), 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
connection.on('error', function (error) {
|
|
||||||
// by design, we catch all normal errors by providing callbacks.
|
|
||||||
// this function should be invoked only when we have no callbacks pending and we have a fatal error
|
|
||||||
assert(error.fatal, 'Non-fatal error on connection object');
|
|
||||||
|
|
||||||
console.error('Unhandled mysql connection error.', error);
|
|
||||||
|
|
||||||
// This is most likely an issue an can cause double callbacks from reconnect()
|
|
||||||
setTimeout(reconnect.bind(null, callback), 1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
gDefaultConnection = connection;
|
|
||||||
|
|
||||||
callback(null);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function clear(callback) {
|
function clear(callback) {
|
||||||
@@ -107,80 +85,43 @@ function clear(callback) {
|
|||||||
child_process.exec(cmd, callback);
|
child_process.exec(cmd, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
function beginTransaction(callback) {
|
|
||||||
assert.strictEqual(typeof callback, 'function');
|
|
||||||
|
|
||||||
if (gConnectionPool === null) return callback(new BoxError(BoxError.DATABASE_ERROR, 'No database connection pool.'));
|
|
||||||
|
|
||||||
gConnectionPool.getConnection(function (error, connection) {
|
|
||||||
if (error) {
|
|
||||||
console.error('Unable to get connection to database. Try again in a bit.', error.message);
|
|
||||||
return setTimeout(beginTransaction.bind(null, callback), 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
connection.beginTransaction(function (error) {
|
|
||||||
if (error) return callback(error);
|
|
||||||
|
|
||||||
return callback(null, connection);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function rollback(connection, callback) {
|
|
||||||
assert.strictEqual(typeof callback, 'function');
|
|
||||||
|
|
||||||
connection.rollback(function (error) {
|
|
||||||
if (error) console.error(error); // can this happen?
|
|
||||||
|
|
||||||
connection.release();
|
|
||||||
callback(null);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: if commit fails, is it supposed to return an error ?
|
|
||||||
function commit(connection, callback) {
|
|
||||||
assert.strictEqual(typeof callback, 'function');
|
|
||||||
|
|
||||||
connection.commit(function (error) {
|
|
||||||
if (error) return rollback(connection, callback);
|
|
||||||
|
|
||||||
connection.release();
|
|
||||||
return callback(null);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function query() {
|
function query() {
|
||||||
var args = Array.prototype.slice.call(arguments);
|
const args = Array.prototype.slice.call(arguments);
|
||||||
var callback = args[args.length - 1];
|
const callback = args[args.length - 1];
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
if (gDefaultConnection === null) return callback(new BoxError(BoxError.DATABASE_ERROR, 'No connection to database'));
|
if (constants.TEST && !gConnectionPool) return callback(new BoxError(BoxError.DATABASE_ERROR, 'database.js not initialized'));
|
||||||
|
|
||||||
args[args.length -1 ] = function (error, result) {
|
gConnectionPool.query.apply(gConnectionPool, args); // this is same as getConnection/query/release
|
||||||
if (error && error.fatal) {
|
|
||||||
gDefaultConnection = null;
|
|
||||||
setTimeout(reconnect, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(error, result);
|
|
||||||
};
|
|
||||||
|
|
||||||
gDefaultConnection.query.apply(gDefaultConnection, args);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function transaction(queries, callback) {
|
function transaction(queries, callback) {
|
||||||
assert(util.isArray(queries));
|
assert(util.isArray(queries));
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
beginTransaction(function (error, conn) {
|
callback = once(callback);
|
||||||
|
|
||||||
|
gConnectionPool.getConnection(function (error, connection) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
async.mapSeries(queries, function iterator(query, done) {
|
const releaseConnection = (error) => { connection.release(); callback(error); };
|
||||||
conn.query(query.query, query.args, done);
|
|
||||||
}, function seriesDone(error, results) {
|
|
||||||
if (error) return rollback(conn, callback.bind(null, error));
|
|
||||||
|
|
||||||
commit(conn, callback.bind(null, null, results));
|
connection.beginTransaction(function (error) {
|
||||||
|
if (error) return releaseConnection(error);
|
||||||
|
|
||||||
|
async.mapSeries(queries, function iterator(query, done) {
|
||||||
|
connection.query(query.query, query.args, done);
|
||||||
|
}, function seriesDone(error, results) {
|
||||||
|
if (error) return connection.rollback(() => releaseConnection(error));
|
||||||
|
|
||||||
|
connection.commit(function (error) {
|
||||||
|
if (error) return connection.rollback(() => releaseConnection(error));
|
||||||
|
|
||||||
|
connection.release();
|
||||||
|
|
||||||
|
callback(null, results);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -203,7 +144,7 @@ function exportToFile(file, callback) {
|
|||||||
|
|
||||||
// latest mysqldump enables column stats by default which is not present in MySQL 5.7 server
|
// latest mysqldump enables column stats by default which is not present in MySQL 5.7 server
|
||||||
// this option must not be set in production cloudrons which still use the old mysqldump
|
// this option must not be set in production cloudrons which still use the old mysqldump
|
||||||
const disableColStats = (constants.TEST && process.env.DESKTOP_SESSION !== 'ubuntu') ? '--column-statistics=0' : '';
|
const disableColStats = (constants.TEST && require('fs').readFileSync('/etc/lsb-release', 'utf-8').includes('20.04')) ? '--column-statistics=0' : '';
|
||||||
|
|
||||||
var cmd = `/usr/bin/mysqldump -h "${gDatabase.hostname}" -u root -p${gDatabase.password} ${disableColStats} --single-transaction --routines --triggers ${gDatabase.name} > "${file}"`;
|
var cmd = `/usr/bin/mysqldump -h "${gDatabase.hostname}" -u root -p${gDatabase.password} ${disableColStats} --single-transaction --routines --triggers ${gDatabase.name} > "${file}"`;
|
||||||
|
|
||||||
|
|||||||
-176
@@ -1,176 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
exports = module.exports = {
|
|
||||||
removePrivateFields: removePrivateFields,
|
|
||||||
injectPrivateFields: injectPrivateFields,
|
|
||||||
upsert: upsert,
|
|
||||||
get: get,
|
|
||||||
del: del,
|
|
||||||
wait: wait,
|
|
||||||
verifyDnsConfig: verifyDnsConfig
|
|
||||||
};
|
|
||||||
|
|
||||||
var assert = require('assert'),
|
|
||||||
BoxError = require('../boxerror.js'),
|
|
||||||
debug = require('debug')('box:dns/caas'),
|
|
||||||
domains = require('../domains.js'),
|
|
||||||
settings = require('../settings.js'),
|
|
||||||
superagent = require('superagent'),
|
|
||||||
util = require('util'),
|
|
||||||
waitForDns = require('./waitfordns.js');
|
|
||||||
|
|
||||||
function formatError(response) {
|
|
||||||
return util.format('Caas DNS error [%s] %j', response.statusCode, response.body);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFqdn(location, domain) {
|
|
||||||
assert.strictEqual(typeof location, 'string');
|
|
||||||
assert.strictEqual(typeof domain, 'string');
|
|
||||||
|
|
||||||
return (location === '') ? domain : location + '-' + domain;
|
|
||||||
}
|
|
||||||
|
|
||||||
function removePrivateFields(domainObject) {
|
|
||||||
domainObject.config.token = domains.SECRET_PLACEHOLDER;
|
|
||||||
|
|
||||||
// do not return the 'key'. in caas, this is private
|
|
||||||
delete domainObject.fallbackCertificate.key;
|
|
||||||
|
|
||||||
return domainObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
function injectPrivateFields(newConfig, currentConfig) {
|
|
||||||
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
|
||||||
}
|
|
||||||
|
|
||||||
function upsert(domainObject, location, type, values, callback) {
|
|
||||||
assert.strictEqual(typeof domainObject, 'object');
|
|
||||||
assert.strictEqual(typeof location, 'string');
|
|
||||||
assert.strictEqual(typeof type, 'string');
|
|
||||||
assert(util.isArray(values));
|
|
||||||
assert.strictEqual(typeof callback, 'function');
|
|
||||||
|
|
||||||
const dnsConfig = domainObject.config;
|
|
||||||
|
|
||||||
let fqdn = location !== '' && type === 'TXT' ? location + '.' + domainObject.domain : getFqdn(location, domainObject.domain);
|
|
||||||
|
|
||||||
debug('add: %s for zone %s of type %s with values %j', location, domainObject.domain, type, values);
|
|
||||||
|
|
||||||
var data = {
|
|
||||||
type: type,
|
|
||||||
values: values
|
|
||||||
};
|
|
||||||
|
|
||||||
superagent
|
|
||||||
.post(settings.apiServerOrigin() + '/api/v1/caas/domains/' + fqdn)
|
|
||||||
.query({ token: dnsConfig.token })
|
|
||||||
.send(data)
|
|
||||||
.timeout(30 * 1000)
|
|
||||||
.end(function (error, result) {
|
|
||||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
|
||||||
if (result.statusCode === 400) return callback(new BoxError(BoxError.BAD_FIELD, result.body.message));
|
|
||||||
if (result.statusCode === 420) return callback(new BoxError(BoxError.BUSY));
|
|
||||||
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
|
|
||||||
|
|
||||||
return callback(null);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function get(domainObject, location, type, callback) {
|
|
||||||
assert.strictEqual(typeof domainObject, 'object');
|
|
||||||
assert.strictEqual(typeof location, 'string');
|
|
||||||
assert.strictEqual(typeof type, 'string');
|
|
||||||
assert.strictEqual(typeof callback, 'function');
|
|
||||||
|
|
||||||
const dnsConfig = domainObject.config;
|
|
||||||
const fqdn = location !== '' && type === 'TXT' ? location + '.' + domainObject.domain : getFqdn(location, domainObject.domain);
|
|
||||||
|
|
||||||
debug('get: zoneName: %s subdomain: %s type: %s fqdn: %s', domainObject.domain, location, type, fqdn);
|
|
||||||
|
|
||||||
superagent
|
|
||||||
.get(settings.apiServerOrigin() + '/api/v1/caas/domains/' + fqdn)
|
|
||||||
.query({ token: dnsConfig.token, type: type })
|
|
||||||
.timeout(30 * 1000)
|
|
||||||
.end(function (error, result) {
|
|
||||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
|
||||||
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
|
|
||||||
|
|
||||||
return callback(null, result.body.values);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function del(domainObject, location, type, values, callback) {
|
|
||||||
assert.strictEqual(typeof domainObject, 'object');
|
|
||||||
assert.strictEqual(typeof location, 'string');
|
|
||||||
assert.strictEqual(typeof type, 'string');
|
|
||||||
assert(util.isArray(values));
|
|
||||||
assert.strictEqual(typeof callback, 'function');
|
|
||||||
|
|
||||||
const dnsConfig = domainObject.config;
|
|
||||||
debug('del: %s for zone %s of type %s with values %j', location, domainObject.domain, type, values);
|
|
||||||
|
|
||||||
var data = {
|
|
||||||
type: type,
|
|
||||||
values: values
|
|
||||||
};
|
|
||||||
|
|
||||||
superagent
|
|
||||||
.del(settings.apiServerOrigin() + '/api/v1/caas/domains/' + getFqdn(location, domainObject.domain))
|
|
||||||
.query({ token: dnsConfig.token })
|
|
||||||
.send(data)
|
|
||||||
.timeout(30 * 1000)
|
|
||||||
.end(function (error, result) {
|
|
||||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
|
||||||
if (result.statusCode === 400) return callback(new BoxError(BoxError.BAD_FIELD, result.body.message));
|
|
||||||
if (result.statusCode === 420) return callback(new BoxError(BoxError.BUSY));
|
|
||||||
if (result.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
|
|
||||||
if (result.statusCode !== 204) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
|
|
||||||
|
|
||||||
return callback(null);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function wait(domainObject, location, type, value, options, callback) {
|
|
||||||
assert.strictEqual(typeof domainObject, 'object');
|
|
||||||
assert.strictEqual(typeof location, 'string');
|
|
||||||
assert.strictEqual(typeof type, 'string');
|
|
||||||
assert.strictEqual(typeof value, 'string');
|
|
||||||
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
|
|
||||||
assert.strictEqual(typeof callback, 'function');
|
|
||||||
|
|
||||||
const fqdn = domains.fqdn(location, domainObject);
|
|
||||||
|
|
||||||
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
function verifyDnsConfig(domainObject, callback) {
|
|
||||||
assert.strictEqual(typeof domainObject, 'object');
|
|
||||||
assert.strictEqual(typeof callback, 'function');
|
|
||||||
|
|
||||||
const dnsConfig = domainObject.config;
|
|
||||||
|
|
||||||
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'token must be a non-empty string', { field: 'token' }));
|
|
||||||
|
|
||||||
const ip = '127.0.0.1';
|
|
||||||
|
|
||||||
var credentials = {
|
|
||||||
token: dnsConfig.token,
|
|
||||||
hyphenatedSubdomains: true // this will ensure we always use them, regardless of passed-in configs
|
|
||||||
};
|
|
||||||
|
|
||||||
const location = 'cloudrontestdns';
|
|
||||||
|
|
||||||
upsert(domainObject, location, 'A', [ ip ], function (error) {
|
|
||||||
if (error) return callback(error);
|
|
||||||
|
|
||||||
debug('verifyDnsConfig: Test A record added');
|
|
||||||
|
|
||||||
del(domainObject, location, 'A', [ ip ], function (error) {
|
|
||||||
if (error) return callback(error);
|
|
||||||
|
|
||||||
debug('verifyDnsConfig: Test A record removed again');
|
|
||||||
|
|
||||||
callback(null, credentials);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
+12
-6
@@ -13,6 +13,7 @@ exports = module.exports = {
|
|||||||
var assert = require('assert'),
|
var assert = require('assert'),
|
||||||
async = require('async'),
|
async = require('async'),
|
||||||
BoxError = require('../boxerror.js'),
|
BoxError = require('../boxerror.js'),
|
||||||
|
constants = require('../constants.js'),
|
||||||
debug = require('debug')('box:dns/cloudflare'),
|
debug = require('debug')('box:dns/cloudflare'),
|
||||||
dns = require('../native-dns.js'),
|
dns = require('../native-dns.js'),
|
||||||
domains = require('../domains.js'),
|
domains = require('../domains.js'),
|
||||||
@@ -25,12 +26,12 @@ var assert = require('assert'),
|
|||||||
var CLOUDFLARE_ENDPOINT = 'https://api.cloudflare.com/client/v4';
|
var CLOUDFLARE_ENDPOINT = 'https://api.cloudflare.com/client/v4';
|
||||||
|
|
||||||
function removePrivateFields(domainObject) {
|
function removePrivateFields(domainObject) {
|
||||||
domainObject.config.token = domains.SECRET_PLACEHOLDER;
|
domainObject.config.token = constants.SECRET_PLACEHOLDER;
|
||||||
return domainObject;
|
return domainObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
function injectPrivateFields(newConfig, currentConfig) {
|
function injectPrivateFields(newConfig, currentConfig) {
|
||||||
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
||||||
}
|
}
|
||||||
|
|
||||||
function translateRequestError(result, callback) {
|
function translateRequestError(result, callback) {
|
||||||
@@ -39,9 +40,14 @@ function translateRequestError(result, callback) {
|
|||||||
|
|
||||||
if (result.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND, util.format('%s %j', result.statusCode, 'API does not exist')));
|
if (result.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND, util.format('%s %j', result.statusCode, 'API does not exist')));
|
||||||
if (result.statusCode === 422) return callback(new BoxError(BoxError.BAD_FIELD, result.body.message));
|
if (result.statusCode === 422) return callback(new BoxError(BoxError.BAD_FIELD, result.body.message));
|
||||||
if ((result.statusCode === 400 || result.statusCode === 401 || result.statusCode === 403) && result.body.errors.length > 0) {
|
if (result.statusCode === 400 || result.statusCode === 401 || result.statusCode === 403) {
|
||||||
let error = result.body.errors[0];
|
let message = 'Unknown error';
|
||||||
let message = `message: ${error.message} statusCode: ${result.statusCode} code:${error.code}`;
|
if (typeof result.body.error === 'string') {
|
||||||
|
message = `message: ${result.body.error} statusCode: ${result.statusCode}`;
|
||||||
|
} else if (Array.isArray(result.body.errors) && result.body.errors.length > 0) {
|
||||||
|
let error = result.body.errors[0];
|
||||||
|
message = `message: ${error.message} statusCode: ${result.statusCode} code:${error.code}`;
|
||||||
|
}
|
||||||
return callback(new BoxError(BoxError.ACCESS_DENIED, message));
|
return callback(new BoxError(BoxError.ACCESS_DENIED, message));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,7 +290,7 @@ function verifyDnsConfig(domainObject, callback) {
|
|||||||
if (dnsConfig.tokenType !== 'GlobalApiKey' && dnsConfig.tokenType !== 'ApiToken') return callback(new BoxError(BoxError.BAD_FIELD, 'tokenType is required', { field: 'tokenType' }));
|
if (dnsConfig.tokenType !== 'GlobalApiKey' && dnsConfig.tokenType !== 'ApiToken') return callback(new BoxError(BoxError.BAD_FIELD, 'tokenType is required', { field: 'tokenType' }));
|
||||||
|
|
||||||
if (dnsConfig.tokenType === 'GlobalApiKey') {
|
if (dnsConfig.tokenType === 'GlobalApiKey') {
|
||||||
if ('email' in dnsConfig && typeof dnsConfig.email !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'email must be a non-empty string', { field: 'email' }));
|
if (typeof dnsConfig.email !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'email must be a non-empty string', { field: 'email' }));
|
||||||
}
|
}
|
||||||
|
|
||||||
const ip = '127.0.0.1';
|
const ip = '127.0.0.1';
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ exports = module.exports = {
|
|||||||
var assert = require('assert'),
|
var assert = require('assert'),
|
||||||
async = require('async'),
|
async = require('async'),
|
||||||
BoxError = require('../boxerror.js'),
|
BoxError = require('../boxerror.js'),
|
||||||
|
constants = require('../constants.js'),
|
||||||
debug = require('debug')('box:dns/digitalocean'),
|
debug = require('debug')('box:dns/digitalocean'),
|
||||||
dns = require('../native-dns.js'),
|
dns = require('../native-dns.js'),
|
||||||
domains = require('../domains.js'),
|
domains = require('../domains.js'),
|
||||||
@@ -28,12 +29,12 @@ function formatError(response) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function removePrivateFields(domainObject) {
|
function removePrivateFields(domainObject) {
|
||||||
domainObject.config.token = domains.SECRET_PLACEHOLDER;
|
domainObject.config.token = constants.SECRET_PLACEHOLDER;
|
||||||
return domainObject;
|
return domainObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
function injectPrivateFields(newConfig, currentConfig) {
|
function injectPrivateFields(newConfig, currentConfig) {
|
||||||
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInternal(dnsConfig, zoneName, name, type, callback) {
|
function getInternal(dnsConfig, zoneName, name, type, callback) {
|
||||||
|
|||||||
+3
-2
@@ -12,6 +12,7 @@ exports = module.exports = {
|
|||||||
|
|
||||||
var assert = require('assert'),
|
var assert = require('assert'),
|
||||||
BoxError = require('../boxerror.js'),
|
BoxError = require('../boxerror.js'),
|
||||||
|
constants = require('../constants.js'),
|
||||||
debug = require('debug')('box:dns/gandi'),
|
debug = require('debug')('box:dns/gandi'),
|
||||||
dns = require('../native-dns.js'),
|
dns = require('../native-dns.js'),
|
||||||
domains = require('../domains.js'),
|
domains = require('../domains.js'),
|
||||||
@@ -26,12 +27,12 @@ function formatError(response) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function removePrivateFields(domainObject) {
|
function removePrivateFields(domainObject) {
|
||||||
domainObject.config.token = domains.SECRET_PLACEHOLDER;
|
domainObject.config.token = constants.SECRET_PLACEHOLDER;
|
||||||
return domainObject;
|
return domainObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
function injectPrivateFields(newConfig, currentConfig) {
|
function injectPrivateFields(newConfig, currentConfig) {
|
||||||
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
||||||
}
|
}
|
||||||
|
|
||||||
function upsert(domainObject, location, type, values, callback) {
|
function upsert(domainObject, location, type, values, callback) {
|
||||||
|
|||||||
+3
-2
@@ -12,6 +12,7 @@ exports = module.exports = {
|
|||||||
|
|
||||||
var assert = require('assert'),
|
var assert = require('assert'),
|
||||||
BoxError = require('../boxerror.js'),
|
BoxError = require('../boxerror.js'),
|
||||||
|
constants = require('../constants.js'),
|
||||||
debug = require('debug')('box:dns/gcdns'),
|
debug = require('debug')('box:dns/gcdns'),
|
||||||
dns = require('../native-dns.js'),
|
dns = require('../native-dns.js'),
|
||||||
domains = require('../domains.js'),
|
domains = require('../domains.js'),
|
||||||
@@ -21,12 +22,12 @@ var assert = require('assert'),
|
|||||||
_ = require('underscore');
|
_ = require('underscore');
|
||||||
|
|
||||||
function removePrivateFields(domainObject) {
|
function removePrivateFields(domainObject) {
|
||||||
domainObject.config.credentials.private_key = domains.SECRET_PLACEHOLDER;
|
domainObject.config.credentials.private_key = constants.SECRET_PLACEHOLDER;
|
||||||
return domainObject;
|
return domainObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
function injectPrivateFields(newConfig, currentConfig) {
|
function injectPrivateFields(newConfig, currentConfig) {
|
||||||
if (newConfig.credentials.private_key === domains.SECRET_PLACEHOLDER && currentConfig.credentials) newConfig.credentials.private_key = currentConfig.credentials.private_key;
|
if (newConfig.credentials.private_key === constants.SECRET_PLACEHOLDER && currentConfig.credentials) newConfig.credentials.private_key = currentConfig.credentials.private_key;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDnsCredentials(dnsConfig) {
|
function getDnsCredentials(dnsConfig) {
|
||||||
|
|||||||
+3
-2
@@ -12,6 +12,7 @@ exports = module.exports = {
|
|||||||
|
|
||||||
var assert = require('assert'),
|
var assert = require('assert'),
|
||||||
BoxError = require('../boxerror.js'),
|
BoxError = require('../boxerror.js'),
|
||||||
|
constants = require('../constants.js'),
|
||||||
debug = require('debug')('box:dns/godaddy'),
|
debug = require('debug')('box:dns/godaddy'),
|
||||||
dns = require('../native-dns.js'),
|
dns = require('../native-dns.js'),
|
||||||
domains = require('../domains.js'),
|
domains = require('../domains.js'),
|
||||||
@@ -32,12 +33,12 @@ function formatError(response) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function removePrivateFields(domainObject) {
|
function removePrivateFields(domainObject) {
|
||||||
domainObject.config.apiSecret = domains.SECRET_PLACEHOLDER;
|
domainObject.config.apiSecret = constants.SECRET_PLACEHOLDER;
|
||||||
return domainObject;
|
return domainObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
function injectPrivateFields(newConfig, currentConfig) {
|
function injectPrivateFields(newConfig, currentConfig) {
|
||||||
if (newConfig.apiSecret === domains.SECRET_PLACEHOLDER) newConfig.apiSecret = currentConfig.apiSecret;
|
if (newConfig.apiSecret === constants.SECRET_PLACEHOLDER) newConfig.apiSecret = currentConfig.apiSecret;
|
||||||
}
|
}
|
||||||
|
|
||||||
function upsert(domainObject, location, type, values, callback) {
|
function upsert(domainObject, location, type, values, callback) {
|
||||||
|
|||||||
@@ -21,13 +21,13 @@ var assert = require('assert'),
|
|||||||
util = require('util');
|
util = require('util');
|
||||||
|
|
||||||
function removePrivateFields(domainObject) {
|
function removePrivateFields(domainObject) {
|
||||||
// in-place removal of tokens and api keys with domains.SECRET_PLACEHOLDER
|
// in-place removal of tokens and api keys with constants.SECRET_PLACEHOLDER
|
||||||
return domainObject;
|
return domainObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
function injectPrivateFields(newConfig, currentConfig) {
|
function injectPrivateFields(newConfig, currentConfig) {
|
||||||
// in-place injection of tokens and api keys which came in with domains.SECRET_PLACEHOLDER
|
// in-place injection of tokens and api keys which came in with constants.SECRET_PLACEHOLDER
|
||||||
}
|
}
|
||||||
|
|
||||||
function upsert(domainObject, location, type, values, callback) {
|
function upsert(domainObject, location, type, values, callback) {
|
||||||
|
|||||||
+3
-2
@@ -12,6 +12,7 @@ exports = module.exports = {
|
|||||||
|
|
||||||
let async = require('async'),
|
let async = require('async'),
|
||||||
assert = require('assert'),
|
assert = require('assert'),
|
||||||
|
constants = require('../constants.js'),
|
||||||
BoxError = require('../boxerror.js'),
|
BoxError = require('../boxerror.js'),
|
||||||
debug = require('debug')('box:dns/linode'),
|
debug = require('debug')('box:dns/linode'),
|
||||||
dns = require('../native-dns.js'),
|
dns = require('../native-dns.js'),
|
||||||
@@ -27,12 +28,12 @@ function formatError(response) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function removePrivateFields(domainObject) {
|
function removePrivateFields(domainObject) {
|
||||||
domainObject.config.token = domains.SECRET_PLACEHOLDER;
|
domainObject.config.token = constants.SECRET_PLACEHOLDER;
|
||||||
return domainObject;
|
return domainObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
function injectPrivateFields(newConfig, currentConfig) {
|
function injectPrivateFields(newConfig, currentConfig) {
|
||||||
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getZoneId(dnsConfig, zoneName, callback) {
|
function getZoneId(dnsConfig, zoneName, callback) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ exports = module.exports = {
|
|||||||
|
|
||||||
var assert = require('assert'),
|
var assert = require('assert'),
|
||||||
BoxError = require('../boxerror.js'),
|
BoxError = require('../boxerror.js'),
|
||||||
|
constants = require('../constants.js'),
|
||||||
debug = require('debug')('box:dns/namecheap'),
|
debug = require('debug')('box:dns/namecheap'),
|
||||||
dns = require('../native-dns.js'),
|
dns = require('../native-dns.js'),
|
||||||
domains = require('../domains.js'),
|
domains = require('../domains.js'),
|
||||||
@@ -25,12 +26,12 @@ var assert = require('assert'),
|
|||||||
const ENDPOINT = 'https://api.namecheap.com/xml.response';
|
const ENDPOINT = 'https://api.namecheap.com/xml.response';
|
||||||
|
|
||||||
function removePrivateFields(domainObject) {
|
function removePrivateFields(domainObject) {
|
||||||
domainObject.config.token = domains.SECRET_PLACEHOLDER;
|
domainObject.config.token = constants.SECRET_PLACEHOLDER;
|
||||||
return domainObject;
|
return domainObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
function injectPrivateFields(newConfig, currentConfig) {
|
function injectPrivateFields(newConfig, currentConfig) {
|
||||||
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getQuery(dnsConfig, callback) {
|
function getQuery(dnsConfig, callback) {
|
||||||
|
|||||||
+11
-2
@@ -12,6 +12,7 @@ exports = module.exports = {
|
|||||||
|
|
||||||
var assert = require('assert'),
|
var assert = require('assert'),
|
||||||
BoxError = require('../boxerror.js'),
|
BoxError = require('../boxerror.js'),
|
||||||
|
constants = require('../constants.js'),
|
||||||
debug = require('debug')('box:dns/namecom'),
|
debug = require('debug')('box:dns/namecom'),
|
||||||
dns = require('../native-dns.js'),
|
dns = require('../native-dns.js'),
|
||||||
domains = require('../domains.js'),
|
domains = require('../domains.js'),
|
||||||
@@ -27,12 +28,12 @@ function formatError(response) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function removePrivateFields(domainObject) {
|
function removePrivateFields(domainObject) {
|
||||||
domainObject.config.token = domains.SECRET_PLACEHOLDER;
|
domainObject.config.token = constants.SECRET_PLACEHOLDER;
|
||||||
return domainObject;
|
return domainObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
function injectPrivateFields(newConfig, currentConfig) {
|
function injectPrivateFields(newConfig, currentConfig) {
|
||||||
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
||||||
}
|
}
|
||||||
|
|
||||||
function addRecord(dnsConfig, zoneName, name, type, values, callback) {
|
function addRecord(dnsConfig, zoneName, name, type, values, callback) {
|
||||||
@@ -54,6 +55,10 @@ function addRecord(dnsConfig, zoneName, name, type, values, callback) {
|
|||||||
if (type === 'MX') {
|
if (type === 'MX') {
|
||||||
data.priority = parseInt(values[0].split(' ')[0], 10);
|
data.priority = parseInt(values[0].split(' ')[0], 10);
|
||||||
data.answer = values[0].split(' ')[1];
|
data.answer = values[0].split(' ')[1];
|
||||||
|
} else if (type === 'TXT') {
|
||||||
|
// we have to strip the quoting for some odd reason for name.com! If you change that also change updateRecord
|
||||||
|
let tmp = values[0];
|
||||||
|
data.answer = tmp.indexOf('"') === 0 && tmp.lastIndexOf('"') === tmp.length-1 ? tmp.slice(1, tmp.length-1) : tmp;
|
||||||
} else {
|
} else {
|
||||||
data.answer = values[0];
|
data.answer = values[0];
|
||||||
}
|
}
|
||||||
@@ -91,6 +96,10 @@ function updateRecord(dnsConfig, zoneName, recordId, name, type, values, callbac
|
|||||||
if (type === 'MX') {
|
if (type === 'MX') {
|
||||||
data.priority = parseInt(values[0].split(' ')[0], 10);
|
data.priority = parseInt(values[0].split(' ')[0], 10);
|
||||||
data.answer = values[0].split(' ')[1];
|
data.answer = values[0].split(' ')[1];
|
||||||
|
} else if (type === 'TXT') {
|
||||||
|
// we have to strip the quoting for some odd reason for name.com! If you change that also change addRecord
|
||||||
|
let tmp = values[0];
|
||||||
|
data.answer = tmp.indexOf('"') === 0 && tmp.lastIndexOf('"') === tmp.length-1 ? tmp.slice(1, tmp.length-1) : tmp;
|
||||||
} else {
|
} else {
|
||||||
data.answer = values[0];
|
data.answer = values[0];
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-4
@@ -13,6 +13,7 @@ exports = module.exports = {
|
|||||||
var assert = require('assert'),
|
var assert = require('assert'),
|
||||||
AWS = require('aws-sdk'),
|
AWS = require('aws-sdk'),
|
||||||
BoxError = require('../boxerror.js'),
|
BoxError = require('../boxerror.js'),
|
||||||
|
constants = require('../constants.js'),
|
||||||
debug = require('debug')('box:dns/route53'),
|
debug = require('debug')('box:dns/route53'),
|
||||||
dns = require('../native-dns.js'),
|
dns = require('../native-dns.js'),
|
||||||
domains = require('../domains.js'),
|
domains = require('../domains.js'),
|
||||||
@@ -21,12 +22,12 @@ var assert = require('assert'),
|
|||||||
_ = require('underscore');
|
_ = require('underscore');
|
||||||
|
|
||||||
function removePrivateFields(domainObject) {
|
function removePrivateFields(domainObject) {
|
||||||
domainObject.config.secretAccessKey = domains.SECRET_PLACEHOLDER;
|
domainObject.config.secretAccessKey = constants.SECRET_PLACEHOLDER;
|
||||||
return domainObject;
|
return domainObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
function injectPrivateFields(newConfig, currentConfig) {
|
function injectPrivateFields(newConfig, currentConfig) {
|
||||||
if (newConfig.secretAccessKey === domains.SECRET_PLACEHOLDER) newConfig.secretAccessKey = currentConfig.secretAccessKey;
|
if (newConfig.secretAccessKey === constants.SECRET_PLACEHOLDER) newConfig.secretAccessKey = currentConfig.secretAccessKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDnsCredentials(dnsConfig) {
|
function getDnsCredentials(dnsConfig) {
|
||||||
@@ -280,13 +281,14 @@ function verifyDnsConfig(domainObject, callback) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const location = 'cloudrontestdns';
|
const location = 'cloudrontestdns';
|
||||||
|
const newDomainObject = Object.assign({ }, domainObject, { config: credentials });
|
||||||
|
|
||||||
upsert(domainObject, location, 'A', [ ip ], function (error) {
|
upsert(newDomainObject, location, 'A', [ ip ], function (error) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
debug('verifyDnsConfig: Test A record added');
|
debug('verifyDnsConfig: Test A record added');
|
||||||
|
|
||||||
del(domainObject, location, 'A', [ ip ], function (error) {
|
del(newDomainObject, location, 'A', [ ip ], function (error) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
debug('verifyDnsConfig: Test A record removed again');
|
debug('verifyDnsConfig: Test A record removed again');
|
||||||
|
|||||||
+32
-44
@@ -6,8 +6,6 @@ exports = module.exports = {
|
|||||||
injectPrivateFields: injectPrivateFields,
|
injectPrivateFields: injectPrivateFields,
|
||||||
removePrivateFields: removePrivateFields,
|
removePrivateFields: removePrivateFields,
|
||||||
|
|
||||||
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8),
|
|
||||||
|
|
||||||
ping: ping,
|
ping: ping,
|
||||||
|
|
||||||
info: info,
|
info: info,
|
||||||
@@ -55,12 +53,6 @@ const CLEARVOLUME_CMD = path.join(__dirname, 'scripts/clearvolume.sh'),
|
|||||||
const DOCKER_SOCKET_PATH = '/var/run/docker.sock';
|
const DOCKER_SOCKET_PATH = '/var/run/docker.sock';
|
||||||
const gConnection = new Docker({ socketPath: DOCKER_SOCKET_PATH });
|
const gConnection = new Docker({ socketPath: DOCKER_SOCKET_PATH });
|
||||||
|
|
||||||
function debugApp(app) {
|
|
||||||
assert(typeof app === 'object');
|
|
||||||
|
|
||||||
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
|
|
||||||
}
|
|
||||||
|
|
||||||
function testRegistryConfig(auth, callback) {
|
function testRegistryConfig(auth, callback) {
|
||||||
assert.strictEqual(typeof auth, 'object');
|
assert.strictEqual(typeof auth, 'object');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
@@ -73,13 +65,13 @@ function testRegistryConfig(auth, callback) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function injectPrivateFields(newConfig, currentConfig) {
|
function injectPrivateFields(newConfig, currentConfig) {
|
||||||
if (newConfig.password === exports.SECRET_PLACEHOLDER) newConfig.password = currentConfig.password;
|
if (newConfig.password === constants.SECRET_PLACEHOLDER) newConfig.password = currentConfig.password;
|
||||||
}
|
}
|
||||||
|
|
||||||
function removePrivateFields(registryConfig) {
|
function removePrivateFields(registryConfig) {
|
||||||
assert.strictEqual(typeof registryConfig, 'object');
|
assert.strictEqual(typeof registryConfig, 'object');
|
||||||
|
|
||||||
if (registryConfig.password) registryConfig.password = exports.SECRET_PLACEHOLDER;
|
if (registryConfig.password) registryConfig.password = constants.SECRET_PLACEHOLDER;
|
||||||
|
|
||||||
return registryConfig;
|
return registryConfig;
|
||||||
}
|
}
|
||||||
@@ -188,6 +180,19 @@ function downloadImage(manifest, callback) {
|
|||||||
}, callback);
|
}, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getBindsSync(app) {
|
||||||
|
assert.strictEqual(typeof app, 'object');
|
||||||
|
|
||||||
|
let binds = [];
|
||||||
|
|
||||||
|
for (let name of Object.keys(app.binds)) {
|
||||||
|
const bind = app.binds[name];
|
||||||
|
binds.push(`${bind.hostPath}:/media/${name}:${bind.readOnly ? 'ro' : 'rw'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return binds;
|
||||||
|
}
|
||||||
|
|
||||||
function createSubcontainer(app, name, cmd, options, callback) {
|
function createSubcontainer(app, name, cmd, options, callback) {
|
||||||
assert.strictEqual(typeof app, 'object');
|
assert.strictEqual(typeof app, 'object');
|
||||||
assert.strictEqual(typeof name, 'string');
|
assert.strictEqual(typeof name, 'string');
|
||||||
@@ -277,6 +282,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
|||||||
},
|
},
|
||||||
HostConfig: {
|
HostConfig: {
|
||||||
Mounts: addons.getMountsSync(app, app.manifest.addons),
|
Mounts: addons.getMountsSync(app, app.manifest.addons),
|
||||||
|
Binds: getBindsSync(app), // ideally, we have to use 'Mounts' but we have to create volumes then
|
||||||
LogConfig: {
|
LogConfig: {
|
||||||
Type: 'syslog',
|
Type: 'syslog',
|
||||||
Config: {
|
Config: {
|
||||||
@@ -299,7 +305,9 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
|||||||
NetworkMode: 'cloudron', // user defined bridge network
|
NetworkMode: 'cloudron', // user defined bridge network
|
||||||
Dns: ['172.18.0.1'], // use internal dns
|
Dns: ['172.18.0.1'], // use internal dns
|
||||||
DnsSearch: ['.'], // use internal dns
|
DnsSearch: ['.'], // use internal dns
|
||||||
SecurityOpt: [ 'apparmor=docker-cloudron-app' ]
|
SecurityOpt: [ 'apparmor=docker-cloudron-app' ],
|
||||||
|
CapAdd: [],
|
||||||
|
CapDrop: []
|
||||||
},
|
},
|
||||||
NetworkingConfig: {
|
NetworkingConfig: {
|
||||||
EndpointsConfig: {
|
EndpointsConfig: {
|
||||||
@@ -311,16 +319,14 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
var capabilities = manifest.capabilities || [];
|
var capabilities = manifest.capabilities || [];
|
||||||
if (capabilities.includes('net_admin')) {
|
|
||||||
containerOptions.HostConfig.CapAdd = [
|
// https://docs-stage.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
|
||||||
'NET_ADMIN'
|
if (capabilities.includes('net_admin')) containerOptions.HostConfig.CapAdd.push('NET_ADMIN', 'NET_RAW');
|
||||||
];
|
if (capabilities.includes('mlock')) containerOptions.HostConfig.CapAdd.push('IPC_LOCK'); // mlock prevents swapping
|
||||||
}
|
if (!capabilities.includes('ping')) containerOptions.HostConfig.CapDrop.push('NET_RAW'); // NET_RAW is included by default by Docker
|
||||||
|
|
||||||
containerOptions = _.extend(containerOptions, options);
|
containerOptions = _.extend(containerOptions, options);
|
||||||
|
|
||||||
debugApp(app, 'Creating container for %s', app.manifest.dockerImage);
|
|
||||||
|
|
||||||
gConnection.createContainer(containerOptions, function (error, container) {
|
gConnection.createContainer(containerOptions, function (error, container) {
|
||||||
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
|
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
|
||||||
|
|
||||||
@@ -338,7 +344,6 @@ function startContainer(containerId, callback) {
|
|||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
var container = gConnection.getContainer(containerId);
|
var container = gConnection.getContainer(containerId);
|
||||||
debug('Starting container %s', containerId);
|
|
||||||
|
|
||||||
container.start(function (error) {
|
container.start(function (error) {
|
||||||
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
|
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||||
@@ -354,7 +359,6 @@ function restartContainer(containerId, callback) {
|
|||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
var container = gConnection.getContainer(containerId);
|
var container = gConnection.getContainer(containerId);
|
||||||
debug('Restarting container %s', containerId);
|
|
||||||
|
|
||||||
container.restart(function (error) {
|
container.restart(function (error) {
|
||||||
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
|
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||||
@@ -375,7 +379,6 @@ function stopContainer(containerId, callback) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var container = gConnection.getContainer(containerId);
|
var container = gConnection.getContainer(containerId);
|
||||||
debug('Stopping container %s', containerId);
|
|
||||||
|
|
||||||
var options = {
|
var options = {
|
||||||
t: 10 // wait for 10 seconds before killing it
|
t: 10 // wait for 10 seconds before killing it
|
||||||
@@ -384,13 +387,9 @@ function stopContainer(containerId, callback) {
|
|||||||
container.stop(options, function (error) {
|
container.stop(options, function (error) {
|
||||||
if (error && (error.statusCode !== 304 && error.statusCode !== 404)) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Error stopping container:' + error.message));
|
if (error && (error.statusCode !== 304 && error.statusCode !== 404)) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Error stopping container:' + error.message));
|
||||||
|
|
||||||
debug('Waiting for container ' + containerId);
|
container.wait(function (error/*, data */) {
|
||||||
|
|
||||||
container.wait(function (error, data) {
|
|
||||||
if (error && (error.statusCode !== 304 && error.statusCode !== 404)) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Error waiting on container:' + error.message));
|
if (error && (error.statusCode !== 304 && error.statusCode !== 404)) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Error waiting on container:' + error.message));
|
||||||
|
|
||||||
debug('Container %s stopped with status code [%s]', containerId, data ? String(data.StatusCode) : '');
|
|
||||||
|
|
||||||
return callback(null);
|
return callback(null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -400,8 +399,6 @@ function deleteContainer(containerId, callback) {
|
|||||||
assert(!containerId || typeof containerId === 'string');
|
assert(!containerId || typeof containerId === 'string');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
debug('deleting container %s', containerId);
|
|
||||||
|
|
||||||
if (containerId === null) return callback(null);
|
if (containerId === null) return callback(null);
|
||||||
|
|
||||||
var container = gConnection.getContainer(containerId);
|
var container = gConnection.getContainer(containerId);
|
||||||
@@ -428,8 +425,6 @@ function deleteContainers(appId, options, callback) {
|
|||||||
assert.strictEqual(typeof options, 'object');
|
assert.strictEqual(typeof options, 'object');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
debug('deleting containers of %s', appId);
|
|
||||||
|
|
||||||
let labels = [ 'appId=' + appId ];
|
let labels = [ 'appId=' + appId ];
|
||||||
if (options.managedOnly) labels.push('isCloudronManaged=true');
|
if (options.managedOnly) labels.push('isCloudronManaged=true');
|
||||||
|
|
||||||
@@ -446,8 +441,6 @@ function stopContainers(appId, callback) {
|
|||||||
assert.strictEqual(typeof appId, 'string');
|
assert.strictEqual(typeof appId, 'string');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
debug('Stopping containers of %s', appId);
|
|
||||||
|
|
||||||
gConnection.listContainers({ all: 1, filters: JSON.stringify({ label: [ 'appId=' + appId ] }) }, function (error, containers) {
|
gConnection.listContainers({ all: 1, filters: JSON.stringify({ label: [ 'appId=' + appId ] }) }, function (error, containers) {
|
||||||
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
|
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
|
||||||
|
|
||||||
@@ -514,7 +507,7 @@ function inspect(containerId, callback) {
|
|||||||
var container = gConnection.getContainer(containerId);
|
var container = gConnection.getContainer(containerId);
|
||||||
|
|
||||||
container.inspect(function (error, result) {
|
container.inspect(function (error, result) {
|
||||||
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
|
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND, `Unable to find container ${containerId}`));
|
||||||
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
|
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
|
||||||
|
|
||||||
callback(null, result);
|
callback(null, result);
|
||||||
@@ -573,10 +566,10 @@ function memoryUsage(containerId, callback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function createVolume(app, name, volumeDataDir, callback) {
|
function createVolume(name, volumeDataDir, labels, callback) {
|
||||||
assert.strictEqual(typeof app, 'object');
|
|
||||||
assert.strictEqual(typeof name, 'string');
|
assert.strictEqual(typeof name, 'string');
|
||||||
assert.strictEqual(typeof volumeDataDir, 'string');
|
assert.strictEqual(typeof volumeDataDir, 'string');
|
||||||
|
assert.strictEqual(typeof labels, 'object');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
const volumeOptions = {
|
const volumeOptions = {
|
||||||
@@ -587,10 +580,7 @@ function createVolume(app, name, volumeDataDir, callback) {
|
|||||||
device: volumeDataDir,
|
device: volumeDataDir,
|
||||||
o: 'bind'
|
o: 'bind'
|
||||||
},
|
},
|
||||||
Labels: {
|
Labels: labels
|
||||||
'fqdn': app.fqdn,
|
|
||||||
'appId': app.id
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// requires sudo because the path can be outside appsdata
|
// requires sudo because the path can be outside appsdata
|
||||||
@@ -605,8 +595,7 @@ function createVolume(app, name, volumeDataDir, callback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearVolume(app, name, options, callback) {
|
function clearVolume(name, options, callback) {
|
||||||
assert.strictEqual(typeof app, 'object');
|
|
||||||
assert.strictEqual(typeof name, 'string');
|
assert.strictEqual(typeof name, 'string');
|
||||||
assert.strictEqual(typeof options, 'object');
|
assert.strictEqual(typeof options, 'object');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
@@ -626,14 +615,13 @@ function clearVolume(app, name, options, callback) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// this only removes the volume and not the data
|
// this only removes the volume and not the data
|
||||||
function removeVolume(app, name, callback) {
|
function removeVolume(name, callback) {
|
||||||
assert.strictEqual(typeof app, 'object');
|
|
||||||
assert.strictEqual(typeof name, 'string');
|
assert.strictEqual(typeof name, 'string');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
let volume = gConnection.getVolume(name);
|
let volume = gConnection.getVolume(name);
|
||||||
volume.remove(function (error) {
|
volume.remove(function (error) {
|
||||||
if (error && error.statusCode !== 404) return callback(new BoxError(BoxError.DOCKER_ERROR, `removeVolume: Error removing volume of ${app.id} ${error.message}`));
|
if (error && error.statusCode !== 404) return callback(new BoxError(BoxError.DOCKER_ERROR, `removeVolume: Error removing volume: ${error.message}`));
|
||||||
|
|
||||||
callback();
|
callback();
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-1
@@ -55,7 +55,7 @@ function attachDockerRequest(req, res, next) {
|
|||||||
// Force node to send out the headers, this is required for the /container/wait api to make the docker cli proceed
|
// Force node to send out the headers, this is required for the /container/wait api to make the docker cli proceed
|
||||||
res.write(' ');
|
res.write(' ');
|
||||||
|
|
||||||
dockerResponse.on('error', function (error) { console.error('dockerResponse error:', error); });
|
dockerResponse.on('error', function (error) { debug('dockerResponse error:', error); });
|
||||||
dockerResponse.pipe(res, { end: true });
|
dockerResponse.pipe(res, { end: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -66,7 +66,7 @@ function add(name, data, callback) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
database.transaction(queries, function (error) {
|
database.transaction(queries, function (error) {
|
||||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, error));
|
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, 'Domain already exists'));
|
||||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||||
|
|
||||||
callback(null);
|
callback(null);
|
||||||
|
|||||||
+2
-6
@@ -28,9 +28,7 @@ module.exports = exports = {
|
|||||||
|
|
||||||
checkDnsRecords: checkDnsRecords,
|
checkDnsRecords: checkDnsRecords,
|
||||||
|
|
||||||
prepareDashboardDomain: prepareDashboardDomain,
|
prepareDashboardDomain: prepareDashboardDomain
|
||||||
|
|
||||||
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var assert = require('assert'),
|
var assert = require('assert'),
|
||||||
@@ -56,7 +54,6 @@ function api(provider) {
|
|||||||
assert.strictEqual(typeof provider, 'string');
|
assert.strictEqual(typeof provider, 'string');
|
||||||
|
|
||||||
switch (provider) {
|
switch (provider) {
|
||||||
case 'caas': return require('./dns/caas.js');
|
|
||||||
case 'cloudflare': return require('./dns/cloudflare.js');
|
case 'cloudflare': return require('./dns/cloudflare.js');
|
||||||
case 'route53': return require('./dns/route53.js');
|
case 'route53': return require('./dns/route53.js');
|
||||||
case 'gcdns': return require('./dns/gcdns.js');
|
case 'gcdns': return require('./dns/gcdns.js');
|
||||||
@@ -151,10 +148,9 @@ function validateTlsConfig(tlsConfig, dnsProvider) {
|
|||||||
case 'letsencrypt-prod':
|
case 'letsencrypt-prod':
|
||||||
case 'letsencrypt-staging':
|
case 'letsencrypt-staging':
|
||||||
case 'fallback':
|
case 'fallback':
|
||||||
case 'caas':
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return new BoxError(BoxError.BAD_FIELD, 'tlsConfig.provider must be caas, fallback, letsencrypt-prod/staging', { field: 'tlsProvider' });
|
return new BoxError(BoxError.BAD_FIELD, 'tlsConfig.provider must be fallback, letsencrypt-prod/staging', { field: 'tlsProvider' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tlsConfig.wildcard) {
|
if (tlsConfig.wildcard) {
|
||||||
|
|||||||
+332
-66
@@ -20,7 +20,9 @@ var assert = require('assert'),
|
|||||||
BoxError = require('./boxerror.js'),
|
BoxError = require('./boxerror.js'),
|
||||||
constants = require('./constants.js'),
|
constants = require('./constants.js'),
|
||||||
debug = require('debug')('box:externalldap'),
|
debug = require('debug')('box:externalldap'),
|
||||||
|
groups = require('./groups.js'),
|
||||||
ldap = require('ldapjs'),
|
ldap = require('ldapjs'),
|
||||||
|
once = require('once'),
|
||||||
settings = require('./settings.js'),
|
settings = require('./settings.js'),
|
||||||
tasks = require('./tasks.js'),
|
tasks = require('./tasks.js'),
|
||||||
users = require('./users.js');
|
users = require('./users.js');
|
||||||
@@ -40,14 +42,14 @@ function translateUser(ldapConfig, ldapUser) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
username: ldapUser[ldapConfig.usernameField],
|
username: ldapUser[ldapConfig.usernameField],
|
||||||
email: ldapUser.mail,
|
email: ldapUser.mail || ldapUser.mailPrimaryAddress,
|
||||||
displayName: ldapUser.cn // user.giveName + ' ' + user.sn
|
displayName: ldapUser.cn // user.giveName + ' ' + user.sn
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function validUserRequirements(user) {
|
function validUserRequirements(user) {
|
||||||
if (!user.username || !user.email || !user.displayName) {
|
if (!user.username || !user.email || !user.displayName) {
|
||||||
debug(`[LDAP user empty username/email/displayName] username=${user.username} email=${user.email} displayName=${user.displayName}`);
|
debug(`[Invalid LDAP user] username=${user.username} email=${user.email} displayName=${user.displayName}`);
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
return true;
|
return true;
|
||||||
@@ -55,40 +57,95 @@ function validUserRequirements(user) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// performs service bind if required
|
// performs service bind if required
|
||||||
function getClient(externalLdapConfig, callback) {
|
function getClient(externalLdapConfig, doBindAuth, callback) {
|
||||||
assert.strictEqual(typeof externalLdapConfig, 'object');
|
assert.strictEqual(typeof externalLdapConfig, 'object');
|
||||||
|
assert.strictEqual(typeof doBindAuth, 'boolean');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
// ensure we only callback once since we also have to listen to client.error events
|
||||||
|
callback = once(callback);
|
||||||
|
|
||||||
// basic validation to not crash
|
// basic validation to not crash
|
||||||
try { ldap.parseDN(externalLdapConfig.baseDn); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid baseDn')); }
|
try { ldap.parseDN(externalLdapConfig.baseDn); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid baseDn')); }
|
||||||
try { ldap.parseFilter(externalLdapConfig.filter); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid filter')); }
|
try { ldap.parseFilter(externalLdapConfig.filter); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid filter')); }
|
||||||
|
|
||||||
|
var config = {
|
||||||
|
url: externalLdapConfig.url,
|
||||||
|
tlsOptions: {
|
||||||
|
rejectUnauthorized: externalLdapConfig.acceptSelfSignedCerts ? false : true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
var client;
|
var client;
|
||||||
try {
|
try {
|
||||||
client = ldap.createClient({ url: externalLdapConfig.url });
|
client = ldap.createClient(config);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof ldap.ProtocolError) return callback(new BoxError(BoxError.BAD_FIELD, 'url protocol is invalid'));
|
if (e instanceof ldap.ProtocolError) return callback(new BoxError(BoxError.BAD_FIELD, 'url protocol is invalid'));
|
||||||
return callback(new BoxError(BoxError.INTERNAL_ERROR, e));
|
return callback(new BoxError(BoxError.INTERNAL_ERROR, e));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!externalLdapConfig.bindDn) return callback(null, client);
|
// ensure we don't just crash
|
||||||
|
client.on('error', function (error) {
|
||||||
|
callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
|
||||||
|
});
|
||||||
|
|
||||||
|
// skip bind auth if none exist or if not wanted
|
||||||
|
if (!externalLdapConfig.bindDn || !doBindAuth) return callback(null, client);
|
||||||
|
|
||||||
client.bind(externalLdapConfig.bindDn, externalLdapConfig.bindPassword, function (error) {
|
client.bind(externalLdapConfig.bindDn, externalLdapConfig.bindPassword, function (error) {
|
||||||
if (error instanceof ldap.InvalidCredentialsError) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
if (error instanceof ldap.InvalidCredentialsError) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||||
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
|
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
|
||||||
|
|
||||||
callback(null, client, externalLdapConfig);
|
callback(null, client);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ldapGetByDN(externalLdapConfig, dn, callback) {
|
||||||
|
assert.strictEqual(typeof externalLdapConfig, 'object');
|
||||||
|
assert.strictEqual(typeof dn, 'string');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
getClient(externalLdapConfig, true, function (error, client) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
let searchOptions = {
|
||||||
|
paged: true,
|
||||||
|
scope: 'sub' // We may have to make this configurable
|
||||||
|
};
|
||||||
|
|
||||||
|
debug(`Get object at ${dn}`);
|
||||||
|
|
||||||
|
// basic validation to not crash
|
||||||
|
try { ldap.parseDN(dn); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid DN')); }
|
||||||
|
|
||||||
|
client.search(dn, searchOptions, function (error, result) {
|
||||||
|
if (error instanceof ldap.NoSuchObjectError) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||||
|
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
|
||||||
|
|
||||||
|
let ldapObjects = [];
|
||||||
|
|
||||||
|
result.on('searchEntry', entry => ldapObjects.push(entry.object));
|
||||||
|
result.on('error', error => callback(new BoxError(BoxError.EXTERNAL_ERROR, error)));
|
||||||
|
|
||||||
|
result.on('end', function (result) {
|
||||||
|
client.unbind();
|
||||||
|
|
||||||
|
if (result.status !== 0) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Server returned status ' + result.status));
|
||||||
|
if (ldapObjects.length === 0) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||||
|
|
||||||
|
callback(null, ldapObjects[0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// TODO support search by email
|
// TODO support search by email
|
||||||
function ldapSearch(externalLdapConfig, options, callback) {
|
function ldapUserSearch(externalLdapConfig, options, callback) {
|
||||||
assert.strictEqual(typeof externalLdapConfig, 'object');
|
assert.strictEqual(typeof externalLdapConfig, 'object');
|
||||||
assert.strictEqual(typeof options, 'object');
|
assert.strictEqual(typeof options, 'object');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
getClient(externalLdapConfig, function (error, client) {
|
getClient(externalLdapConfig, true, function (error, client) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
let searchOptions = {
|
let searchOptions = {
|
||||||
@@ -124,6 +181,48 @@ function ldapSearch(externalLdapConfig, options, callback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ldapGroupSearch(externalLdapConfig, options, callback) {
|
||||||
|
assert.strictEqual(typeof externalLdapConfig, 'object');
|
||||||
|
assert.strictEqual(typeof options, 'object');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
getClient(externalLdapConfig, true, function (error, client) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
let searchOptions = {
|
||||||
|
paged: true,
|
||||||
|
scope: 'sub' // We may have to make this configurable
|
||||||
|
};
|
||||||
|
|
||||||
|
if (externalLdapConfig.groupFilter) searchOptions.filter = ldap.parseFilter(externalLdapConfig.groupFilter);
|
||||||
|
|
||||||
|
if (options.filter) { // https://github.com/ldapjs/node-ldapjs/blob/master/docs/filters.md
|
||||||
|
let extraFilter = ldap.parseFilter(options.filter);
|
||||||
|
searchOptions.filter = new ldap.AndFilter({ filters: [ extraFilter, searchOptions.filter ] });
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(`Listing groups at ${externalLdapConfig.groupBaseDn} with filter ${searchOptions.filter.toString()}`);
|
||||||
|
|
||||||
|
client.search(externalLdapConfig.groupBaseDn, searchOptions, function (error, result) {
|
||||||
|
if (error instanceof ldap.NoSuchObjectError) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||||
|
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
|
||||||
|
|
||||||
|
let ldapGroups = [];
|
||||||
|
|
||||||
|
result.on('searchEntry', entry => ldapGroups.push(entry.object));
|
||||||
|
result.on('error', error => callback(new BoxError(BoxError.EXTERNAL_ERROR, error)));
|
||||||
|
|
||||||
|
result.on('end', function (result) {
|
||||||
|
client.unbind();
|
||||||
|
|
||||||
|
if (result.status !== 0) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Server returned status ' + result.status));
|
||||||
|
|
||||||
|
callback(null, ldapGroups);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function testConfig(config, callback) {
|
function testConfig(config, callback) {
|
||||||
assert.strictEqual(typeof config, 'object');
|
assert.strictEqual(typeof config, 'object');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
@@ -141,7 +240,20 @@ function testConfig(config, callback) {
|
|||||||
if (!config.filter) return callback(new BoxError(BoxError.BAD_FIELD, 'filter must not be empty'));
|
if (!config.filter) return callback(new BoxError(BoxError.BAD_FIELD, 'filter must not be empty'));
|
||||||
try { ldap.parseFilter(config.filter); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid filter')); }
|
try { ldap.parseFilter(config.filter); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid filter')); }
|
||||||
|
|
||||||
getClient(config, function (error, client) {
|
if ('syncGroups' in config && typeof config.syncGroups !== 'boolean') return callback(new BoxError(BoxError.BAD_FIELD, 'syncGroups must be a boolean'));
|
||||||
|
if ('acceptSelfSignedCerts' in config && typeof config.acceptSelfSignedCerts !== 'boolean') return callback(new BoxError(BoxError.BAD_FIELD, 'acceptSelfSignedCerts must be a boolean'));
|
||||||
|
|
||||||
|
if (config.syncGroups) {
|
||||||
|
if (!config.groupBaseDn) return callback(new BoxError(BoxError.BAD_FIELD, 'groupBaseDn must not be empty'));
|
||||||
|
try { ldap.parseDN(config.groupBaseDn); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid groupBaseDn')); }
|
||||||
|
|
||||||
|
if (!config.groupFilter) return callback(new BoxError(BoxError.BAD_FIELD, 'groupFilter must not be empty'));
|
||||||
|
try { ldap.parseFilter(config.groupFilter); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid groupFilter')); }
|
||||||
|
|
||||||
|
if (!config.groupnameField || typeof config.groupnameField !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'groupFilter must not be empty'));
|
||||||
|
}
|
||||||
|
|
||||||
|
getClient(config, true, function (error, client) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
var opts = {
|
var opts = {
|
||||||
@@ -167,7 +279,7 @@ function search(identifier, callback) {
|
|||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled'));
|
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled'));
|
||||||
|
|
||||||
ldapSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${identifier}` }, function (error, ldapUsers) {
|
ldapUserSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${identifier}` }, function (error, ldapUsers) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
// translate ldap properties to ours
|
// translate ldap properties to ours
|
||||||
@@ -188,7 +300,7 @@ function createAndVerifyUserIfNotExist(identifier, password, callback) {
|
|||||||
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled'));
|
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled'));
|
||||||
if (!externalLdapConfig.autoCreate) return callback(new BoxError(BoxError.BAD_STATE, 'auto create not enabled'));
|
if (!externalLdapConfig.autoCreate) return callback(new BoxError(BoxError.BAD_STATE, 'auto create not enabled'));
|
||||||
|
|
||||||
ldapSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${identifier}` }, function (error, ldapUsers) {
|
ldapUserSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${identifier}` }, function (error, ldapUsers) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
if (ldapUsers.length === 0) return callback(new BoxError(BoxError.NOT_FOUND));
|
if (ldapUsers.length === 0) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||||
if (ldapUsers.length > 1) return callback(new BoxError(BoxError.CONFLICT));
|
if (ldapUsers.length > 1) return callback(new BoxError(BoxError.CONFLICT));
|
||||||
@@ -198,7 +310,7 @@ function createAndVerifyUserIfNotExist(identifier, password, callback) {
|
|||||||
|
|
||||||
users.create(user.username, null /* password */, user.email, user.displayName, { source: 'ldap' }, auditSource.EXTERNAL_LDAP_AUTO_CREATE, function (error, user) {
|
users.create(user.username, null /* password */, user.email, user.displayName, { source: 'ldap' }, auditSource.EXTERNAL_LDAP_AUTO_CREATE, function (error, user) {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error('Failed to auto create user', user.username, error);
|
debug(`createAndVerifyUserIfNotExist: Failed to auto create user ${user.username}`, error);
|
||||||
return callback(new BoxError(BoxError.INTERNAL_ERROR));
|
return callback(new BoxError(BoxError.INTERNAL_ERROR));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,17 +332,20 @@ function verifyPassword(user, password, callback) {
|
|||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled'));
|
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled'));
|
||||||
|
|
||||||
ldapSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${user.username}` }, function (error, ldapUsers) {
|
ldapUserSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${user.username}` }, function (error, ldapUsers) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
if (ldapUsers.length === 0) return callback(new BoxError(BoxError.NOT_FOUND));
|
if (ldapUsers.length === 0) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||||
if (ldapUsers.length > 1) return callback(new BoxError(BoxError.CONFLICT));
|
if (ldapUsers.length > 1) return callback(new BoxError(BoxError.CONFLICT));
|
||||||
|
|
||||||
let client = ldap.createClient({ url: externalLdapConfig.url });
|
getClient(externalLdapConfig, false, function (error, client) {
|
||||||
client.bind(ldapUsers[0].dn, password, function (error) {
|
if (error) return callback(error);
|
||||||
if (error instanceof ldap.InvalidCredentialsError) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
|
||||||
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
|
|
||||||
|
|
||||||
callback(null, translateUser(externalLdapConfig, ldapUsers[0]));
|
client.bind(ldapUsers[0].dn, password, function (error) {
|
||||||
|
if (error instanceof ldap.InvalidCredentialsError) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||||
|
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
|
||||||
|
|
||||||
|
callback(null, translateUser(externalLdapConfig, ldapUsers[0]));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -255,6 +370,197 @@ function startSyncer(callback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function syncUsers(externalLdapConfig, progressCallback, callback) {
|
||||||
|
assert.strictEqual(typeof externalLdapConfig, 'object');
|
||||||
|
assert.strictEqual(typeof progressCallback, 'function');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
ldapUserSearch(externalLdapConfig, {}, function (error, ldapUsers) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
debug(`Found ${ldapUsers.length} users`);
|
||||||
|
|
||||||
|
let percent = 10;
|
||||||
|
let step = 30/(ldapUsers.length+1); // ensure no divide by 0
|
||||||
|
|
||||||
|
// we ignore all errors here and just log them for now
|
||||||
|
async.eachSeries(ldapUsers, function (user, iteratorCallback) {
|
||||||
|
user = translateUser(externalLdapConfig, user);
|
||||||
|
|
||||||
|
if (!validUserRequirements(user)) return iteratorCallback();
|
||||||
|
|
||||||
|
percent += step;
|
||||||
|
progressCallback({ percent, message: `Syncing... ${user.username}` });
|
||||||
|
|
||||||
|
users.getByUsername(user.username, function (error, result) {
|
||||||
|
if (error && error.reason !== BoxError.NOT_FOUND) return iteratorCallback(error);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
debug(`[adding user] username=${user.username} email=${user.email} displayName=${user.displayName}`);
|
||||||
|
|
||||||
|
users.create(user.username, null /* password */, user.email, user.displayName, { source: 'ldap' }, auditSource.EXTERNAL_LDAP_TASK, function (error) {
|
||||||
|
if (error) debug('syncUsers: Failed to create user', user, error.message);
|
||||||
|
iteratorCallback();
|
||||||
|
});
|
||||||
|
} else if (result.source !== 'ldap') {
|
||||||
|
debug(`[conflicting user] username=${user.username} email=${user.email} displayName=${user.displayName}`);
|
||||||
|
|
||||||
|
iteratorCallback();
|
||||||
|
} else if (result.email !== user.email || result.displayName !== user.displayName) {
|
||||||
|
debug(`[updating user] username=${user.username} email=${user.email} displayName=${user.displayName}`);
|
||||||
|
|
||||||
|
users.update(result, { email: user.email, fallbackEmail: user.email, displayName: user.displayName }, auditSource.EXTERNAL_LDAP_TASK, function (error) {
|
||||||
|
if (error) debug('Failed to update user', user, error);
|
||||||
|
|
||||||
|
iteratorCallback();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// user known and up-to-date
|
||||||
|
debug(`[up-to-date user] username=${user.username} email=${user.email} displayName=${user.displayName}`);
|
||||||
|
|
||||||
|
iteratorCallback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, callback);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncGroups(externalLdapConfig, progressCallback, callback) {
|
||||||
|
assert.strictEqual(typeof externalLdapConfig, 'object');
|
||||||
|
assert.strictEqual(typeof progressCallback, 'function');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
if (!externalLdapConfig.syncGroups) {
|
||||||
|
debug('Group sync is disabled');
|
||||||
|
progressCallback({ percent: 70, message: 'Skipping group sync...' });
|
||||||
|
return callback(null, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
ldapGroupSearch(externalLdapConfig, {}, function (error, ldapGroups) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
debug(`Found ${ldapGroups.length} groups`);
|
||||||
|
|
||||||
|
let percent = 40;
|
||||||
|
let step = 30/(ldapGroups.length+1); // ensure no divide by 0
|
||||||
|
|
||||||
|
// we ignore all non internal errors here and just log them for now
|
||||||
|
async.eachSeries(ldapGroups, function (ldapGroup, iteratorCallback) {
|
||||||
|
var groupName = ldapGroup[externalLdapConfig.groupnameField];
|
||||||
|
if (!groupName) return iteratorCallback();
|
||||||
|
// some servers return empty array for unknown properties :-/
|
||||||
|
if (typeof groupName !== 'string') return iteratorCallback();
|
||||||
|
|
||||||
|
// groups are lowercase
|
||||||
|
groupName = groupName.toLowerCase();
|
||||||
|
|
||||||
|
percent += step;
|
||||||
|
progressCallback({ percent, message: `Syncing... ${groupName}` });
|
||||||
|
|
||||||
|
groups.getByName(groupName, function (error, result) {
|
||||||
|
if (error && error.reason !== BoxError.NOT_FOUND) return iteratorCallback(error);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
debug(`[adding group] groupname=${groupName}`);
|
||||||
|
|
||||||
|
groups.create(groupName, 'ldap', function (error) {
|
||||||
|
if (error) debug('syncGroups: Failed to create group', groupName, error);
|
||||||
|
iteratorCallback();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
debug(`[up-to-date group] groupname=${groupName}`);
|
||||||
|
|
||||||
|
iteratorCallback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, function (error) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
debug('sync: ldap sync is done', error);
|
||||||
|
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncGroupUsers(externalLdapConfig, progressCallback, callback) {
|
||||||
|
assert.strictEqual(typeof externalLdapConfig, 'object');
|
||||||
|
assert.strictEqual(typeof progressCallback, 'function');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
if (!externalLdapConfig.syncGroups) {
|
||||||
|
debug('Group users sync is disabled');
|
||||||
|
progressCallback({ percent: 99, message: 'Skipping group users sync...' });
|
||||||
|
return callback(null, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
groups.getAll(function (error, result) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
var ldapGroups = result.filter(function (g) { return g.source === 'ldap'; });
|
||||||
|
debug(`Found ${ldapGroups.length} groups to sync users`);
|
||||||
|
|
||||||
|
async.eachSeries(ldapGroups, function (group, iteratorCallback) {
|
||||||
|
debug(`Sync users for group ${group.name}`);
|
||||||
|
|
||||||
|
ldapGroupSearch(externalLdapConfig, {}, function (error, result) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
if (!result || result.length === 0) {
|
||||||
|
debug(`syncGroupUsers: Unable to find group ${group.name} ignoring for now.`);
|
||||||
|
return callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
// since our group names are lowercase we cannot use potentially case matching ldap filters
|
||||||
|
let found = result.find(function (r) {
|
||||||
|
if (!r[externalLdapConfig.groupnameField]) return false;
|
||||||
|
return r[externalLdapConfig.groupnameField].toLowerCase() === group.name;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
debug(`syncGroupUsers: Unable to find group ${group.name} ignoring for now.`);
|
||||||
|
return callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
var ldapGroupMembers = found.member || found.uniqueMember || [];
|
||||||
|
|
||||||
|
// if only one entry is in the group ldap returns a string, not an array!
|
||||||
|
if (typeof ldapGroupMembers === 'string') ldapGroupMembers = [ ldapGroupMembers ];
|
||||||
|
|
||||||
|
debug(`Group ${group.name} has ${ldapGroupMembers.length} members.`);
|
||||||
|
|
||||||
|
async.eachSeries(ldapGroupMembers, function (memberDn, iteratorCallback) {
|
||||||
|
ldapGetByDN(externalLdapConfig, memberDn, function (error, result) {
|
||||||
|
if (error) {
|
||||||
|
debug(`Failed to get ${memberDn}:`, error);
|
||||||
|
return iteratorCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(`Found member object at ${memberDn} adding to group ${group.name}`);
|
||||||
|
|
||||||
|
const username = result[externalLdapConfig.usernameField];
|
||||||
|
if (!username) return iteratorCallback();
|
||||||
|
|
||||||
|
users.getByUsername(username, function (error, result) {
|
||||||
|
if (error) {
|
||||||
|
debug(`syncGroupUsers: Failed to get user by username ${username}`, error);
|
||||||
|
return iteratorCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
groups.addMember(group.id, result.id, function (error) {
|
||||||
|
if (error && error.reason !== BoxError.ALREADY_EXISTS) debug('syncGroupUsers: Failed to add member', error);
|
||||||
|
iteratorCallback();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, function (error) {
|
||||||
|
if (error) debug('syncGroupUsers: ', error);
|
||||||
|
iteratorCallback();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, callback);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function sync(progressCallback, callback) {
|
function sync(progressCallback, callback) {
|
||||||
assert.strictEqual(typeof progressCallback, 'function');
|
assert.strictEqual(typeof progressCallback, 'function');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
@@ -265,58 +571,18 @@ function sync(progressCallback, callback) {
|
|||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled'));
|
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled'));
|
||||||
|
|
||||||
ldapSearch(externalLdapConfig, {}, function (error, ldapUsers) {
|
async.series([
|
||||||
|
syncUsers.bind(null, externalLdapConfig, progressCallback),
|
||||||
|
syncGroups.bind(null, externalLdapConfig, progressCallback),
|
||||||
|
syncGroupUsers.bind(null, externalLdapConfig, progressCallback)
|
||||||
|
], function (error) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
debug(`Found ${ldapUsers.length} users`);
|
progressCallback({ percent: 100, message: 'Done' });
|
||||||
let percent = 10;
|
|
||||||
let step = 90/(ldapUsers.length+1); // ensure no divide by 0
|
|
||||||
|
|
||||||
// we ignore all errors here and just log them for now
|
debug('sync: ldap sync is done', error);
|
||||||
async.eachSeries(ldapUsers, function (user, iteratorCallback) {
|
|
||||||
user = translateUser(externalLdapConfig, user);
|
|
||||||
|
|
||||||
if (!validUserRequirements(user)) return iteratorCallback();
|
callback(error);
|
||||||
|
|
||||||
percent += step;
|
|
||||||
progressCallback({ percent, message: `Syncing... ${user.username}` });
|
|
||||||
|
|
||||||
users.getByUsername(user.username, function (error, result) {
|
|
||||||
if (error && error.reason !== BoxError.NOT_FOUND) {
|
|
||||||
debug(`Could not find user with username ${user.username}: ${error.message}`);
|
|
||||||
return iteratorCallback();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
debug(`[adding user] username=${user.username} email=${user.email} displayName=${user.displayName}`);
|
|
||||||
|
|
||||||
users.create(user.username, null /* password */, user.email, user.displayName, { source: 'ldap' }, auditSource.EXTERNAL_LDAP_TASK, function (error) {
|
|
||||||
if (error) console.error('Failed to create user', user, error);
|
|
||||||
iteratorCallback();
|
|
||||||
});
|
|
||||||
} else if (result.source !== 'ldap') {
|
|
||||||
debug(`[conflicting user] username=${user.username} email=${user.email} displayName=${user.displayName}`);
|
|
||||||
|
|
||||||
iteratorCallback();
|
|
||||||
} else if (result.email !== user.email || result.displayName !== user.displayName) {
|
|
||||||
debug(`[updating user] username=${user.username} email=${user.email} displayName=${user.displayName}`);
|
|
||||||
|
|
||||||
users.update(result, { email: user.email, fallbackEmail: user.email, displayName: user.displayName }, auditSource.EXTERNAL_LDAP_TASK, function (error) {
|
|
||||||
if (error) debug('Failed to update user', user, error);
|
|
||||||
|
|
||||||
iteratorCallback();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// user known and up-to-date
|
|
||||||
debug(`[up-to-date user] username=${user.username} email=${user.email} displayName=${user.displayName}`);
|
|
||||||
|
|
||||||
iteratorCallback();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, function (error) {
|
|
||||||
debug('sync: ldap sync is done', error);
|
|
||||||
callback(error);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-2
@@ -5,6 +5,7 @@ exports = module.exports = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
var assert = require('assert'),
|
var assert = require('assert'),
|
||||||
|
async = require('async'),
|
||||||
infra = require('./infra_version.js'),
|
infra = require('./infra_version.js'),
|
||||||
paths = require('./paths.js'),
|
paths = require('./paths.js'),
|
||||||
shell = require('./shell.js');
|
shell = require('./shell.js');
|
||||||
@@ -26,7 +27,7 @@ function startGraphite(existingInfra, callback) {
|
|||||||
--log-opt syslog-address=udp://127.0.0.1:2514 \
|
--log-opt syslog-address=udp://127.0.0.1:2514 \
|
||||||
--log-opt syslog-format=rfc5424 \
|
--log-opt syslog-format=rfc5424 \
|
||||||
--log-opt tag=graphite \
|
--log-opt tag=graphite \
|
||||||
-m 75m \
|
-m 150m \
|
||||||
--memory-swap 150m \
|
--memory-swap 150m \
|
||||||
--dns 172.18.0.1 \
|
--dns 172.18.0.1 \
|
||||||
--dns-search=. \
|
--dns-search=. \
|
||||||
@@ -37,5 +38,9 @@ function startGraphite(existingInfra, callback) {
|
|||||||
--label isCloudronManaged=true \
|
--label isCloudronManaged=true \
|
||||||
--read-only -v /tmp -v /run "${tag}"`;
|
--read-only -v /tmp -v /run "${tag}"`;
|
||||||
|
|
||||||
shell.exec('startGraphite', cmd, callback);
|
async.series([
|
||||||
|
shell.exec.bind(null, 'stopGraphite', 'docker stop graphite || true'),
|
||||||
|
shell.exec.bind(null, 'removeGraphite', 'docker rm -f graphite || true'),
|
||||||
|
shell.exec.bind(null, 'startGraphite', cmd)
|
||||||
|
], callback);
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-20
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
exports = module.exports = {
|
exports = module.exports = {
|
||||||
get: get,
|
get: get,
|
||||||
|
getByName: getByName,
|
||||||
getWithMembers: getWithMembers,
|
getWithMembers: getWithMembers,
|
||||||
getAll: getAll,
|
getAll: getAll,
|
||||||
getAllWithMembers: getAllWithMembers,
|
getAllWithMembers: getAllWithMembers,
|
||||||
@@ -19,8 +20,6 @@ exports = module.exports = {
|
|||||||
getMembership: getMembership,
|
getMembership: getMembership,
|
||||||
setMembership: setMembership,
|
setMembership: setMembership,
|
||||||
|
|
||||||
getGroups: getGroups,
|
|
||||||
|
|
||||||
_clear: clear
|
_clear: clear
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -28,7 +27,7 @@ var assert = require('assert'),
|
|||||||
BoxError = require('./boxerror.js'),
|
BoxError = require('./boxerror.js'),
|
||||||
database = require('./database.js');
|
database = require('./database.js');
|
||||||
|
|
||||||
var GROUPS_FIELDS = [ 'id', 'name' ].join(',');
|
var GROUPS_FIELDS = [ 'id', 'name', 'source' ].join(',');
|
||||||
|
|
||||||
function get(groupId, callback) {
|
function get(groupId, callback) {
|
||||||
assert.strictEqual(typeof groupId, 'string');
|
assert.strictEqual(typeof groupId, 'string');
|
||||||
@@ -42,6 +41,18 @@ function get(groupId, callback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getByName(name, callback) {
|
||||||
|
assert.strictEqual(typeof name, 'string');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
database.query('SELECT ' + GROUPS_FIELDS + ' FROM userGroups WHERE name = ?', [ name ], function (error, result) {
|
||||||
|
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||||
|
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Group not found'));
|
||||||
|
|
||||||
|
callback(null, result[0]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getWithMembers(groupId, callback) {
|
function getWithMembers(groupId, callback) {
|
||||||
assert.strictEqual(typeof groupId, 'string');
|
assert.strictEqual(typeof groupId, 'string');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
@@ -63,7 +74,7 @@ function getWithMembers(groupId, callback) {
|
|||||||
function getAll(callback) {
|
function getAll(callback) {
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
database.query('SELECT ' + GROUPS_FIELDS + ' FROM userGroups', function (error, results) {
|
database.query('SELECT ' + GROUPS_FIELDS + ' FROM userGroups ORDER BY name', function (error, results) {
|
||||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||||
|
|
||||||
callback(null, results);
|
callback(null, results);
|
||||||
@@ -73,9 +84,8 @@ function getAll(callback) {
|
|||||||
function getAllWithMembers(callback) {
|
function getAllWithMembers(callback) {
|
||||||
database.query('SELECT ' + GROUPS_FIELDS + ',GROUP_CONCAT(groupMembers.userId) AS userIds ' +
|
database.query('SELECT ' + GROUPS_FIELDS + ',GROUP_CONCAT(groupMembers.userId) AS userIds ' +
|
||||||
' FROM userGroups LEFT OUTER JOIN groupMembers ON userGroups.id = groupMembers.groupId ' +
|
' FROM userGroups LEFT OUTER JOIN groupMembers ON userGroups.id = groupMembers.groupId ' +
|
||||||
' GROUP BY userGroups.id', function (error, results) {
|
' GROUP BY userGroups.id ORDER BY name', function (error, results) {
|
||||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||||
if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Group not found'));
|
|
||||||
|
|
||||||
results.forEach(function (result) { result.userIds = result.userIds ? result.userIds.split(',') : [ ]; });
|
results.forEach(function (result) { result.userIds = result.userIds ? result.userIds.split(',') : [ ]; });
|
||||||
|
|
||||||
@@ -83,12 +93,13 @@ function getAllWithMembers(callback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function add(id, name, callback) {
|
function add(id, name, source, callback) {
|
||||||
assert.strictEqual(typeof id, 'string');
|
assert.strictEqual(typeof id, 'string');
|
||||||
assert.strictEqual(typeof name, 'string');
|
assert.strictEqual(typeof name, 'string');
|
||||||
|
assert.strictEqual(typeof source, 'string');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
database.query('INSERT INTO userGroups (id, name) VALUES (?, ?)', [ id, name ], function (error, result) {
|
database.query('INSERT INTO userGroups (id, name, source) VALUES (?, ?, ?)', [ id, name, source ], function (error, result) {
|
||||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, error));
|
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, error));
|
||||||
if (error || result.affectedRows !== 1) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
if (error || result.affectedRows !== 1) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||||
|
|
||||||
@@ -260,15 +271,3 @@ function isMember(groupId, userId, callback) {
|
|||||||
callback(null, result.length !== 0);
|
callback(null, result.length !== 0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGroups(userId, callback) {
|
|
||||||
assert.strictEqual(typeof userId, 'string');
|
|
||||||
assert.strictEqual(typeof callback, 'function');
|
|
||||||
|
|
||||||
database.query('SELECT ' + GROUPS_FIELDS + ' ' +
|
|
||||||
' FROM userGroups INNER JOIN groupMembers ON userGroups.id = groupMembers.groupId AND groupMembers.userId = ?', [ userId ], function (error, results) {
|
|
||||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
|
||||||
|
|
||||||
callback(null, results);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
+26
-15
@@ -4,6 +4,7 @@ exports = module.exports = {
|
|||||||
create: create,
|
create: create,
|
||||||
remove: remove,
|
remove: remove,
|
||||||
get: get,
|
get: get,
|
||||||
|
getByName: getByName,
|
||||||
update: update,
|
update: update,
|
||||||
getWithMembers: getWithMembers,
|
getWithMembers: getWithMembers,
|
||||||
getAll: getAll,
|
getAll: getAll,
|
||||||
@@ -15,8 +16,6 @@ exports = module.exports = {
|
|||||||
removeMember: removeMember,
|
removeMember: removeMember,
|
||||||
isMember: isMember,
|
isMember: isMember,
|
||||||
|
|
||||||
getGroups: getGroups,
|
|
||||||
|
|
||||||
setMembership: setMembership,
|
setMembership: setMembership,
|
||||||
getMembership: getMembership,
|
getMembership: getMembership,
|
||||||
|
|
||||||
@@ -45,8 +44,17 @@ function validateGroupname(name) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function create(name, callback) {
|
function validateGroupSource(source) {
|
||||||
|
assert.strictEqual(typeof source, 'string');
|
||||||
|
|
||||||
|
if (source !== '' && source !== 'ldap') return new BoxError(BoxError.BAD_FIELD, 'source must be "" or "ldap"', { field: source });
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function create(name, source, callback) {
|
||||||
assert.strictEqual(typeof name, 'string');
|
assert.strictEqual(typeof name, 'string');
|
||||||
|
assert.strictEqual(typeof source, 'string');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
// we store names in lowercase
|
// we store names in lowercase
|
||||||
@@ -55,8 +63,11 @@ function create(name, callback) {
|
|||||||
var error = validateGroupname(name);
|
var error = validateGroupname(name);
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
error = validateGroupSource(source);
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
var id = 'gid-' + uuid.v4();
|
var id = 'gid-' + uuid.v4();
|
||||||
groupdb.add(id, name, function (error) {
|
groupdb.add(id, name, source, function (error) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
callback(null, { id: id, name: name });
|
callback(null, { id: id, name: name });
|
||||||
@@ -85,6 +96,17 @@ function get(id, callback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getByName(name, callback) {
|
||||||
|
assert.strictEqual(typeof name, 'string');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
groupdb.getByName(name, function (error, result) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
return callback(null, result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getWithMembers(id, callback) {
|
function getWithMembers(id, callback) {
|
||||||
assert.strictEqual(typeof id, 'string');
|
assert.strictEqual(typeof id, 'string');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
@@ -217,17 +239,6 @@ function update(groupId, data, callback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGroups(userId, callback) {
|
|
||||||
assert.strictEqual(typeof userId, 'string');
|
|
||||||
assert.strictEqual(typeof callback, 'function');
|
|
||||||
|
|
||||||
groupdb.getGroups(userId, function (error, results) {
|
|
||||||
if (error) return callback(error);
|
|
||||||
|
|
||||||
callback(null, results);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function count(callback) {
|
function count(callback) {
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
|||||||
@@ -9,19 +9,19 @@ exports = module.exports = {
|
|||||||
'version': '48.17.0',
|
'version': '48.17.0',
|
||||||
|
|
||||||
'baseImages': [
|
'baseImages': [
|
||||||
{ repo: 'cloudron/base', tag: 'cloudron/base:1.0.0@sha256:147a648a068a2e746644746bbfb42eb7a50d682437cead3c67c933c546357617' }
|
{ repo: 'cloudron/base', tag: 'cloudron/base:2.0.0@sha256:f9fea80513aa7c92fe2e7bf3978b54c8ac5222f47a9a32a7f8833edf0eb5a4f4' }
|
||||||
],
|
],
|
||||||
|
|
||||||
// a major version bump in the db containers will trigger the restore logic that uses the db dumps
|
// a major version bump in the db containers will trigger the restore logic that uses the db dumps
|
||||||
// docker inspect --format='{{index .RepoDigests 0}}' $IMAGE to get the sha256
|
// docker inspect --format='{{index .RepoDigests 0}}' $IMAGE to get the sha256
|
||||||
'images': {
|
'images': {
|
||||||
'turn': { repo: 'cloudron/turn', tag: 'cloudron/turn:1.0.2@sha256:2643b73fe371154e37647957cc7103cacb34c50737f2954abd7d70f167a1f33a' },
|
'turn': { repo: 'cloudron/turn', tag: 'cloudron/turn:1.1.0@sha256:e1dd22aa6eef5beb7339834b200a8bb787ffc2264ce11139857a054108fefb4f' },
|
||||||
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:2.2.0@sha256:440c8a9ca4d2958d51a375359f8158ef702b83395aa9ac4f450c51825ec09239' },
|
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:2.3.1@sha256:c1145d43c8a912fe6f5a5629a4052454a4aa6f23391c1efbffeec9d12d72a256' },
|
||||||
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:2.0.2@sha256:6dcee0731dfb9b013ed94d56205eee219040ee806c7e251db3b3886eaa4947ff' },
|
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:3.0.0@sha256:b00e5118a8f829c422234117bf113803be79a1d5102c51497c6d3005b041ce37' },
|
||||||
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:2.1.0@sha256:6d1bf221cfe6124957e2c58b57c0a47214353496009296acb16adf56df1da9d5' },
|
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:3.0.0@sha256:59e50b1f55e433ffdf6d678f8c658812b4119f631db8325572a52ee40d3bc562' },
|
||||||
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.1.0@sha256:f2cda21bd15c21bbf44432df412525369ef831a2d53860b5c5b1675e6f384de2' },
|
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.3.0@sha256:0e31ec817e235b1814c04af97b1e7cf0053384aca2569570ce92bef0d95e94d2' },
|
||||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.7.2@sha256:f20d112ff9a97e052a9187063eabbd8d484ce369114d44186e344169a1b3ef6b' },
|
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.9.4@sha256:0e169b97a0584a76197d2bbc039d8698bf93f815588b3b43c251bd83dd545465' },
|
||||||
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.2.0@sha256:fc9ca69d16e6ebdbd98ed53143d4a0d2212eef60cb638dc71219234e6f427a2c' },
|
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.3.0@sha256:b7bc1ca4f4d0603a01369a689129aa273a938ce195fe43d00d42f4f2d5212f50' },
|
||||||
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:1.0.0@sha256:3b70aac36700225945a4a39b5a400c28e010e980879d0dcca76e4a37b04a16ed' }
|
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:2.0.2@sha256:cbd604eaa970c99ba5c4c2e7984929668e05de824172f880e8c576b2fb7c976d' }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+5
-21
@@ -16,21 +16,15 @@ const NOOP_CALLBACK = function () { };
|
|||||||
|
|
||||||
const gConnection = new Docker({ socketPath: '/var/run/docker.sock' });
|
const gConnection = new Docker({ socketPath: '/var/run/docker.sock' });
|
||||||
|
|
||||||
function ignoreError(func) {
|
function cleanupTokens(callback) {
|
||||||
return function (callback) {
|
assert(!callback || typeof callback === 'function'); // callback is null when called from cronjob
|
||||||
func(function (error) {
|
|
||||||
if (error) console.error('Ignored error:', error);
|
|
||||||
|
|
||||||
callback();
|
callback = callback || NOOP_CALLBACK;
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanupExpiredTokens(callback) {
|
debug('Cleaning up expired tokens');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
|
||||||
|
|
||||||
tokendb.delExpired(function (error, result) {
|
tokendb.delExpired(function (error, result) {
|
||||||
if (error) return callback(error);
|
if (error) return debug('cleanupTokens: error removing expired tokens', error);
|
||||||
|
|
||||||
debug('Cleaned up %s expired tokens.', result);
|
debug('Cleaned up %s expired tokens.', result);
|
||||||
|
|
||||||
@@ -38,16 +32,6 @@ function cleanupExpiredTokens(callback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanupTokens(callback) {
|
|
||||||
assert(!callback || typeof callback === 'function'); // callback is null when called from cronjob
|
|
||||||
|
|
||||||
debug('Cleaning up expired tokens');
|
|
||||||
|
|
||||||
async.series([
|
|
||||||
ignoreError(cleanupExpiredTokens)
|
|
||||||
], callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanupTmpVolume(containerInfo, callback) {
|
function cleanupTmpVolume(containerInfo, callback) {
|
||||||
assert.strictEqual(typeof containerInfo, 'object');
|
assert.strictEqual(typeof containerInfo, 'object');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|||||||
+9
-12
@@ -154,7 +154,6 @@ function userSearch(req, res, next) {
|
|||||||
givenName: firstName,
|
givenName: firstName,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
samaccountname: user.username, // to support ActiveDirectory clients
|
samaccountname: user.username, // to support ActiveDirectory clients
|
||||||
isadmin: users.compareRoles(user.role, users.ROLE_ADMIN) >= 0,
|
|
||||||
memberof: groups
|
memberof: groups
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -347,7 +346,7 @@ function mailboxSearch(req, res, next) {
|
|||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
aliases.forEach(function (a, idx) {
|
aliases.forEach(function (a, idx) {
|
||||||
obj.attributes['mail' + idx] = `${a}@${mailbox.domain}`;
|
obj.attributes['mail' + idx] = `${a.name}@${a.domain}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
// ensure all filter values are also lowercase
|
// ensure all filter values are also lowercase
|
||||||
@@ -392,7 +391,7 @@ function mailAliasSearch(req, res, next) {
|
|||||||
objectclass: ['nisMailAlias'],
|
objectclass: ['nisMailAlias'],
|
||||||
objectcategory: 'nisMailAlias',
|
objectcategory: 'nisMailAlias',
|
||||||
cn: `${alias.name}@${alias.domain}`,
|
cn: `${alias.name}@${alias.domain}`,
|
||||||
rfc822MailMember: `${alias.aliasTarget}@${alias.domain}`
|
rfc822MailMember: `${alias.aliasName}@${alias.aliasDomain}`
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -418,7 +417,7 @@ function mailingListSearch(req, res, next) {
|
|||||||
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||||
const name = parts[0], domain = parts[1];
|
const name = parts[0], domain = parts[1];
|
||||||
|
|
||||||
mail.resolveList(parts[0], parts[1], function (error, resolvedMembers) {
|
mail.resolveList(parts[0], parts[1], function (error, resolvedMembers, list) {
|
||||||
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||||
if (error) return next(new ldap.OperationsError(error.toString()));
|
if (error) return next(new ldap.OperationsError(error.toString()));
|
||||||
|
|
||||||
@@ -431,6 +430,7 @@ function mailingListSearch(req, res, next) {
|
|||||||
objectcategory: 'mailGroup',
|
objectcategory: 'mailGroup',
|
||||||
cn: `${name}@${domain}`, // fully qualified
|
cn: `${name}@${domain}`, // fully qualified
|
||||||
mail: `${name}@${domain}`,
|
mail: `${name}@${domain}`,
|
||||||
|
membersOnly: list.membersOnly, // ldapjs only supports strings and string array. so this is not a bool!
|
||||||
mgrpRFC822MailMember: resolvedMembers // fully qualified
|
mgrpRFC822MailMember: resolvedMembers // fully qualified
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -577,7 +577,7 @@ function userSearchSftp(req, res, next) {
|
|||||||
var obj = {
|
var obj = {
|
||||||
dn: ldap.parseDN(`cn=${username}@${appFqdn},ou=sftp,dc=cloudron`).toString(),
|
dn: ldap.parseDN(`cn=${username}@${appFqdn},ou=sftp,dc=cloudron`).toString(),
|
||||||
attributes: {
|
attributes: {
|
||||||
homeDirectory: path.join('/app/data', app.id, 'data'),
|
homeDirectory: path.join('/app/data', app.id),
|
||||||
objectclass: ['user'],
|
objectclass: ['user'],
|
||||||
objectcategory: 'person',
|
objectcategory: 'person',
|
||||||
cn: user.id,
|
cn: user.id,
|
||||||
@@ -618,10 +618,7 @@ function authenticateMailAddon(req, res, next) {
|
|||||||
// note: with sendmail addon, apps can send mail without a mailbox (unlike users)
|
// note: with sendmail addon, apps can send mail without a mailbox (unlike users)
|
||||||
appdb.getAppIdByAddonConfigValue(addonId, namePattern, req.credentials || '', function (error, appId) {
|
appdb.getAppIdByAddonConfigValue(addonId, namePattern, req.credentials || '', function (error, appId) {
|
||||||
if (error && error.reason !== BoxError.NOT_FOUND) return next(new ldap.OperationsError(error.message));
|
if (error && error.reason !== BoxError.NOT_FOUND) return next(new ldap.OperationsError(error.message));
|
||||||
if (appId) { // matched app password
|
if (appId) return res.end();
|
||||||
eventlog.add(eventlog.ACTION_APP_LOGIN, { authType: 'ldap', mailboxId: email }, { appId: appId, addonId: addonId });
|
|
||||||
return res.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
mailboxdb.getMailbox(parts[0], parts[1], function (error, mailbox) {
|
mailboxdb.getMailbox(parts[0], parts[1], function (error, mailbox) {
|
||||||
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||||
@@ -648,14 +645,14 @@ function start(callback) {
|
|||||||
debug: NOOP,
|
debug: NOOP,
|
||||||
info: debug,
|
info: debug,
|
||||||
warn: debug,
|
warn: debug,
|
||||||
error: console.error,
|
error: debug,
|
||||||
fatal: console.error
|
fatal: debug
|
||||||
};
|
};
|
||||||
|
|
||||||
gServer = ldap.createServer({ log: logger });
|
gServer = ldap.createServer({ log: logger });
|
||||||
|
|
||||||
gServer.on('error', function (error) {
|
gServer.on('error', function (error) {
|
||||||
console.error('LDAP:', error);
|
debug('start: server error ', error);
|
||||||
});
|
});
|
||||||
|
|
||||||
gServer.search('ou=users,dc=cloudron', authenticateApp, userSearch);
|
gServer.search('ou=users,dc=cloudron', authenticateApp, userSearch);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
maxsize 1M
|
maxsize 1M
|
||||||
missingok
|
missingok
|
||||||
delaycompress
|
delaycompress
|
||||||
|
# this truncates the original log file and not the rotated one
|
||||||
copytruncate
|
copytruncate
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@
|
|||||||
missingok
|
missingok
|
||||||
# we never compress so we can simply tail the files
|
# we never compress so we can simply tail the files
|
||||||
nocompress
|
nocompress
|
||||||
|
# this truncates the original log file and not the rotated one
|
||||||
copytruncate
|
copytruncate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+96
-73
@@ -1,54 +1,54 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
exports = module.exports = {
|
exports = module.exports = {
|
||||||
getStatus: getStatus,
|
getStatus,
|
||||||
checkConfiguration: checkConfiguration,
|
checkConfiguration,
|
||||||
|
|
||||||
getDomains: getDomains,
|
getDomains,
|
||||||
|
|
||||||
getDomain: getDomain,
|
getDomain,
|
||||||
clearDomains: clearDomains,
|
clearDomains,
|
||||||
|
|
||||||
onDomainAdded: onDomainAdded,
|
onDomainAdded,
|
||||||
onDomainRemoved: onDomainRemoved,
|
onDomainRemoved,
|
||||||
|
onMailFqdnChanged,
|
||||||
|
|
||||||
removePrivateFields: removePrivateFields,
|
removePrivateFields,
|
||||||
|
|
||||||
setDnsRecords: setDnsRecords,
|
setDnsRecords,
|
||||||
onMailFqdnChanged: onMailFqdnChanged,
|
|
||||||
|
|
||||||
validateName: validateName,
|
validateName,
|
||||||
|
|
||||||
setMailFromValidation: setMailFromValidation,
|
setMailFromValidation,
|
||||||
setCatchAllAddress: setCatchAllAddress,
|
setCatchAllAddress,
|
||||||
setMailRelay: setMailRelay,
|
setMailRelay,
|
||||||
setMailEnabled: setMailEnabled,
|
setMailEnabled,
|
||||||
|
|
||||||
startMail: restartMail,
|
startMail: restartMail,
|
||||||
restartMail: restartMail,
|
restartMail,
|
||||||
handleCertChanged: handleCertChanged,
|
handleCertChanged,
|
||||||
getMailAuth: getMailAuth,
|
getMailAuth,
|
||||||
|
|
||||||
sendTestMail: sendTestMail,
|
sendTestMail,
|
||||||
|
|
||||||
listMailboxes: listMailboxes,
|
getMailboxCount,
|
||||||
removeMailboxes: removeMailboxes,
|
listMailboxes,
|
||||||
getMailbox: getMailbox,
|
getMailbox,
|
||||||
addMailbox: addMailbox,
|
addMailbox,
|
||||||
updateMailboxOwner: updateMailboxOwner,
|
updateMailboxOwner,
|
||||||
removeMailbox: removeMailbox,
|
removeMailbox,
|
||||||
|
|
||||||
listAliases: listAliases,
|
getAliases,
|
||||||
getAliases: getAliases,
|
setAliases,
|
||||||
setAliases: setAliases,
|
|
||||||
|
|
||||||
getLists: getLists,
|
getLists,
|
||||||
getList: getList,
|
getList,
|
||||||
addList: addList,
|
addList,
|
||||||
updateList: updateList,
|
updateList,
|
||||||
removeList: removeList,
|
removeList,
|
||||||
resolveList: resolveList,
|
resolveList,
|
||||||
|
|
||||||
|
_removeMailboxes: removeMailboxes,
|
||||||
_readDkimPublicKeySync: readDkimPublicKeySync
|
_readDkimPublicKeySync: readDkimPublicKeySync
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -82,6 +82,7 @@ var assert = require('assert'),
|
|||||||
|
|
||||||
const DNS_OPTIONS = { timeout: 5000 };
|
const DNS_OPTIONS = { timeout: 5000 };
|
||||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||||
|
const REMOVE_MAILBOX = path.join(__dirname, 'scripts/rmmailbox.sh');
|
||||||
|
|
||||||
function validateName(name) {
|
function validateName(name) {
|
||||||
assert.strictEqual(typeof name, 'string');
|
assert.strictEqual(typeof name, 'string');
|
||||||
@@ -101,7 +102,6 @@ function checkOutboundPort25(callback) {
|
|||||||
var smtpServer = _.sample([
|
var smtpServer = _.sample([
|
||||||
'smtp.gmail.com',
|
'smtp.gmail.com',
|
||||||
'smtp.live.com',
|
'smtp.live.com',
|
||||||
'smtp.mail.yahoo.com',
|
|
||||||
'smtp.1und1.de',
|
'smtp.1und1.de',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -208,7 +208,8 @@ function checkDkim(mailDomain, callback) {
|
|||||||
|
|
||||||
if (txtRecords.length !== 0) {
|
if (txtRecords.length !== 0) {
|
||||||
dkim.value = txtRecords[0].join('');
|
dkim.value = txtRecords[0].join('');
|
||||||
dkim.status = (dkim.value === dkim.expected);
|
const actual = txtToDict(dkim.value);
|
||||||
|
dkim.status = actual.p === dkimKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
callback(null, dkim);
|
callback(null, dkim);
|
||||||
@@ -269,7 +270,7 @@ function checkMx(domain, mailFqdn, callback) {
|
|||||||
if (error) return callback(error, mx);
|
if (error) return callback(error, mx);
|
||||||
if (mxRecords.length === 0) return callback(null, mx);
|
if (mxRecords.length === 0) return callback(null, mx);
|
||||||
|
|
||||||
mx.status = mxRecords.length == 1 && mxRecords[0].exchange === mailFqdn;
|
mx.status = mxRecords.some(mx => mx.exchange === mailFqdn); // this lets use change priority and/or setup backup MX
|
||||||
mx.value = mxRecords.map(function (r) { return r.priority + ' ' + r.exchange + '.'; }).join(' ');
|
mx.value = mxRecords.map(function (r) { return r.priority + ' ' + r.exchange + '.'; }).join(' ');
|
||||||
|
|
||||||
if (mx.status) return callback(null, mx); // MX record is "my."
|
if (mx.status) return callback(null, mx); // MX record is "my."
|
||||||
@@ -629,7 +630,10 @@ function configureMail(mailFqdn, mailDomain, callback) {
|
|||||||
if (!safe.child_process.execSync(`cp ${bundle.certFilePath} ${mailCertFilePath}`)) return callback(new BoxError(BoxError.FS_ERROR, 'Could not create cert file:' + safe.error.message));
|
if (!safe.child_process.execSync(`cp ${bundle.certFilePath} ${mailCertFilePath}`)) return callback(new BoxError(BoxError.FS_ERROR, 'Could not create cert file:' + safe.error.message));
|
||||||
if (!safe.child_process.execSync(`cp ${bundle.keyFilePath} ${mailKeyFilePath}`)) return callback(new BoxError(BoxError.FS_ERROR, 'Could not create key file:' + safe.error.message));
|
if (!safe.child_process.execSync(`cp ${bundle.keyFilePath} ${mailKeyFilePath}`)) return callback(new BoxError(BoxError.FS_ERROR, 'Could not create key file:' + safe.error.message));
|
||||||
|
|
||||||
shell.exec('startMail', 'docker rm -f mail || true', function (error) {
|
async.series([
|
||||||
|
shell.exec.bind(null, 'stopMail', 'docker stop mail || true'),
|
||||||
|
shell.exec.bind(null, 'removeMail', 'docker rm -f mail || true'),
|
||||||
|
], function (error) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
createMailConfig(mailFqdn, mailDomain, function (error, allowInbound) {
|
createMailConfig(mailFqdn, mailDomain, function (error, allowInbound) {
|
||||||
@@ -798,6 +802,7 @@ function ensureDkimKeySync(mailDomain) {
|
|||||||
return new BoxError(BoxError.FS_ERROR, safe.error);
|
return new BoxError(BoxError.FS_ERROR, safe.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://www.unlocktheinbox.com/dkim-key-length-statistics/ and https://docs.aws.amazon.com/ses/latest/DeveloperGuide/send-email-authentication-dkim-easy.html for key size
|
||||||
if (!safe.child_process.execSync('openssl genrsa -out ' + dkimPrivateKeyFile + ' 1024')) return new BoxError(BoxError.OPENSSL_ERROR, safe.error);
|
if (!safe.child_process.execSync('openssl genrsa -out ' + dkimPrivateKeyFile + ' 1024')) return new BoxError(BoxError.OPENSSL_ERROR, safe.error);
|
||||||
if (!safe.child_process.execSync('openssl rsa -in ' + dkimPrivateKeyFile + ' -out ' + dkimPublicKeyFile + ' -pubout -outform PEM')) return new BoxError(BoxError.OPENSSL_ERROR, safe.error);
|
if (!safe.child_process.execSync('openssl rsa -in ' + dkimPrivateKeyFile + ' -out ' + dkimPublicKeyFile + ' -pubout -outform PEM')) return new BoxError(BoxError.OPENSSL_ERROR, safe.error);
|
||||||
|
|
||||||
@@ -1031,13 +1036,25 @@ function sendTestMail(domain, to, callback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function listMailboxes(domain, page, perPage, callback) {
|
function listMailboxes(domain, search, page, perPage, callback) {
|
||||||
assert.strictEqual(typeof domain, 'string');
|
assert.strictEqual(typeof domain, 'string');
|
||||||
|
assert(typeof search === 'string' || search === null);
|
||||||
assert.strictEqual(typeof page, 'number');
|
assert.strictEqual(typeof page, 'number');
|
||||||
assert.strictEqual(typeof perPage, 'number');
|
assert.strictEqual(typeof perPage, 'number');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
mailboxdb.listMailboxes(domain, page, perPage, function (error, result) {
|
mailboxdb.listMailboxes(domain, search, page, perPage, function (error, result) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
callback(null, result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMailboxCount(domain, callback) {
|
||||||
|
assert.strictEqual(typeof domain, 'string');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
mailboxdb.getMailboxCount(domain, function (error, result) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
callback(null, result);
|
callback(null, result);
|
||||||
@@ -1110,31 +1127,25 @@ function updateMailboxOwner(name, domain, userId, auditSource, callback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeMailbox(name, domain, auditSource, callback) {
|
function removeMailbox(name, domain, options, auditSource, callback) {
|
||||||
assert.strictEqual(typeof domain, 'string');
|
assert.strictEqual(typeof domain, 'string');
|
||||||
assert.strictEqual(typeof name, 'string');
|
assert.strictEqual(typeof name, 'string');
|
||||||
|
assert.strictEqual(typeof options, 'object');
|
||||||
assert.strictEqual(typeof auditSource, 'object');
|
assert.strictEqual(typeof auditSource, 'object');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
mailboxdb.del(name, domain, function (error) {
|
const deleteMailFunc = options.deleteMails ? shell.sudo.bind(null, 'removeMailbox', [ REMOVE_MAILBOX, `${name}@${domain}` ], {}) : (next) => next();
|
||||||
if (error) return callback(error);
|
|
||||||
|
|
||||||
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_REMOVE, auditSource, { name, domain });
|
deleteMailFunc(function (error) {
|
||||||
|
if (error) return callback(new BoxError(BoxError.FS_ERROR, `Error removing mailbox: ${error.message}`));
|
||||||
|
|
||||||
callback(null);
|
mailboxdb.del(name, domain, function (error) {
|
||||||
});
|
if (error) return callback(error);
|
||||||
}
|
|
||||||
|
|
||||||
function listAliases(domain, page, perPage, callback) {
|
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_REMOVE, auditSource, { name, domain });
|
||||||
assert.strictEqual(typeof domain, 'string');
|
|
||||||
assert.strictEqual(typeof page, 'number');
|
|
||||||
assert.strictEqual(typeof perPage, 'number');
|
|
||||||
assert.strictEqual(typeof callback, 'function');
|
|
||||||
|
|
||||||
mailboxdb.listAliases(domain, page, perPage, function (error, result) {
|
callback();
|
||||||
if (error) return callback(error);
|
});
|
||||||
|
|
||||||
callback(null, result);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1161,12 +1172,15 @@ function setAliases(name, domain, aliases, callback) {
|
|||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
for (var i = 0; i < aliases.length; i++) {
|
for (var i = 0; i < aliases.length; i++) {
|
||||||
aliases[i] = aliases[i].toLowerCase();
|
let name = aliases[i].name.toLowerCase();
|
||||||
|
let domain = aliases[i].domain.toLowerCase();
|
||||||
|
|
||||||
var error = validateName(aliases[i]);
|
let error = validateName(name);
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
}
|
|
||||||
|
|
||||||
|
if (!validator.isEmail(`${name}@${domain}`)) return callback(new BoxError(BoxError.BAD_FIELD, `Invalid email: ${name}@${domain}`));
|
||||||
|
aliases[i] = { name, domain };
|
||||||
|
}
|
||||||
mailboxdb.setAliasesForName(name, domain, aliases, function (error) {
|
mailboxdb.setAliasesForName(name, domain, aliases, function (error) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
@@ -1174,11 +1188,14 @@ function setAliases(name, domain, aliases, callback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLists(domain, callback) {
|
function getLists(domain, search, page, perPage, callback) {
|
||||||
assert.strictEqual(typeof domain, 'string');
|
assert.strictEqual(typeof domain, 'string');
|
||||||
|
assert(typeof search === 'string' || search === null);
|
||||||
|
assert.strictEqual(typeof page, 'number');
|
||||||
|
assert.strictEqual(typeof perPage, 'number');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
mailboxdb.getLists(domain, function (error, result) {
|
mailboxdb.getLists(domain, search, page, perPage, function (error, result) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
callback(null, result);
|
callback(null, result);
|
||||||
@@ -1197,10 +1214,11 @@ function getList(name, domain, callback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function addList(name, domain, members, auditSource, callback) {
|
function addList(name, domain, members, membersOnly, auditSource, callback) {
|
||||||
assert.strictEqual(typeof domain, 'string');
|
assert.strictEqual(typeof domain, 'string');
|
||||||
assert.strictEqual(typeof name, 'string');
|
assert.strictEqual(typeof name, 'string');
|
||||||
assert(Array.isArray(members));
|
assert(Array.isArray(members));
|
||||||
|
assert.strictEqual(typeof membersOnly, 'boolean');
|
||||||
assert.strictEqual(typeof auditSource, 'object');
|
assert.strictEqual(typeof auditSource, 'object');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
@@ -1213,19 +1231,20 @@ function addList(name, domain, members, auditSource, callback) {
|
|||||||
if (!validator.isEmail(members[i])) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid mail member: ' + members[i]));
|
if (!validator.isEmail(members[i])) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid mail member: ' + members[i]));
|
||||||
}
|
}
|
||||||
|
|
||||||
mailboxdb.addList(name, domain, members, function (error) {
|
mailboxdb.addList(name, domain, members, membersOnly, function (error) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
eventlog.add(eventlog.ACTION_MAIL_LIST_ADD, auditSource, { name, domain, members });
|
eventlog.add(eventlog.ACTION_MAIL_LIST_ADD, auditSource, { name, domain, members, membersOnly });
|
||||||
|
|
||||||
callback();
|
callback();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateList(name, domain, members, auditSource, callback) {
|
function updateList(name, domain, members, membersOnly, auditSource, callback) {
|
||||||
assert.strictEqual(typeof name, 'string');
|
assert.strictEqual(typeof name, 'string');
|
||||||
assert.strictEqual(typeof domain, 'string');
|
assert.strictEqual(typeof domain, 'string');
|
||||||
assert(Array.isArray(members));
|
assert(Array.isArray(members));
|
||||||
|
assert.strictEqual(typeof membersOnly, 'boolean');
|
||||||
assert.strictEqual(typeof auditSource, 'object');
|
assert.strictEqual(typeof auditSource, 'object');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
@@ -1241,10 +1260,10 @@ function updateList(name, domain, members, auditSource, callback) {
|
|||||||
getList(name, domain, function (error, result) {
|
getList(name, domain, function (error, result) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
mailboxdb.updateList(name, domain, members, function (error) {
|
mailboxdb.updateList(name, domain, members, membersOnly, function (error) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_UPDATE, auditSource, { name, domain, oldMembers: result.members, members });
|
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_UPDATE, auditSource, { name, domain, oldMembers: result.members, members, membersOnly });
|
||||||
|
|
||||||
callback(null);
|
callback(null);
|
||||||
});
|
});
|
||||||
@@ -1266,6 +1285,7 @@ function removeList(name, domain, auditSource, callback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resolves the members of a list. i.e the lists and aliases
|
||||||
function resolveList(listName, listDomain, callback) {
|
function resolveList(listName, listDomain, callback) {
|
||||||
assert.strictEqual(typeof listName, 'string');
|
assert.strictEqual(typeof listName, 'string');
|
||||||
assert.strictEqual(typeof listDomain, 'string');
|
assert.strictEqual(typeof listDomain, 'string');
|
||||||
@@ -1296,18 +1316,21 @@ function resolveList(listName, listDomain, callback) {
|
|||||||
visited.push(member);
|
visited.push(member);
|
||||||
|
|
||||||
mailboxdb.get(memberName, memberDomain, function (error, entry) {
|
mailboxdb.get(memberName, memberDomain, function (error, entry) {
|
||||||
if (error && error.reason == BoxError.NOT_FOUND) { result.push(member); return iteratorCallback(); }
|
if (error && error.reason == BoxError.NOT_FOUND) { result.push(member); return iteratorCallback(); } // let it bounce
|
||||||
if (error) return iteratorCallback(error);
|
if (error) return iteratorCallback(error);
|
||||||
|
|
||||||
if (entry.type === mailboxdb.TYPE_MAILBOX) { result.push(member); return iteratorCallback(); }
|
if (entry.type === mailboxdb.TYPE_MAILBOX) { // concrete mailbox
|
||||||
// no need to resolve alias because we only allow one level and within same domain
|
result.push(member);
|
||||||
if (entry.type === mailboxdb.TYPE_ALIAS) { result.push(`${entry.aliasTarget}@${entry.domain}`); return iteratorCallback(); }
|
} else if (entry.type === mailboxdb.TYPE_ALIAS) { // resolve aliases
|
||||||
|
toResolve = toResolve.concat(`${entry.aliasName}@${entry.aliasDomain}`);
|
||||||
|
} else { // resolve list members
|
||||||
|
toResolve = toResolve.concat(entry.members);
|
||||||
|
}
|
||||||
|
|
||||||
toResolve = toResolve.concat(entry.members);
|
|
||||||
iteratorCallback();
|
iteratorCallback();
|
||||||
});
|
});
|
||||||
}, function (error) {
|
}, function (error) {
|
||||||
callback(error, result);
|
callback(error, result, list);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Dear <%= cloudronName %> Admin,
|
|||||||
|
|
||||||
If this message appears repeatedly, give the app more memory.
|
If this message appears repeatedly, give the app more memory.
|
||||||
|
|
||||||
* To increase an app's memory limit - https://cloudron.io/documentation/apps/#increasing-the-memory-limit-of-an-app
|
* To increase an app's memory limit - https://cloudron.io/documentation/apps/#memory-limit
|
||||||
* To increase a service's memory limit - https://cloudron.io/documentation/troubleshooting/#services
|
* To increase a service's memory limit - https://cloudron.io/documentation/troubleshooting/#services
|
||||||
|
|
||||||
Out of memory event:
|
Out of memory event:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ be reset. If you did not request this reset, please ignore this message.
|
|||||||
To reset your password, please visit the following page:
|
To reset your password, please visit the following page:
|
||||||
<%- resetLink %>
|
<%- resetLink %>
|
||||||
|
|
||||||
|
Please note that the password reset link will expire in 24 hours.
|
||||||
|
|
||||||
Powered by https://cloudron.io
|
Powered by https://cloudron.io
|
||||||
|
|
||||||
@@ -29,6 +29,10 @@ Powered by https://cloudron.io
|
|||||||
<a href="<%= resetLink %>">Click to reset your password</a>
|
<a href="<%= resetLink %>">Click to reset your password</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
Please note that the password reset link will expire in 24 hours.
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ Follow the link to get started.
|
|||||||
You are receiving this email because you were invited by <%= invitor.email %>.
|
You are receiving this email because you were invited by <%= invitor.email %>.
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
|
Please note that the invite link will expire in 7 days.
|
||||||
|
|
||||||
Powered by https://cloudron.io
|
Powered by https://cloudron.io
|
||||||
|
|
||||||
@@ -36,6 +37,9 @@ Powered by https://cloudron.io
|
|||||||
You are receiving this email because you were invited by <%= invitor.email %>.
|
You are receiving this email because you were invited by <%= invitor.email %>.
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
Please note that the invite link will expire in 7 days.
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
Powered by <a href="https://cloudron.io">Cloudron</a>
|
Powered by <a href="https://cloudron.io">Cloudron</a>
|
||||||
|
|||||||
+74
-64
@@ -1,32 +1,32 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
exports = module.exports = {
|
exports = module.exports = {
|
||||||
addMailbox: addMailbox,
|
addMailbox,
|
||||||
addList: addList,
|
addList,
|
||||||
|
|
||||||
updateMailboxOwner: updateMailboxOwner,
|
updateMailboxOwner,
|
||||||
updateList: updateList,
|
updateList,
|
||||||
del: del,
|
del,
|
||||||
|
|
||||||
listAliases: listAliases,
|
getMailboxCount,
|
||||||
listMailboxes: listMailboxes,
|
listMailboxes,
|
||||||
getLists: getLists,
|
getLists,
|
||||||
|
|
||||||
listAllMailboxes: listAllMailboxes,
|
listAllMailboxes,
|
||||||
|
|
||||||
get: get,
|
get,
|
||||||
getMailbox: getMailbox,
|
getMailbox,
|
||||||
getList: getList,
|
getList,
|
||||||
getAlias: getAlias,
|
getAlias,
|
||||||
|
|
||||||
getAliasesForName: getAliasesForName,
|
getAliasesForName,
|
||||||
setAliasesForName: setAliasesForName,
|
setAliasesForName,
|
||||||
|
|
||||||
getByOwnerId: getByOwnerId,
|
getByOwnerId,
|
||||||
delByOwnerId: delByOwnerId,
|
delByOwnerId,
|
||||||
delByDomain: delByDomain,
|
delByDomain,
|
||||||
|
|
||||||
updateName: updateName,
|
updateName,
|
||||||
|
|
||||||
_clear: clear,
|
_clear: clear,
|
||||||
|
|
||||||
@@ -38,15 +38,18 @@ exports = module.exports = {
|
|||||||
var assert = require('assert'),
|
var assert = require('assert'),
|
||||||
BoxError = require('./boxerror.js'),
|
BoxError = require('./boxerror.js'),
|
||||||
database = require('./database.js'),
|
database = require('./database.js'),
|
||||||
|
mysql = require('mysql'),
|
||||||
safe = require('safetydance'),
|
safe = require('safetydance'),
|
||||||
util = require('util');
|
util = require('util');
|
||||||
|
|
||||||
var MAILBOX_FIELDS = [ 'name', 'type', 'ownerId', 'aliasTarget', 'creationTime', 'membersJson', 'domain' ].join(',');
|
var MAILBOX_FIELDS = [ 'name', 'type', 'ownerId', 'aliasName', 'aliasDomain', 'creationTime', 'membersJson', 'membersOnly', 'domain' ].join(',');
|
||||||
|
|
||||||
function postProcess(data) {
|
function postProcess(data) {
|
||||||
data.members = safe.JSON.parse(data.membersJson) || [ ];
|
data.members = safe.JSON.parse(data.membersJson) || [ ];
|
||||||
delete data.membersJson;
|
delete data.membersJson;
|
||||||
|
|
||||||
|
data.membersOnly = !!data.membersOnly;
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,14 +81,15 @@ function updateMailboxOwner(name, domain, ownerId, callback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function addList(name, domain, members, callback) {
|
function addList(name, domain, members, membersOnly, callback) {
|
||||||
assert.strictEqual(typeof name, 'string');
|
assert.strictEqual(typeof name, 'string');
|
||||||
assert.strictEqual(typeof domain, 'string');
|
assert.strictEqual(typeof domain, 'string');
|
||||||
assert(Array.isArray(members));
|
assert(Array.isArray(members));
|
||||||
|
assert.strictEqual(typeof membersOnly, 'boolean');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
database.query('INSERT INTO mailboxes (name, type, domain, ownerId, membersJson) VALUES (?, ?, ?, ?, ?)',
|
database.query('INSERT INTO mailboxes (name, type, domain, ownerId, membersJson, membersOnly) VALUES (?, ?, ?, ?, ?, ?)',
|
||||||
[ name, exports.TYPE_LIST, domain, 'admin', JSON.stringify(members) ], function (error) {
|
[ name, exports.TYPE_LIST, domain, 'admin', JSON.stringify(members), membersOnly ], function (error) {
|
||||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, 'mailbox already exists'));
|
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, 'mailbox already exists'));
|
||||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||||
|
|
||||||
@@ -93,14 +97,15 @@ function addList(name, domain, members, callback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateList(name, domain, members, callback) {
|
function updateList(name, domain, members, membersOnly, callback) {
|
||||||
assert.strictEqual(typeof name, 'string');
|
assert.strictEqual(typeof name, 'string');
|
||||||
assert.strictEqual(typeof domain, 'string');
|
assert.strictEqual(typeof domain, 'string');
|
||||||
assert(Array.isArray(members));
|
assert(Array.isArray(members));
|
||||||
|
assert.strictEqual(typeof membersOnly, 'boolean');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
database.query('UPDATE mailboxes SET membersJson = ? WHERE name = ? AND domain = ?',
|
database.query('UPDATE mailboxes SET membersJson = ?, membersOnly = ? WHERE name = ? AND domain = ?',
|
||||||
[ JSON.stringify(members), name, domain ], function (error, result) {
|
[ JSON.stringify(members), membersOnly, name, domain ], function (error, result) {
|
||||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||||
if (result.affectedRows === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found'));
|
if (result.affectedRows === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found'));
|
||||||
|
|
||||||
@@ -123,7 +128,7 @@ function del(name, domain, callback) {
|
|||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
// deletes aliases as well
|
// deletes aliases as well
|
||||||
database.query('DELETE FROM mailboxes WHERE (name=? OR aliasTarget = ?) AND domain = ?', [ name, name, domain ], function (error, result) {
|
database.query('DELETE FROM mailboxes WHERE ((name=? AND domain=?) OR (aliasName = ? AND aliasDomain=?))', [ name, domain, name, domain ], function (error, result) {
|
||||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||||
if (result.affectedRows === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found'));
|
if (result.affectedRows === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found'));
|
||||||
|
|
||||||
@@ -200,20 +205,35 @@ function getMailbox(name, domain, callback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function listMailboxes(domain, page, perPage, callback) {
|
function getMailboxCount(domain, callback) {
|
||||||
assert.strictEqual(typeof domain, 'string');
|
assert.strictEqual(typeof domain, 'string');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
database.query('SELECT COUNT(*) AS total FROM mailboxes WHERE type = ? AND domain = ?', [ exports.TYPE_MAILBOX, domain ], function (error, results) {
|
||||||
|
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||||
|
|
||||||
|
callback(null, results[0].total);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function listMailboxes(domain, search, page, perPage, callback) {
|
||||||
|
assert.strictEqual(typeof domain, 'string');
|
||||||
|
assert(typeof search === 'string' || search === null);
|
||||||
assert.strictEqual(typeof page, 'number');
|
assert.strictEqual(typeof page, 'number');
|
||||||
assert.strictEqual(typeof perPage, 'number');
|
assert.strictEqual(typeof perPage, 'number');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
database.query(`SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE type = ? AND domain = ? ORDER BY name LIMIT ${(page-1)*perPage},${perPage}`,
|
let query = `SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE type = ? AND domain = ?`;
|
||||||
[ exports.TYPE_MAILBOX, domain ], function (error, results) {
|
if (search) query += ' AND (name LIKE ' + mysql.escape('%' + search + '%') + ')';
|
||||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
query += 'ORDER BY name LIMIT ?,?';
|
||||||
|
|
||||||
results.forEach(function (result) { postProcess(result); });
|
database.query(query, [ exports.TYPE_MAILBOX, domain, (page-1)*perPage, perPage ], function (error, results) {
|
||||||
|
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||||
|
|
||||||
callback(null, results);
|
results.forEach(function (result) { postProcess(result); });
|
||||||
});
|
|
||||||
|
callback(null, results);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function listAllMailboxes(page, perPage, callback) {
|
function listAllMailboxes(page, perPage, callback) {
|
||||||
@@ -221,8 +241,8 @@ function listAllMailboxes(page, perPage, callback) {
|
|||||||
assert.strictEqual(typeof perPage, 'number');
|
assert.strictEqual(typeof perPage, 'number');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
database.query(`SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE type = ? ORDER BY name LIMIT ${(page-1)*perPage},${perPage}`,
|
database.query(`SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE type = ? ORDER BY name LIMIT ?,?`,
|
||||||
[ exports.TYPE_MAILBOX ], function (error, results) {
|
[ exports.TYPE_MAILBOX, (page-1)*perPage, perPage ], function (error, results) {
|
||||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||||
|
|
||||||
results.forEach(function (result) { postProcess(result); });
|
results.forEach(function (result) { postProcess(result); });
|
||||||
@@ -231,18 +251,25 @@ function listAllMailboxes(page, perPage, callback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLists(domain, callback) {
|
function getLists(domain, search, page, perPage, callback) {
|
||||||
assert.strictEqual(typeof domain, 'string');
|
assert.strictEqual(typeof domain, 'string');
|
||||||
|
assert(typeof search === 'string' || search === null);
|
||||||
|
assert.strictEqual(typeof page, 'number');
|
||||||
|
assert.strictEqual(typeof perPage, 'number');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE type = ? AND domain = ?',
|
let query = `SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE type = ? AND domain = ?`;
|
||||||
[ exports.TYPE_LIST, domain ], function (error, results) {
|
if (search) query += ' AND (name LIKE ' + mysql.escape('%' + search + '%') + ' OR membersJson LIKE ' + mysql.escape('%' + search + '%') + ')';
|
||||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
|
||||||
|
|
||||||
results.forEach(function (result) { postProcess(result); });
|
query += 'ORDER BY name LIMIT ?,?';
|
||||||
|
|
||||||
callback(null, results);
|
database.query(query, [ exports.TYPE_LIST, domain, (page-1)*perPage, perPage ], function (error, results) {
|
||||||
});
|
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||||
|
|
||||||
|
results.forEach(function (result) { postProcess(result); });
|
||||||
|
|
||||||
|
callback(null, results);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getList(name, domain, callback) {
|
function getList(name, domain, callback) {
|
||||||
@@ -285,10 +312,10 @@ function setAliasesForName(name, domain, aliases, callback) {
|
|||||||
|
|
||||||
var queries = [];
|
var queries = [];
|
||||||
// clear existing aliases
|
// clear existing aliases
|
||||||
queries.push({ query: 'DELETE FROM mailboxes WHERE aliasTarget = ? AND domain = ? AND type = ?', args: [ name, domain, exports.TYPE_ALIAS ] });
|
queries.push({ query: 'DELETE FROM mailboxes WHERE aliasName = ? AND aliasDomain = ? AND type = ?', args: [ name, domain, exports.TYPE_ALIAS ] });
|
||||||
aliases.forEach(function (alias) {
|
aliases.forEach(function (alias) {
|
||||||
queries.push({ query: 'INSERT INTO mailboxes (name, type, domain, aliasTarget, ownerId) VALUES (?, ?, ?, ?, ?)',
|
queries.push({ query: 'INSERT INTO mailboxes (name, domain, type, aliasName, aliasDomain, ownerId) VALUES (?, ?, ?, ?, ?, ?)',
|
||||||
args: [ alias, exports.TYPE_ALIAS, domain, name, results[0].ownerId ] });
|
args: [ alias.name, alias.domain, exports.TYPE_ALIAS, name, domain, results[0].ownerId ] });
|
||||||
});
|
});
|
||||||
|
|
||||||
database.transaction(queries, function (error) {
|
database.transaction(queries, function (error) {
|
||||||
@@ -311,27 +338,10 @@ function getAliasesForName(name, domain, callback) {
|
|||||||
assert.strictEqual(typeof domain, 'string');
|
assert.strictEqual(typeof domain, 'string');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
database.query('SELECT name FROM mailboxes WHERE type = ? AND aliasTarget = ? AND domain = ? ORDER BY name',
|
database.query('SELECT name, domain FROM mailboxes WHERE type = ? AND aliasName = ? AND aliasDomain = ? ORDER BY name',
|
||||||
[ exports.TYPE_ALIAS, name, domain ], function (error, results) {
|
[ exports.TYPE_ALIAS, name, domain ], function (error, results) {
|
||||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||||
|
|
||||||
results = results.map(function (r) { return r.name; });
|
|
||||||
callback(null, results);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function listAliases(domain, page, perPage, callback) {
|
|
||||||
assert.strictEqual(typeof domain, 'string');
|
|
||||||
assert.strictEqual(typeof page, 'number');
|
|
||||||
assert.strictEqual(typeof perPage, 'number');
|
|
||||||
assert.strictEqual(typeof callback, 'function');
|
|
||||||
|
|
||||||
database.query(`SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE domain = ? AND type = ? ORDER BY name LIMIT ${(page-1)*perPage},${perPage}`,
|
|
||||||
[ domain, exports.TYPE_ALIAS ], function (error, results) {
|
|
||||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
|
||||||
|
|
||||||
results.forEach(function (result) { postProcess(result); });
|
|
||||||
|
|
||||||
callback(null, results);
|
callback(null, results);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-10
@@ -7,6 +7,7 @@ map $http_upgrade $connection_upgrade {
|
|||||||
# http server
|
# http server
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
|
server_tokens off; # hide version
|
||||||
<% if (hasIPv6) { -%>
|
<% if (hasIPv6) { -%>
|
||||||
listen [::]:80;
|
listen [::]:80;
|
||||||
<% } -%>
|
<% } -%>
|
||||||
@@ -42,18 +43,19 @@ server {
|
|||||||
server {
|
server {
|
||||||
<% if (vhost) { -%>
|
<% if (vhost) { -%>
|
||||||
server_name <%= vhost %>;
|
server_name <%= vhost %>;
|
||||||
listen 443 http2;
|
listen 443 ssl http2;
|
||||||
<% if (hasIPv6) { -%>
|
<% if (hasIPv6) { -%>
|
||||||
listen [::]:443 http2;
|
listen [::]:443 ssl http2;
|
||||||
<% } -%>
|
<% } -%>
|
||||||
<% } else { -%>
|
<% } else { -%>
|
||||||
listen 443 http2 default_server;
|
listen 443 ssl http2 default_server;
|
||||||
<% if (hasIPv6) { -%>
|
<% if (hasIPv6) { -%>
|
||||||
listen [::]:443 http2 default_server;
|
listen [::]:443 ssl http2 default_server;
|
||||||
<% } -%>
|
<% } -%>
|
||||||
<% } -%>
|
<% } -%>
|
||||||
|
|
||||||
ssl on;
|
server_tokens off; # hide version
|
||||||
|
|
||||||
# paths are relative to prefix and not to this file
|
# paths are relative to prefix and not to this file
|
||||||
ssl_certificate <%= certFilePath %>;
|
ssl_certificate <%= certFilePath %>;
|
||||||
ssl_certificate_key <%= keyFilePath %>;
|
ssl_certificate_key <%= keyFilePath %>;
|
||||||
@@ -135,14 +137,21 @@ server {
|
|||||||
# internal means this is for internal routing and cannot be accessed as URL from browser
|
# internal means this is for internal routing and cannot be accessed as URL from browser
|
||||||
internal;
|
internal;
|
||||||
}
|
}
|
||||||
location /appstatus.html {
|
|
||||||
internal;
|
location @wellknown-upstream {
|
||||||
|
<% if ( endpoint === 'admin' ) { %>
|
||||||
|
proxy_pass http://127.0.0.1:3000;
|
||||||
|
<% } else if ( endpoint === 'app' ) { %>
|
||||||
|
proxy_pass http://127.0.0.1:<%= port %>;
|
||||||
|
<% } else if ( endpoint === 'redirect' ) { %>
|
||||||
|
return 302 https://<%= redirectTo %>$request_uri;
|
||||||
|
<% } %>
|
||||||
}
|
}
|
||||||
|
|
||||||
# user defined .well-known resources
|
# user defined .well-known resources
|
||||||
# alias means only the part after matched location is appended (unlike root)
|
location ~ ^/.well-known/(.*)$ {
|
||||||
location /.well-known/ {
|
root /home/yellowtent/boxdata/well-known/$host;
|
||||||
alias /home/yellowtent/boxdata/well-known/$host/;
|
try_files /$1 @wellknown-upstream;
|
||||||
}
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
@@ -186,6 +195,11 @@ server {
|
|||||||
client_max_body_size 0;
|
client_max_body_size 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location ~ ^/api/v1/apps/.*/files/ {
|
||||||
|
proxy_pass http://127.0.0.1:3000;
|
||||||
|
client_max_body_size 0;
|
||||||
|
}
|
||||||
|
|
||||||
# graphite paths (uncomment block below and visit /graphite-web/dashboard)
|
# graphite paths (uncomment block below and visit /graphite-web/dashboard)
|
||||||
# remember to comment out the CSP policy as well to access the graphite dashboard
|
# remember to comment out the CSP policy as well to access the graphite dashboard
|
||||||
# location ~ ^/graphite-web/ {
|
# location ~ ^/graphite-web/ {
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ function oomEvent(eventId, app, addon, containerId, event, callback) {
|
|||||||
if (app) {
|
if (app) {
|
||||||
program = `App ${app.fqdn}`;
|
program = `App ${app.fqdn}`;
|
||||||
title = `The application ${app.fqdn} (${app.manifest.title}) ran out of memory.`;
|
title = `The application ${app.fqdn} (${app.manifest.title}) ran out of memory.`;
|
||||||
message = 'The application has been restarted automatically. If you see this notification often, consider increasing the [memory limit](https://cloudron.io/documentation/apps/#increasing-the-memory-limit-of-an-app)';
|
message = 'The application has been restarted automatically. If you see this notification often, consider increasing the [memory limit](https://cloudron.io/documentation/apps/#memory-limit)';
|
||||||
} else if (addon) {
|
} else if (addon) {
|
||||||
program = `${addon.name} service`;
|
program = `${addon.name} service`;
|
||||||
title = `The ${addon.name} service ran out of memory`;
|
title = `The ${addon.name} service ran out of memory`;
|
||||||
@@ -211,7 +211,7 @@ function appUpdated(eventId, app, callback) {
|
|||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
mailer.appUpdated(admin.email, app, function (error) {
|
mailer.appUpdated(admin.email, app, function (error) {
|
||||||
if (error) console.error('Failed to send app updated email', error); // non fatal
|
if (error) debug('appUpdated: Failed to send app updated email', error); // non fatal
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -273,7 +273,7 @@ function alert(id, title, message, callback) {
|
|||||||
assert.strictEqual(typeof message, 'string');
|
assert.strictEqual(typeof message, 'string');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
debug(`alert: id=${id} title=${title} message=${message}`);
|
debug(`alert: id=${id} title=${title}`);
|
||||||
|
|
||||||
const acknowledged = !message;
|
const acknowledged = !message;
|
||||||
|
|
||||||
@@ -301,7 +301,7 @@ function alert(id, title, message, callback) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}, function (error) {
|
}, function (error) {
|
||||||
if (error) console.error(error);
|
if (error) debug('alert: error notifying', error);
|
||||||
|
|
||||||
callback();
|
callback();
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,6 @@ exports = module.exports = {
|
|||||||
CLOUDRON_DEFAULT_AVATAR_FILE: path.join(__dirname + '/../assets/avatar.png'),
|
CLOUDRON_DEFAULT_AVATAR_FILE: path.join(__dirname + '/../assets/avatar.png'),
|
||||||
INFRA_VERSION_FILE: path.join(baseDir(), 'platformdata/INFRA_VERSION'),
|
INFRA_VERSION_FILE: path.join(baseDir(), 'platformdata/INFRA_VERSION'),
|
||||||
|
|
||||||
LICENSE_FILE: '/etc/cloudron/LICENSE',
|
|
||||||
PROVIDER_FILE: '/etc/cloudron/PROVIDER',
|
PROVIDER_FILE: '/etc/cloudron/PROVIDER',
|
||||||
|
|
||||||
PLATFORM_DATA_DIR: path.join(baseDir(), 'platformdata'),
|
PLATFORM_DATA_DIR: path.join(baseDir(), 'platformdata'),
|
||||||
@@ -51,6 +50,7 @@ exports = module.exports = {
|
|||||||
LOG_DIR: path.join(baseDir(), 'platformdata/logs'),
|
LOG_DIR: path.join(baseDir(), 'platformdata/logs'),
|
||||||
TASKS_LOG_DIR: path.join(baseDir(), 'platformdata/logs/tasks'),
|
TASKS_LOG_DIR: path.join(baseDir(), 'platformdata/logs/tasks'),
|
||||||
CRASH_LOG_DIR: path.join(baseDir(), 'platformdata/logs/crash'),
|
CRASH_LOG_DIR: path.join(baseDir(), 'platformdata/logs/crash'),
|
||||||
|
BOX_LOG_FILE: path.join(baseDir(), 'platformdata/logs/box.log'),
|
||||||
|
|
||||||
GHOST_USER_FILE: path.join(baseDir(), 'platformdata/cloudron_ghost.json'),
|
GHOST_USER_FILE: path.join(baseDir(), 'platformdata/cloudron_ghost.json'),
|
||||||
|
|
||||||
|
|||||||
+27
-32
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
exports = module.exports = {
|
exports = module.exports = {
|
||||||
start: start,
|
start: start,
|
||||||
stop: stop,
|
stopAllTasks: stopAllTasks,
|
||||||
|
|
||||||
// exported for testing
|
// exported for testing
|
||||||
_isReady: false
|
_isReady: false
|
||||||
@@ -56,9 +56,8 @@ function start(callback) {
|
|||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
async.series([
|
async.series([
|
||||||
stopContainers.bind(null, existingInfra),
|
(next) => { if (existingInfra.version !== infra.version) removeAllContainers(existingInfra, next); else next(); },
|
||||||
// mark app state before we start addons. this gives the db import logic a chance to mark an app as errored
|
markApps.bind(null, existingInfra), // mark app state before we start addons. this gives the db import logic a chance to mark an app as errored
|
||||||
startApps.bind(null, existingInfra),
|
|
||||||
graphs.startGraphite.bind(null, existingInfra),
|
graphs.startGraphite.bind(null, existingInfra),
|
||||||
sftp.startSftp.bind(null, existingInfra),
|
sftp.startSftp.bind(null, existingInfra),
|
||||||
addons.startServices.bind(null, existingInfra),
|
addons.startServices.bind(null, existingInfra),
|
||||||
@@ -74,7 +73,7 @@ function start(callback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function stop(callback) {
|
function stopAllTasks(callback) {
|
||||||
tasks.stopAllTasks(callback);
|
tasks.stopAllTasks(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,41 +129,37 @@ function pruneInfraImages(callback) {
|
|||||||
}, callback);
|
}, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopContainers(existingInfra, callback) {
|
function removeAllContainers(existingInfra, callback) {
|
||||||
// always stop addons to restart them on any infra change, regardless of minor or major update
|
debug('removeAllContainers: removing all containers for infra upgrade');
|
||||||
if (existingInfra.version !== infra.version) {
|
|
||||||
debug('stopping all containers for infra upgrade');
|
|
||||||
async.series([
|
|
||||||
shell.exec.bind(null, 'stopContainers', 'docker ps -qa --filter \'label=isCloudronManaged\' | xargs --no-run-if-empty docker stop'),
|
|
||||||
shell.exec.bind(null, 'stopContainers', 'docker ps -qa --filter \'label=isCloudronManaged\' | xargs --no-run-if-empty docker rm -f')
|
|
||||||
], callback);
|
|
||||||
} else {
|
|
||||||
assert(typeof infra.images, 'object');
|
|
||||||
var changedAddons = [ ];
|
|
||||||
for (var imageName in existingInfra.images) { // do not use infra.images because we can only stop things which are existing
|
|
||||||
if (infra.images[imageName].tag !== existingInfra.images[imageName].tag) changedAddons.push(imageName);
|
|
||||||
}
|
|
||||||
|
|
||||||
debug('stopContainer: stopping addons for incremental infra update: %j', changedAddons);
|
async.series([
|
||||||
let filterArg = changedAddons.map(function (c) { return `--filter 'name=${c}'`; }).join(' '); // name=c matches *c*. required for redis-{appid}
|
shell.exec.bind(null, 'removeAllContainers', 'docker ps -qa --filter \'label=isCloudronManaged\' | xargs --no-run-if-empty docker stop'),
|
||||||
// ignore error if container not found (and fail later) so that this code works across restarts
|
shell.exec.bind(null, 'removeAllContainers', 'docker ps -qa --filter \'label=isCloudronManaged\' | xargs --no-run-if-empty docker rm -f')
|
||||||
async.series([
|
], callback);
|
||||||
shell.exec.bind(null, 'stopContainers', `docker ps -qa ${filterArg} --filter 'label=isCloudronManaged' | xargs --no-run-if-empty docker stop || true`),
|
|
||||||
shell.exec.bind(null, 'stopContainers', `docker ps -qa ${filterArg} --filter 'label=isCloudronManaged' | xargs --no-run-if-empty docker rm -f || true`)
|
|
||||||
], callback);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function startApps(existingInfra, callback) {
|
function markApps(existingInfra, callback) {
|
||||||
if (existingInfra.version === 'none') { // cloudron is being restored from backup
|
if (existingInfra.version === 'none') { // cloudron is being restored from backup
|
||||||
debug('startApps: restoring installed apps');
|
debug('markApps: restoring installed apps');
|
||||||
apps.restoreInstalledApps(callback);
|
apps.restoreInstalledApps(callback);
|
||||||
} else if (existingInfra.version !== infra.version) {
|
} else if (existingInfra.version !== infra.version) {
|
||||||
debug('startApps: reconfiguring installed apps');
|
debug('markApps: reconfiguring installed apps');
|
||||||
reverseProxy.removeAppConfigs(); // should we change the cert location, nginx will not start
|
reverseProxy.removeAppConfigs(); // should we change the cert location, nginx will not start
|
||||||
apps.configureInstalledApps(callback);
|
apps.configureInstalledApps(callback);
|
||||||
} else {
|
} else {
|
||||||
debug('startApps: apps are already uptodate');
|
let changedAddons = [];
|
||||||
callback();
|
if (infra.images.mysql.tag !== existingInfra.images.mysql.tag) changedAddons.push('mysql');
|
||||||
|
if (infra.images.postgresql.tag !== existingInfra.images.postgresql.tag) changedAddons.push('postgresql');
|
||||||
|
if (infra.images.mongodb.tag !== existingInfra.images.mongodb.tag) changedAddons.push('mongodb');
|
||||||
|
if (infra.images.redis.tag !== existingInfra.images.redis.tag) changedAddons.push('redis');
|
||||||
|
|
||||||
|
if (changedAddons.length) {
|
||||||
|
// restart apps if docker image changes since the IP changes and any "persistent" connections fail
|
||||||
|
debug(`markApps: changedAddons: ${JSON.stringify(changedAddons)}`);
|
||||||
|
apps.restartAppsUsingAddons(changedAddons, callback);
|
||||||
|
} else {
|
||||||
|
debug('markApps: apps are already uptodate');
|
||||||
|
callback();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-32
@@ -4,13 +4,10 @@ exports = module.exports = {
|
|||||||
setup: setup,
|
setup: setup,
|
||||||
restore: restore,
|
restore: restore,
|
||||||
activate: activate,
|
activate: activate,
|
||||||
getStatus: getStatus,
|
getStatus: getStatus
|
||||||
|
|
||||||
autoRegister: autoRegister
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var appstore = require('./appstore.js'),
|
var assert = require('assert'),
|
||||||
assert = require('assert'),
|
|
||||||
async = require('async'),
|
async = require('async'),
|
||||||
backups = require('./backups.js'),
|
backups = require('./backups.js'),
|
||||||
BoxError = require('./boxerror.js'),
|
BoxError = require('./boxerror.js'),
|
||||||
@@ -19,10 +16,7 @@ var appstore = require('./appstore.js'),
|
|||||||
debug = require('debug')('box:provision'),
|
debug = require('debug')('box:provision'),
|
||||||
domains = require('./domains.js'),
|
domains = require('./domains.js'),
|
||||||
eventlog = require('./eventlog.js'),
|
eventlog = require('./eventlog.js'),
|
||||||
fs = require('fs'),
|
|
||||||
mail = require('./mail.js'),
|
mail = require('./mail.js'),
|
||||||
paths = require('./paths.js'),
|
|
||||||
safe = require('safetydance'),
|
|
||||||
semver = require('semver'),
|
semver = require('semver'),
|
||||||
settings = require('./settings.js'),
|
settings = require('./settings.js'),
|
||||||
sysinfo = require('./sysinfo.js'),
|
sysinfo = require('./sysinfo.js'),
|
||||||
@@ -53,27 +47,6 @@ function setProgress(task, message, callback) {
|
|||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
|
|
||||||
function autoRegister(domain, callback) {
|
|
||||||
assert.strictEqual(typeof domain, 'string');
|
|
||||||
assert.strictEqual(typeof callback, 'function');
|
|
||||||
|
|
||||||
if (!fs.existsSync(paths.LICENSE_FILE)) return callback();
|
|
||||||
|
|
||||||
const license = safe.fs.readFileSync(paths.LICENSE_FILE, 'utf8');
|
|
||||||
if (!license) return callback(new BoxError(BoxError.LICENSE_ERROR, 'Cannot read license'));
|
|
||||||
|
|
||||||
debug('Auto-registering cloudron');
|
|
||||||
|
|
||||||
appstore.registerWithLicense(license.trim(), domain, function (error) {
|
|
||||||
if (error && error.reason !== BoxError.CONFLICT) { // not already registered
|
|
||||||
debug('Failed to auto-register cloudron', error);
|
|
||||||
return callback(new BoxError(BoxError.LICENSE_ERROR, 'Failed to auto-register Cloudron with license. Please contact support@cloudron.io'));
|
|
||||||
}
|
|
||||||
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function unprovision(callback) {
|
function unprovision(callback) {
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
@@ -134,7 +107,6 @@ function setup(dnsConfig, sysinfoConfig, auditSource, callback) {
|
|||||||
callback(); // now that args are validated run the task in the background
|
callback(); // now that args are validated run the task in the background
|
||||||
|
|
||||||
async.series([
|
async.series([
|
||||||
autoRegister.bind(null, domain),
|
|
||||||
settings.setSysinfoConfig.bind(null, sysinfoConfig),
|
settings.setSysinfoConfig.bind(null, sysinfoConfig),
|
||||||
domains.prepareDashboardDomain.bind(null, domain, auditSource, (progress) => setProgress('setup', progress.message, NOOP_CALLBACK)),
|
domains.prepareDashboardDomain.bind(null, domain, auditSource, (progress) => setProgress('setup', progress.message, NOOP_CALLBACK)),
|
||||||
cloudron.setDashboardDomain.bind(null, domain, auditSource),
|
cloudron.setDashboardDomain.bind(null, domain, auditSource),
|
||||||
@@ -206,9 +178,16 @@ function restore(backupConfig, backupId, version, sysinfoConfig, auditSource, ca
|
|||||||
if (error) return done(error);
|
if (error) return done(error);
|
||||||
if (activated) return done(new BoxError(BoxError.CONFLICT, 'Already activated. Restore with a fresh Cloudron installation.'));
|
if (activated) return done(new BoxError(BoxError.CONFLICT, 'Already activated. Restore with a fresh Cloudron installation.'));
|
||||||
|
|
||||||
backups.testConfig(backupConfig, function (error) {
|
backups.testProviderConfig(backupConfig, function (error) {
|
||||||
if (error) return done(error);
|
if (error) return done(error);
|
||||||
|
|
||||||
|
if ('password' in backupConfig) {
|
||||||
|
backupConfig.encryption = backups.generateEncryptionKeysSync(backupConfig.password);
|
||||||
|
delete backupConfig.password;
|
||||||
|
} else {
|
||||||
|
backupConfig.encryption = null;
|
||||||
|
}
|
||||||
|
|
||||||
sysinfo.testConfig(sysinfoConfig, function (error) {
|
sysinfo.testConfig(sysinfoConfig, function (error) {
|
||||||
if (error) return done(error);
|
if (error) return done(error);
|
||||||
|
|
||||||
@@ -247,11 +226,11 @@ function getStatus(callback) {
|
|||||||
version: constants.VERSION,
|
version: constants.VERSION,
|
||||||
apiServerOrigin: settings.apiServerOrigin(), // used by CaaS tool
|
apiServerOrigin: settings.apiServerOrigin(), // used by CaaS tool
|
||||||
webServerOrigin: settings.webServerOrigin(), // used by CaaS tool
|
webServerOrigin: settings.webServerOrigin(), // used by CaaS tool
|
||||||
provider: settings.provider(),
|
|
||||||
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
|
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
|
||||||
footer: allSettings[settings.FOOTER_KEY] || constants.FOOTER,
|
footer: allSettings[settings.FOOTER_KEY] || constants.FOOTER,
|
||||||
adminFqdn: settings.adminDomain() ? settings.adminFqdn() : null,
|
adminFqdn: settings.adminDomain() ? settings.adminFqdn() : null,
|
||||||
activated: activated,
|
activated: activated,
|
||||||
|
provider: settings.provider() // used by setup wizard of marketplace images
|
||||||
}, gProvisionStatus));
|
}, gProvisionStatus));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Binary file not shown.
+50
-45
@@ -27,7 +27,7 @@ exports = module.exports = {
|
|||||||
removeAppConfigs: removeAppConfigs,
|
removeAppConfigs: removeAppConfigs,
|
||||||
|
|
||||||
// exported for testing
|
// exported for testing
|
||||||
_getCertApi: getCertApi
|
_getAcmeApi: getAcmeApi
|
||||||
};
|
};
|
||||||
|
|
||||||
var acme2 = require('./cert/acme2.js'),
|
var acme2 = require('./cert/acme2.js'),
|
||||||
@@ -35,14 +35,12 @@ var acme2 = require('./cert/acme2.js'),
|
|||||||
assert = require('assert'),
|
assert = require('assert'),
|
||||||
async = require('async'),
|
async = require('async'),
|
||||||
BoxError = require('./boxerror.js'),
|
BoxError = require('./boxerror.js'),
|
||||||
caas = require('./cert/caas.js'),
|
|
||||||
constants = require('./constants.js'),
|
constants = require('./constants.js'),
|
||||||
crypto = require('crypto'),
|
crypto = require('crypto'),
|
||||||
debug = require('debug')('box:reverseproxy'),
|
debug = require('debug')('box:reverseproxy'),
|
||||||
domains = require('./domains.js'),
|
domains = require('./domains.js'),
|
||||||
ejs = require('ejs'),
|
ejs = require('ejs'),
|
||||||
eventlog = require('./eventlog.js'),
|
eventlog = require('./eventlog.js'),
|
||||||
fallback = require('./cert/fallback.js'),
|
|
||||||
fs = require('fs'),
|
fs = require('fs'),
|
||||||
mail = require('./mail.js'),
|
mail = require('./mail.js'),
|
||||||
os = require('os'),
|
os = require('os'),
|
||||||
@@ -59,27 +57,23 @@ var acme2 = require('./cert/acme2.js'),
|
|||||||
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/nginxconfig.ejs', { encoding: 'utf8' }),
|
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/nginxconfig.ejs', { encoding: 'utf8' }),
|
||||||
RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh');
|
RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh');
|
||||||
|
|
||||||
function getCertApi(domainObject, callback) {
|
function getAcmeApi(domainObject, callback) {
|
||||||
assert.strictEqual(typeof domainObject, 'object');
|
assert.strictEqual(typeof domainObject, 'object');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
if (domainObject.tlsConfig.provider === 'fallback') return callback(null, fallback, { fallback: true });
|
const api = acme2;
|
||||||
|
|
||||||
var api = domainObject.tlsConfig.provider === 'caas' ? caas : acme2;
|
let options = { prod: false, performHttpAuthorization: false, wildcard: false, email: '' };
|
||||||
|
options.prod = domainObject.tlsConfig.provider.match(/.*-prod/) !== null; // matches 'le-prod' or 'letsencrypt-prod'
|
||||||
var options = { prod: false, performHttpAuthorization: false, wildcard: false, email: '' };
|
options.performHttpAuthorization = domainObject.provider.match(/noop|manual|wildcard/) !== null;
|
||||||
if (domainObject.tlsConfig.provider !== 'caas') {
|
options.wildcard = !!domainObject.tlsConfig.wildcard;
|
||||||
options.prod = domainObject.tlsConfig.provider.match(/.*-prod/) !== null; // matches 'le-prod' or 'letsencrypt-prod'
|
|
||||||
options.performHttpAuthorization = domainObject.provider.match(/noop|manual|wildcard/) !== null;
|
|
||||||
options.wildcard = !!domainObject.tlsConfig.wildcard;
|
|
||||||
}
|
|
||||||
|
|
||||||
// registering user with an email requires A or MX record (https://github.com/letsencrypt/boulder/issues/1197)
|
// registering user with an email requires A or MX record (https://github.com/letsencrypt/boulder/issues/1197)
|
||||||
// we cannot use admin@fqdn because the user might not have set it up.
|
// we cannot use admin@fqdn because the user might not have set it up.
|
||||||
// we simply update the account with the latest email we have each time when getting letsencrypt certs
|
// we simply update the account with the latest email we have each time when getting letsencrypt certs
|
||||||
// https://github.com/ietf-wg-acme/acme/issues/30
|
// https://github.com/ietf-wg-acme/acme/issues/30
|
||||||
users.getOwner(function (error, owner) {
|
users.getOwner(function (error, owner) {
|
||||||
options.email = error ? 'support@cloudron.io' : owner.email; // can error if not activated yet
|
options.email = error ? 'webmaster@cloudron.io' : owner.email; // can error if not activated yet
|
||||||
|
|
||||||
callback(null, api, options);
|
callback(null, api, options);
|
||||||
});
|
});
|
||||||
@@ -108,8 +102,6 @@ function providerMatchesSync(domainObject, certFilePath, apiOptions) {
|
|||||||
|
|
||||||
if (!fs.existsSync(certFilePath)) return false; // not found
|
if (!fs.existsSync(certFilePath)) return false; // not found
|
||||||
|
|
||||||
if (apiOptions.fallback) return certFilePath.includes('.host.cert');
|
|
||||||
|
|
||||||
const subjectAndIssuer = safe.child_process.execSync(`/usr/bin/openssl x509 -noout -subject -issuer -in "${certFilePath}"`, { encoding: 'utf8' });
|
const subjectAndIssuer = safe.child_process.execSync(`/usr/bin/openssl x509 -noout -subject -issuer -in "${certFilePath}"`, { encoding: 'utf8' });
|
||||||
if (!subjectAndIssuer) return false; // something bad happenned
|
if (!subjectAndIssuer) return false; // something bad happenned
|
||||||
|
|
||||||
@@ -215,15 +207,9 @@ function setFallbackCertificate(domain, fallback, callback) {
|
|||||||
assert.strictEqual(typeof fallback, 'object');
|
assert.strictEqual(typeof fallback, 'object');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
if (fallback.restricted) { // restricted certs are not backed up
|
debug(`setFallbackCertificate: setting certs for domain ${domain}`);
|
||||||
debug(`setFallbackCertificate: setting restricted certs for domain ${domain}`);
|
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`), fallback.cert)) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message));
|
||||||
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain}.host.cert`), fallback.cert)) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message));
|
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.key`), fallback.key)) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message));
|
||||||
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain}.host.key`), fallback.key)) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message));
|
|
||||||
} else {
|
|
||||||
debug(`setFallbackCertificate: setting certs for domain ${domain}`);
|
|
||||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`), fallback.cert)) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message));
|
|
||||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.key`), fallback.key)) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message));
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: maybe the cert is being used by the mail container
|
// TODO: maybe the cert is being used by the mail container
|
||||||
reload(function (error) {
|
reload(function (error) {
|
||||||
@@ -237,15 +223,8 @@ function getFallbackCertificate(domain, callback) {
|
|||||||
assert.strictEqual(typeof domain, 'string');
|
assert.strictEqual(typeof domain, 'string');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
// check for any pre-provisioned (caas) certs. they get first priority
|
const certFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`);
|
||||||
var certFilePath = path.join(paths.NGINX_CERT_DIR, `${domain}.host.cert`);
|
const keyFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.key`);
|
||||||
var keyFilePath = path.join(paths.NGINX_CERT_DIR, `${domain}.host.key`);
|
|
||||||
|
|
||||||
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
|
|
||||||
|
|
||||||
// check for auto-generated or user set fallback certs
|
|
||||||
certFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`);
|
|
||||||
keyFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.key`);
|
|
||||||
|
|
||||||
callback(null, { certFilePath, keyFilePath });
|
callback(null, { certFilePath, keyFilePath });
|
||||||
}
|
}
|
||||||
@@ -267,15 +246,12 @@ function setAppCertificateSync(location, domainObject, certificate) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCertificateByHostname(hostname, domainObject, callback) {
|
function getAcmeCertificate(hostname, domainObject, callback) {
|
||||||
assert.strictEqual(typeof hostname, 'string');
|
assert.strictEqual(typeof hostname, 'string');
|
||||||
assert.strictEqual(typeof domainObject, 'object');
|
assert.strictEqual(typeof domainObject, 'object');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
let certFilePath = path.join(paths.APP_CERTS_DIR, `${hostname}.user.cert`);
|
let certFilePath, keyFilePath;
|
||||||
let keyFilePath = path.join(paths.APP_CERTS_DIR, `${hostname}.user.key`);
|
|
||||||
|
|
||||||
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
|
|
||||||
|
|
||||||
if (hostname !== domainObject.domain && domainObject.tlsConfig.wildcard) { // bare domain is not part of wildcard SAN
|
if (hostname !== domainObject.domain && domainObject.tlsConfig.wildcard) { // bare domain is not part of wildcard SAN
|
||||||
let certName = domains.makeWildcard(hostname).replace('*.', '_.');
|
let certName = domains.makeWildcard(hostname).replace('*.', '_.');
|
||||||
@@ -298,10 +274,22 @@ function getCertificate(fqdn, domain, callback) {
|
|||||||
assert.strictEqual(typeof domain, 'string');
|
assert.strictEqual(typeof domain, 'string');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
// 1. user cert always wins
|
||||||
|
// 2. if using fallback provider, return that cert
|
||||||
|
// 3. look for LE certs
|
||||||
|
|
||||||
domains.get(domain, function (error, domainObject) {
|
domains.get(domain, function (error, domainObject) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
getCertificateByHostname(fqdn, domainObject, function (error, result) {
|
// user cert always wins
|
||||||
|
let certFilePath = path.join(paths.APP_CERTS_DIR, `${fqdn}.user.cert`);
|
||||||
|
let keyFilePath = path.join(paths.APP_CERTS_DIR, `${fqdn}.user.key`);
|
||||||
|
|
||||||
|
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
|
||||||
|
|
||||||
|
if (domainObject.tlsConfig.provider === 'fallback') return getFallbackCertificate(domain, callback);
|
||||||
|
|
||||||
|
getAcmeCertificate(fqdn, domainObject, function (error, result) {
|
||||||
if (error || result) return callback(error, result);
|
if (error || result) return callback(error, result);
|
||||||
|
|
||||||
return getFallbackCertificate(domain, callback);
|
return getFallbackCertificate(domain, callback);
|
||||||
@@ -329,14 +317,32 @@ function ensureCertificate(vhost, domain, auditSource, callback) {
|
|||||||
domains.get(domain, function (error, domainObject) {
|
domains.get(domain, function (error, domainObject) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
getCertApi(domainObject, function (error, api, apiOptions) {
|
// user cert always wins
|
||||||
|
let certFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.user.cert`);
|
||||||
|
let keyFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.user.key`);
|
||||||
|
|
||||||
|
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) {
|
||||||
|
debug(`ensureCertificate: ${vhost} will use custom app certs`);
|
||||||
|
return callback(null, { certFilePath, keyFilePath }, { renewed: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (domainObject.tlsConfig.provider === 'fallback') {
|
||||||
|
debug(`ensureCertificate: ${vhost} will use fallback certs`);
|
||||||
|
|
||||||
|
return getFallbackCertificate(domain, function (error, bundle) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
callback(null, bundle, { renewed: false });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getAcmeApi(domainObject, function (error, acmeApi, apiOptions) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
getCertificateByHostname(vhost, domainObject, function (_error, currentBundle) {
|
getAcmeCertificate(vhost, domainObject, function (_error, currentBundle) {
|
||||||
if (currentBundle) {
|
if (currentBundle) {
|
||||||
debug(`ensureCertificate: ${vhost} certificate already exists at ${currentBundle.keyFilePath}`);
|
debug(`ensureCertificate: ${vhost} certificate already exists at ${currentBundle.keyFilePath}`);
|
||||||
|
|
||||||
if (currentBundle.certFilePath.endsWith('.user.cert')) return callback(null, currentBundle, { renewed: false }); // user certs cannot be renewed
|
|
||||||
if (!isExpiringSync(currentBundle.certFilePath, 24 * 30) && providerMatchesSync(domainObject, currentBundle.certFilePath, apiOptions)) return callback(null, currentBundle, { renewed: false });
|
if (!isExpiringSync(currentBundle.certFilePath, 24 * 30) && providerMatchesSync(domainObject, currentBundle.certFilePath, apiOptions)) return callback(null, currentBundle, { renewed: false });
|
||||||
debug(`ensureCertificate: ${vhost} cert require renewal`);
|
debug(`ensureCertificate: ${vhost} cert require renewal`);
|
||||||
} else {
|
} else {
|
||||||
@@ -345,7 +351,7 @@ function ensureCertificate(vhost, domain, auditSource, callback) {
|
|||||||
|
|
||||||
debug('ensureCertificate: getting certificate for %s with options %j', vhost, apiOptions);
|
debug('ensureCertificate: getting certificate for %s with options %j', vhost, apiOptions);
|
||||||
|
|
||||||
api.getCertificate(vhost, domain, apiOptions, function (error, certFilePath, keyFilePath) {
|
acmeApi.getCertificate(vhost, domain, apiOptions, function (error, certFilePath, keyFilePath) {
|
||||||
debug(`ensureCertificate: error: ${error ? error.message : 'null'} cert: ${certFilePath || 'null'}`);
|
debug(`ensureCertificate: error: ${error ? error.message : 'null'} cert: ${certFilePath || 'null'}`);
|
||||||
|
|
||||||
eventlog.add(currentBundle ? eventlog.ACTION_CERTIFICATE_RENEWAL : eventlog.ACTION_CERTIFICATE_NEW, auditSource, { domain: vhost, errorMessage: error ? error.message : '' });
|
eventlog.add(currentBundle ? eventlog.ACTION_CERTIFICATE_RENEWAL : eventlog.ACTION_CERTIFICATE_NEW, auditSource, { domain: vhost, errorMessage: error ? error.message : '' });
|
||||||
@@ -362,7 +368,6 @@ function ensureCertificate(vhost, domain, auditSource, callback) {
|
|||||||
|
|
||||||
debug(`ensureCertificate: renewal of ${vhost} failed. using fallback certificates for ${domain}`);
|
debug(`ensureCertificate: renewal of ${vhost} failed. using fallback certificates for ${domain}`);
|
||||||
|
|
||||||
// if no cert was returned use fallback. the fallback/caas provider will not provide any for example
|
|
||||||
getFallbackCertificate(domain, function (error, bundle) {
|
getFallbackCertificate(domain, function (error, bundle) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
|||||||
+22
-1
@@ -30,6 +30,7 @@ exports = module.exports = {
|
|||||||
setMailbox: setMailbox,
|
setMailbox: setMailbox,
|
||||||
setLocation: setLocation,
|
setLocation: setLocation,
|
||||||
setDataDir: setDataDir,
|
setDataDir: setDataDir,
|
||||||
|
setBinds: setBinds,
|
||||||
|
|
||||||
stop: stop,
|
stop: stop,
|
||||||
start: start,
|
start: start,
|
||||||
@@ -427,7 +428,7 @@ function importApp(req, res, next) {
|
|||||||
|
|
||||||
if (req.body.backupConfig) {
|
if (req.body.backupConfig) {
|
||||||
if (typeof backupConfig.provider !== 'string') return next(new HttpError(400, 'provider is required'));
|
if (typeof backupConfig.provider !== 'string') return next(new HttpError(400, 'provider is required'));
|
||||||
if ('key' in backupConfig && typeof backupConfig.key !== 'string') return next(new HttpError(400, 'key must be a string'));
|
if ('password' in backupConfig && typeof backupConfig.password !== 'string') return next(new HttpError(400, 'password must be a string'));
|
||||||
if ('acceptSelfSignedCerts' in backupConfig && typeof backupConfig.acceptSelfSignedCerts !== 'boolean') return next(new HttpError(400, 'format must be a boolean'));
|
if ('acceptSelfSignedCerts' in backupConfig && typeof backupConfig.acceptSelfSignedCerts !== 'boolean') return next(new HttpError(400, 'format must be a boolean'));
|
||||||
|
|
||||||
// testing backup config can take sometime
|
// testing backup config can take sometime
|
||||||
@@ -764,3 +765,23 @@ function downloadFile(req, res, next) {
|
|||||||
stream.pipe(res);
|
stream.pipe(res);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setBinds(req, res, next) {
|
||||||
|
assert.strictEqual(typeof req.body, 'object');
|
||||||
|
assert.strictEqual(typeof req.resource, 'object');
|
||||||
|
|
||||||
|
if (!req.body.binds || typeof req.body.binds !== 'object') return next(new HttpError(400, 'binds should be an object'));
|
||||||
|
|
||||||
|
for (let name of Object.keys(req.body.binds)) {
|
||||||
|
if (!req.body.binds[name] || typeof req.body.binds[name] !== 'object') return next(new HttpError(400, 'each bind should be an object'));
|
||||||
|
if (typeof req.body.binds[name].hostPath !== 'string') return next(new HttpError(400, 'hostPath must be a string'));
|
||||||
|
if (typeof req.body.binds[name].readOnly !== 'boolean') return next(new HttpError(400, 'readOnly must be a boolean'));
|
||||||
|
}
|
||||||
|
|
||||||
|
apps.setBinds(req.resource, req.body.binds, auditSource.fromRequest(req), function (error, result) {
|
||||||
|
if (error) return next(BoxError.toHttpError(error));
|
||||||
|
|
||||||
|
next(new HttpSuccess(202, { taskId: result.taskId }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-3
@@ -3,11 +3,11 @@
|
|||||||
exports = module.exports = {
|
exports = module.exports = {
|
||||||
list: list,
|
list: list,
|
||||||
startBackup: startBackup,
|
startBackup: startBackup,
|
||||||
cleanup: cleanup
|
cleanup: cleanup,
|
||||||
|
check: check
|
||||||
};
|
};
|
||||||
|
|
||||||
let auditSource = require('../auditsource.js'),
|
let auditSource = require('../auditsource.js'),
|
||||||
backupdb = require('../backupdb.js'),
|
|
||||||
backups = require('../backups.js'),
|
backups = require('../backups.js'),
|
||||||
BoxError = require('../boxerror.js'),
|
BoxError = require('../boxerror.js'),
|
||||||
HttpError = require('connect-lastmile').HttpError,
|
HttpError = require('connect-lastmile').HttpError,
|
||||||
@@ -20,7 +20,7 @@ function list(req, res, next) {
|
|||||||
var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
|
var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
|
||||||
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
|
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
|
||||||
|
|
||||||
backups.getByStatePaged(backupdb.BACKUP_STATE_NORMAL, page, perPage, function (error, result) {
|
backups.getByIdentifierAndStatePaged(backups.BACKUP_IDENTIFIER_BOX, backups.BACKUP_STATE_NORMAL, page, perPage, function (error, result) {
|
||||||
if (error) return next(BoxError.toHttpError(error));
|
if (error) return next(BoxError.toHttpError(error));
|
||||||
|
|
||||||
next(new HttpSuccess(200, { backups: result }));
|
next(new HttpSuccess(200, { backups: result }));
|
||||||
@@ -42,3 +42,11 @@ function cleanup(req, res, next) {
|
|||||||
next(new HttpSuccess(202, { taskId }));
|
next(new HttpSuccess(202, { taskId }));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function check(req, res, next) {
|
||||||
|
backups.checkConfiguration(function (error, message) {
|
||||||
|
if (error) return next(BoxError.toHttpError(error));
|
||||||
|
|
||||||
|
next(new HttpSuccess(200, { ok: !message, message: message }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
+16
-29
@@ -86,8 +86,8 @@ function logout(req, res) {
|
|||||||
function passwordResetRequest(req, res, next) {
|
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'));
|
if (!req.body.identifier || typeof req.body.identifier !== 'string') return next(new HttpError(401, 'A identifier must be non-empty string'));
|
||||||
|
|
||||||
users.resetPasswordByIdentifier(req.body.identifier, function (error) {
|
users.sendPasswordResetByIdentifier(req.body.identifier, function (error) {
|
||||||
if (error && error.reason !== BoxError.NOT_FOUND) console.error(error);
|
if (error && error.reason !== BoxError.NOT_FOUND) return next(BoxError.toHttpError(error));
|
||||||
|
|
||||||
next(new HttpSuccess(202, {}));
|
next(new HttpSuccess(202, {}));
|
||||||
});
|
});
|
||||||
@@ -102,16 +102,17 @@ function passwordReset(req, res, next) {
|
|||||||
users.getByResetToken(req.body.resetToken, function (error, userObject) {
|
users.getByResetToken(req.body.resetToken, function (error, userObject) {
|
||||||
if (error) return next(new HttpError(401, 'Invalid resetToken'));
|
if (error) return next(new HttpError(401, 'Invalid resetToken'));
|
||||||
|
|
||||||
if (Date.now() - userObject.resetTokenCreationTime > 24 * 60 * 60 * 1000) return next(new HttpError(401, 'Token expired'));
|
// if you fix the duration here, the emails and UI have to be fixed as well
|
||||||
|
if (Date.now() - userObject.resetTokenCreationTime > 7 * 24 * 60 * 60 * 1000) return next(new HttpError(401, 'Token expired'));
|
||||||
if (!userObject.username) return next(new HttpError(409, 'No username set'));
|
if (!userObject.username) return next(new HttpError(409, 'No username set'));
|
||||||
|
|
||||||
// setPassword clears the resetToken
|
// setPassword clears the resetToken
|
||||||
users.setPassword(userObject, req.body.password, function (error) {
|
users.setPassword(userObject, req.body.password, function (error) {
|
||||||
if (error && error.reason === BoxError.BAD_FIELD) return next(new HttpError(400, error.message));
|
if (error && error.reason === BoxError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||||
if (error) return next(new HttpError(500, error));
|
if (error) return next(BoxError.toHttpError(error));
|
||||||
|
|
||||||
tokens.add(tokens.ID_WEBADMIN, userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
|
tokens.add(tokens.ID_WEBADMIN, userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
|
||||||
if (error) return next(new HttpError(500, error));
|
if (error) return next(BoxError.toHttpError(error));
|
||||||
|
|
||||||
next(new HttpSuccess(202, { accessToken: result.accessToken }));
|
next(new HttpSuccess(202, { accessToken: result.accessToken }));
|
||||||
});
|
});
|
||||||
@@ -122,37 +123,23 @@ function passwordReset(req, res, next) {
|
|||||||
function setupAccount(req, res, next) {
|
function setupAccount(req, res, next) {
|
||||||
assert.strictEqual(typeof req.body, 'object');
|
assert.strictEqual(typeof req.body, 'object');
|
||||||
|
|
||||||
if (!req.body.email || typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be a non-empty string'));
|
|
||||||
if (!req.body.resetToken || typeof req.body.resetToken !== 'string') return next(new HttpError(400, 'resetToken must be a non-empty string'));
|
if (!req.body.resetToken || typeof req.body.resetToken !== 'string') return next(new HttpError(400, 'resetToken must be a non-empty string'));
|
||||||
if (!req.body.password || typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be a non-empty string'));
|
if (!req.body.password || typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be a non-empty string'));
|
||||||
if (!req.body.username || typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be a non-empty string'));
|
|
||||||
if (!req.body.displayName || typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be a non-empty string'));
|
// only sent if profile is not locked
|
||||||
|
if ('username' in req.body && typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be a non-empty string'));
|
||||||
|
if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be a non-empty string'));
|
||||||
|
|
||||||
users.getByResetToken(req.body.resetToken, function (error, userObject) {
|
users.getByResetToken(req.body.resetToken, function (error, userObject) {
|
||||||
if (error) return next(new HttpError(401, 'Invalid Reset Token'));
|
if (error) return next(new HttpError(401, 'Invalid Reset Token'));
|
||||||
|
|
||||||
|
// if you fix the duration here, the emails and UI have to be fixed as well
|
||||||
if (Date.now() - userObject.resetTokenCreationTime > 24 * 60 * 60 * 1000) return next(new HttpError(401, 'Token expired'));
|
if (Date.now() - userObject.resetTokenCreationTime > 24 * 60 * 60 * 1000) return next(new HttpError(401, 'Token expired'));
|
||||||
|
|
||||||
users.update(userObject, { username: req.body.username, displayName: req.body.displayName }, auditSource.fromRequest(req), function (error) {
|
users.setupAccount(userObject, req.body, auditSource.fromRequest(req), function (error, accessToken) {
|
||||||
if (error && error.reason === BoxError.ALREADY_EXISTS) return next(new HttpError(409, 'Username already used'));
|
if (error) return next(BoxError.toHttpError(error));
|
||||||
if (error && error.reason === BoxError.BAD_FIELD) return next(new HttpError(400, error.message));
|
|
||||||
if (error && error.reason === BoxError.NOT_FOUND) return next(new HttpError(404, 'No such user'));
|
|
||||||
if (error) return next(new HttpError(500, error));
|
|
||||||
|
|
||||||
userObject.username = req.body.username;
|
next(new HttpSuccess(201, { accessToken }));
|
||||||
userObject.displayName = req.body.displayName;
|
|
||||||
|
|
||||||
// setPassword clears the resetToken
|
|
||||||
users.setPassword(userObject, req.body.password, function (error) {
|
|
||||||
if (error && error.reason === BoxError.BAD_FIELD) return next(new HttpError(400, error.message));
|
|
||||||
if (error) return next(new HttpError(500, error));
|
|
||||||
|
|
||||||
tokens.add(tokens.ID_WEBADMIN, userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
|
|
||||||
if (error) return next(new HttpError(500, error));
|
|
||||||
|
|
||||||
next(new HttpSuccess(201, { accessToken: result.accessToken }));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -218,8 +205,8 @@ function checkForUpdates(req, res, next) {
|
|||||||
req.clearTimeout();
|
req.clearTimeout();
|
||||||
|
|
||||||
async.series([
|
async.series([
|
||||||
updateChecker.checkAppUpdates,
|
(done) => updateChecker.checkAppUpdates({ automatic: false }, done),
|
||||||
updateChecker.checkBoxUpdates
|
(done) => updateChecker.checkBoxUpdates({ automatic: false }, done),
|
||||||
], function () {
|
], function () {
|
||||||
next(new HttpSuccess(200, { update: updateChecker.getUpdateInfo() }));
|
next(new HttpSuccess(200, { update: updateChecker.getUpdateInfo() }));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ function add(req, res, next) {
|
|||||||
let fallbackCertificate = req.body.fallbackCertificate;
|
let fallbackCertificate = req.body.fallbackCertificate;
|
||||||
if (!fallbackCertificate.cert || typeof fallbackCertificate.cert !== 'string') return next(new HttpError(400, 'fallbackCertificate.cert must be a string'));
|
if (!fallbackCertificate.cert || typeof fallbackCertificate.cert !== 'string') return next(new HttpError(400, 'fallbackCertificate.cert must be a string'));
|
||||||
if (!fallbackCertificate.key || typeof fallbackCertificate.key !== 'string') return next(new HttpError(400, 'fallbackCertificate.key must be a string'));
|
if (!fallbackCertificate.key || typeof fallbackCertificate.key !== 'string') return next(new HttpError(400, 'fallbackCertificate.key must be a string'));
|
||||||
if ('restricted' in fallbackCertificate && typeof fallbackCertificate.restricted !== 'boolean') return next(new HttpError(400, 'fallbackCertificate.restricted must be a boolean'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('tlsConfig' in req.body) {
|
if ('tlsConfig' in req.body) {
|
||||||
@@ -95,7 +94,6 @@ function update(req, res, next) {
|
|||||||
let fallbackCertificate = req.body.fallbackCertificate;
|
let fallbackCertificate = req.body.fallbackCertificate;
|
||||||
if (!fallbackCertificate.cert || typeof fallbackCertificate.cert !== 'string') return next(new HttpError(400, 'fallbackCertificate.cert must be a string'));
|
if (!fallbackCertificate.cert || typeof fallbackCertificate.cert !== 'string') return next(new HttpError(400, 'fallbackCertificate.cert must be a string'));
|
||||||
if (!fallbackCertificate.key || typeof fallbackCertificate.key !== 'string') return next(new HttpError(400, 'fallbackCertificate.key must be a string'));
|
if (!fallbackCertificate.key || typeof fallbackCertificate.key !== 'string') return next(new HttpError(400, 'fallbackCertificate.key must be a string'));
|
||||||
if ('restricted' in fallbackCertificate && typeof fallbackCertificate.restricted !== 'boolean') return next(new HttpError(400, 'fallbackCertificate.restricted must be a boolean'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('tlsConfig' in req.body) {
|
if ('tlsConfig' in req.body) {
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
exports = module.exports = {
|
||||||
|
proxy
|
||||||
|
};
|
||||||
|
|
||||||
|
var assert = require('assert'),
|
||||||
|
BoxError = require('../boxerror.js'),
|
||||||
|
docker = require('../docker.js'),
|
||||||
|
middleware = require('../middleware/index.js'),
|
||||||
|
HttpError = require('connect-lastmile').HttpError,
|
||||||
|
safe = require('safetydance'),
|
||||||
|
url = require('url');
|
||||||
|
|
||||||
|
function proxy(req, res, next) {
|
||||||
|
assert.strictEqual(typeof req.params.id, 'string');
|
||||||
|
|
||||||
|
const appId = req.params.id;
|
||||||
|
|
||||||
|
req.clearTimeout();
|
||||||
|
|
||||||
|
docker.inspect('sftp', function (error, result) {
|
||||||
|
if (error)return next(BoxError.toHttpError(error));
|
||||||
|
|
||||||
|
const ip = safe.query(result, 'NetworkSettings.Networks.cloudron.IPAddress', null);
|
||||||
|
if (!ip) return next(new BoxError(BoxError.INACTIVE, 'Error getting IP of sftp service'));
|
||||||
|
|
||||||
|
req.url = req.originalUrl.replace(`/api/v1/apps/${appId}/files`, `/files/${appId}`);
|
||||||
|
|
||||||
|
const proxyOptions = url.parse(`https://${ip}:3000`);
|
||||||
|
proxyOptions.rejectUnauthorized = false;
|
||||||
|
const fileManagerProxy = middleware.proxy(proxyOptions);
|
||||||
|
|
||||||
|
fileManagerProxy(req, res, function (error) {
|
||||||
|
if (!error) return next();
|
||||||
|
|
||||||
|
if (error.code === 'ECONNREFUSED') return next(new HttpError(424, 'Unable to connect to filemanager server'));
|
||||||
|
if (error.code === 'ECONNRESET') return next(new HttpError(424, 'Unable to query filemanager server'));
|
||||||
|
|
||||||
|
next(new HttpError(500, error));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -20,7 +20,9 @@ function create(req, res, next) {
|
|||||||
|
|
||||||
if (typeof req.body.name !== 'string') return next(new HttpError(400, 'name must be string'));
|
if (typeof req.body.name !== 'string') return next(new HttpError(400, 'name must be string'));
|
||||||
|
|
||||||
groups.create(req.body.name, function (error, group) {
|
var source = ''; // means local
|
||||||
|
|
||||||
|
groups.create(req.body.name, source, function (error, group) {
|
||||||
if (error) return next(BoxError.toHttpError(error));
|
if (error) return next(BoxError.toHttpError(error));
|
||||||
|
|
||||||
var groupInfo = {
|
var groupInfo = {
|
||||||
@@ -69,7 +71,7 @@ function updateMembers(req, res, next) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function list(req, res, next) {
|
function list(req, res, next) {
|
||||||
groups.getAll(function (error, result) {
|
groups.getAllWithMembers(function (error, result) {
|
||||||
if (error) return next(BoxError.toHttpError(error));
|
if (error) return next(BoxError.toHttpError(error));
|
||||||
|
|
||||||
next(new HttpSuccess(200, { groups: result }));
|
next(new HttpSuccess(200, { groups: result }));
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ exports = module.exports = {
|
|||||||
cloudron: require('./cloudron.js'),
|
cloudron: require('./cloudron.js'),
|
||||||
domains: require('./domains.js'),
|
domains: require('./domains.js'),
|
||||||
eventlog: require('./eventlog.js'),
|
eventlog: require('./eventlog.js'),
|
||||||
|
filemanager: require('./filemanager.js'),
|
||||||
graphs: require('./graphs.js'),
|
graphs: require('./graphs.js'),
|
||||||
groups: require('./groups.js'),
|
groups: require('./groups.js'),
|
||||||
mail: require('./mail.js'),
|
mail: require('./mail.js'),
|
||||||
|
|||||||
+55
-44
@@ -1,34 +1,35 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
exports = module.exports = {
|
exports = module.exports = {
|
||||||
getDomain: getDomain,
|
getDomain,
|
||||||
|
|
||||||
setDnsRecords: setDnsRecords,
|
setDnsRecords,
|
||||||
|
|
||||||
getStatus: getStatus,
|
getStatus,
|
||||||
|
|
||||||
setMailFromValidation: setMailFromValidation,
|
setMailFromValidation,
|
||||||
setCatchAllAddress: setCatchAllAddress,
|
setCatchAllAddress,
|
||||||
setMailRelay: setMailRelay,
|
setMailRelay,
|
||||||
setMailEnabled: setMailEnabled,
|
setMailEnabled,
|
||||||
|
|
||||||
sendTestMail: sendTestMail,
|
sendTestMail,
|
||||||
|
|
||||||
listMailboxes: listMailboxes,
|
listMailboxes,
|
||||||
getMailbox: getMailbox,
|
getMailbox,
|
||||||
addMailbox: addMailbox,
|
addMailbox,
|
||||||
updateMailbox: updateMailbox,
|
updateMailbox,
|
||||||
removeMailbox: removeMailbox,
|
removeMailbox,
|
||||||
|
|
||||||
listAliases: listAliases,
|
getAliases,
|
||||||
getAliases: getAliases,
|
setAliases,
|
||||||
setAliases: setAliases,
|
|
||||||
|
|
||||||
getLists: getLists,
|
getLists,
|
||||||
getList: getList,
|
getList,
|
||||||
addList: addList,
|
addList,
|
||||||
updateList: updateList,
|
updateList,
|
||||||
removeList: removeList,
|
removeList,
|
||||||
|
|
||||||
|
getMailboxCount
|
||||||
};
|
};
|
||||||
|
|
||||||
var assert = require('assert'),
|
var assert = require('assert'),
|
||||||
@@ -160,13 +161,25 @@ function listMailboxes(req, res, next) {
|
|||||||
var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
|
var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
|
||||||
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a positive number'));
|
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a positive number'));
|
||||||
|
|
||||||
mail.listMailboxes(req.params.domain, page, perPage, function (error, result) {
|
if (req.query.search && typeof req.query.search !== 'string') return next(new HttpError(400, 'search must be a string'));
|
||||||
|
|
||||||
|
mail.listMailboxes(req.params.domain, req.query.search || null, page, perPage, function (error, result) {
|
||||||
if (error) return next(BoxError.toHttpError(error));
|
if (error) return next(BoxError.toHttpError(error));
|
||||||
|
|
||||||
next(new HttpSuccess(200, { mailboxes: result }));
|
next(new HttpSuccess(200, { mailboxes: result }));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getMailboxCount(req, res, next) {
|
||||||
|
assert.strictEqual(typeof req.params.domain, 'string');
|
||||||
|
|
||||||
|
mail.getMailboxCount(req.params.domain, function (error, count) {
|
||||||
|
if (error) return next(BoxError.toHttpError(error));
|
||||||
|
|
||||||
|
next(new HttpSuccess(200, { count }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getMailbox(req, res, next) {
|
function getMailbox(req, res, next) {
|
||||||
assert.strictEqual(typeof req.params.domain, 'string');
|
assert.strictEqual(typeof req.params.domain, 'string');
|
||||||
assert.strictEqual(typeof req.params.name, 'string');
|
assert.strictEqual(typeof req.params.name, 'string');
|
||||||
@@ -208,29 +221,15 @@ function removeMailbox(req, res, next) {
|
|||||||
assert.strictEqual(typeof req.params.domain, 'string');
|
assert.strictEqual(typeof req.params.domain, 'string');
|
||||||
assert.strictEqual(typeof req.params.name, 'string');
|
assert.strictEqual(typeof req.params.name, 'string');
|
||||||
|
|
||||||
mail.removeMailbox(req.params.name, req.params.domain, auditSource.fromRequest(req), function (error) {
|
if (typeof req.body.deleteMails !== 'boolean') return next(new HttpError(400, 'deleteMails must be a boolean'));
|
||||||
|
|
||||||
|
mail.removeMailbox(req.params.name, req.params.domain, req.body, auditSource.fromRequest(req), function (error) {
|
||||||
if (error) return next(BoxError.toHttpError(error));
|
if (error) return next(BoxError.toHttpError(error));
|
||||||
|
|
||||||
next(new HttpSuccess(201, {}));
|
next(new HttpSuccess(201, {}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function listAliases(req, res, next) {
|
|
||||||
assert.strictEqual(typeof req.params.domain, 'string');
|
|
||||||
|
|
||||||
var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
|
|
||||||
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a positive number'));
|
|
||||||
|
|
||||||
var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
|
|
||||||
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a positive number'));
|
|
||||||
|
|
||||||
mail.listAliases(req.params.domain, page, perPage, function (error, result) {
|
|
||||||
if (error) return next(BoxError.toHttpError(error));
|
|
||||||
|
|
||||||
next(new HttpSuccess(200, { aliases: result }));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAliases(req, res, next) {
|
function getAliases(req, res, next) {
|
||||||
assert.strictEqual(typeof req.params.domain, 'string');
|
assert.strictEqual(typeof req.params.domain, 'string');
|
||||||
assert.strictEqual(typeof req.params.name, 'string');
|
assert.strictEqual(typeof req.params.name, 'string');
|
||||||
@@ -249,8 +248,10 @@ function setAliases(req, res, next) {
|
|||||||
|
|
||||||
if (!Array.isArray(req.body.aliases)) return next(new HttpError(400, 'aliases must be an array'));
|
if (!Array.isArray(req.body.aliases)) return next(new HttpError(400, 'aliases must be an array'));
|
||||||
|
|
||||||
for (var i = 0; i < req.body.aliases.length; i++) {
|
for (let alias of req.body.aliases) {
|
||||||
if (typeof req.body.aliases[i] !== 'string') return next(new HttpError(400, 'alias must be a string'));
|
if (!alias || typeof alias !== 'object') return next(new HttpError(400, 'each alias must have a name and domain'));
|
||||||
|
if (typeof alias.name !== 'string') return next(new HttpError(400, 'name must be a string'));
|
||||||
|
if (typeof alias.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
|
||||||
}
|
}
|
||||||
|
|
||||||
mail.setAliases(req.params.name, req.params.domain, req.body.aliases, function (error) {
|
mail.setAliases(req.params.name, req.params.domain, req.body.aliases, function (error) {
|
||||||
@@ -263,7 +264,15 @@ function setAliases(req, res, next) {
|
|||||||
function getLists(req, res, next) {
|
function getLists(req, res, next) {
|
||||||
assert.strictEqual(typeof req.params.domain, 'string');
|
assert.strictEqual(typeof req.params.domain, 'string');
|
||||||
|
|
||||||
mail.getLists(req.params.domain, function (error, result) {
|
const page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
|
||||||
|
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a positive number'));
|
||||||
|
|
||||||
|
const perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
|
||||||
|
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a positive number'));
|
||||||
|
|
||||||
|
if (req.query.search && typeof req.query.search !== 'string') return next(new HttpError(400, 'search must be a string'));
|
||||||
|
|
||||||
|
mail.getLists(req.params.domain, req.query.search || null, page, perPage, function (error, result) {
|
||||||
if (error) return next(BoxError.toHttpError(error));
|
if (error) return next(BoxError.toHttpError(error));
|
||||||
|
|
||||||
next(new HttpSuccess(200, { lists: result }));
|
next(new HttpSuccess(200, { lists: result }));
|
||||||
@@ -292,8 +301,9 @@ function addList(req, res, next) {
|
|||||||
for (var i = 0; i < req.body.members.length; i++) {
|
for (var i = 0; i < req.body.members.length; i++) {
|
||||||
if (typeof req.body.members[i] !== 'string') return next(new HttpError(400, 'member must be a string'));
|
if (typeof req.body.members[i] !== 'string') return next(new HttpError(400, 'member must be a string'));
|
||||||
}
|
}
|
||||||
|
if (typeof req.body.membersOnly !== 'boolean') return next(new HttpError(400, 'membersOnly must be a boolean'));
|
||||||
|
|
||||||
mail.addList(req.body.name, req.params.domain, req.body.members, auditSource.fromRequest(req), function (error) {
|
mail.addList(req.body.name, req.params.domain, req.body.members, req.body.membersOnly, auditSource.fromRequest(req), function (error) {
|
||||||
if (error) return next(BoxError.toHttpError(error));
|
if (error) return next(BoxError.toHttpError(error));
|
||||||
|
|
||||||
next(new HttpSuccess(201, {}));
|
next(new HttpSuccess(201, {}));
|
||||||
@@ -310,8 +320,9 @@ function updateList(req, res, next) {
|
|||||||
for (var i = 0; i < req.body.members.length; i++) {
|
for (var i = 0; i < req.body.members.length; i++) {
|
||||||
if (typeof req.body.members[i] !== 'string') return next(new HttpError(400, 'member must be a string'));
|
if (typeof req.body.members[i] !== 'string') return next(new HttpError(400, 'member must be a string'));
|
||||||
}
|
}
|
||||||
|
if (typeof req.body.membersOnly !== 'boolean') return next(new HttpError(400, 'membersOnly must be a boolean'));
|
||||||
|
|
||||||
mail.updateList(req.params.name, req.params.domain, req.body.members, auditSource.fromRequest(req), function (error) {
|
mail.updateList(req.params.name, req.params.domain, req.body.members, req.body.membersOnly, auditSource.fromRequest(req), function (error) {
|
||||||
if (error) return next(BoxError.toHttpError(error));
|
if (error) return next(BoxError.toHttpError(error));
|
||||||
|
|
||||||
next(new HttpSuccess(204));
|
next(new HttpSuccess(204));
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ function proxy(req, res, next) {
|
|||||||
delete req.headers['authorization'];
|
delete req.headers['authorization'];
|
||||||
delete req.headers['cookies'];
|
delete req.headers['cookies'];
|
||||||
|
|
||||||
addons.getServiceDetails('mail', 'CLOUDRON_MAIL_TOKEN', function (error, addonDetails) {
|
addons.getContainerDetails('mail', 'CLOUDRON_MAIL_TOKEN', function (error, addonDetails) {
|
||||||
if (error) return next(BoxError.toHttpError(error));
|
if (error) return next(BoxError.toHttpError(error));
|
||||||
|
|
||||||
parsedUrl.query['access_token'] = addonDetails.token;
|
parsedUrl.query['access_token'] = addonDetails.token;
|
||||||
|
|||||||
+34
-21
@@ -1,34 +1,41 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
exports = module.exports = {
|
exports = module.exports = {
|
||||||
get: get,
|
authorize,
|
||||||
update: update,
|
get,
|
||||||
getAvatar: getAvatar,
|
update,
|
||||||
setAvatar: setAvatar,
|
getAvatar,
|
||||||
clearAvatar: clearAvatar,
|
setAvatar,
|
||||||
changePassword: changePassword,
|
clearAvatar,
|
||||||
setTwoFactorAuthenticationSecret: setTwoFactorAuthenticationSecret,
|
changePassword,
|
||||||
enableTwoFactorAuthentication: enableTwoFactorAuthentication,
|
setTwoFactorAuthenticationSecret,
|
||||||
disableTwoFactorAuthentication: disableTwoFactorAuthentication
|
enableTwoFactorAuthentication,
|
||||||
|
disableTwoFactorAuthentication,
|
||||||
};
|
};
|
||||||
|
|
||||||
var assert = require('assert'),
|
var assert = require('assert'),
|
||||||
auditSource = require('../auditsource.js'),
|
auditSource = require('../auditsource.js'),
|
||||||
BoxError = require('../boxerror.js'),
|
BoxError = require('../boxerror.js'),
|
||||||
fs = require('fs'),
|
|
||||||
HttpError = require('connect-lastmile').HttpError,
|
HttpError = require('connect-lastmile').HttpError,
|
||||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||||
path = require('path'),
|
|
||||||
paths = require('../paths.js'),
|
|
||||||
safe = require('safetydance'),
|
|
||||||
users = require('../users.js'),
|
users = require('../users.js'),
|
||||||
settings = require('../settings.js'),
|
settings = require('../settings.js'),
|
||||||
_ = require('underscore');
|
_ = require('underscore');
|
||||||
|
|
||||||
function get(req, res, next) {
|
function authorize(req, res, next) {
|
||||||
assert.strictEqual(typeof req.user, 'object');
|
assert.strictEqual(typeof req.user, 'object');
|
||||||
|
|
||||||
const emailHash = require('crypto').createHash('md5').update(req.user.email).digest('hex');
|
settings.getDirectoryConfig(function (error, directoryConfig) {
|
||||||
|
if (error) return next(BoxError.toHttpError(error));
|
||||||
|
|
||||||
|
if (directoryConfig.lockUserProfiles) return next(new HttpError(403, 'admin has disallowed users from editing profiles'));
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(req, res, next) {
|
||||||
|
assert.strictEqual(typeof req.user, 'object');
|
||||||
|
|
||||||
next(new HttpSuccess(200, {
|
next(new HttpSuccess(200, {
|
||||||
id: req.user.id,
|
id: req.user.id,
|
||||||
@@ -39,7 +46,7 @@ function get(req, res, next) {
|
|||||||
twoFactorAuthenticationEnabled: req.user.twoFactorAuthenticationEnabled,
|
twoFactorAuthenticationEnabled: req.user.twoFactorAuthenticationEnabled,
|
||||||
role: req.user.role,
|
role: req.user.role,
|
||||||
source: req.user.source,
|
source: req.user.source,
|
||||||
avatarUrl: fs.existsSync(path.join(paths.PROFILE_ICONS_DIR, req.user.id)) ? `${settings.adminOrigin()}/api/v1/profile/avatar/${req.user.id}` : `https://www.gravatar.com/avatar/${emailHash}.jpg`
|
avatarUrl: users.getAvatarUrlSync(req.user)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,21 +72,27 @@ function setAvatar(req, res, next) {
|
|||||||
|
|
||||||
if (!req.files.avatar) return next(new HttpError(400, 'avatar is missing'));
|
if (!req.files.avatar) return next(new HttpError(400, 'avatar is missing'));
|
||||||
|
|
||||||
if (!safe.fs.renameSync(req.files.avatar.path, path.join(paths.PROFILE_ICONS_DIR, req.user.id))) return next(new HttpError(500, safe.error));
|
users.setAvatar(req.user.id, req.files.avatar.path, function (error) {
|
||||||
|
if (error) return next(BoxError.toHttpError(error));
|
||||||
|
|
||||||
next(new HttpSuccess(202, {}));
|
next(new HttpSuccess(202, {}));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearAvatar(req, res, next) {
|
function clearAvatar(req, res, next) {
|
||||||
assert.strictEqual(typeof req.user, 'object');
|
assert.strictEqual(typeof req.user, 'object');
|
||||||
|
|
||||||
safe.fs.unlinkSync(path.join(paths.PROFILE_ICONS_DIR, req.user.id));
|
users.clearAvatar(req.user.id, function (error) {
|
||||||
|
if (error) return next(BoxError.toHttpError(error));
|
||||||
|
|
||||||
next(new HttpSuccess(202, {}));
|
next(new HttpSuccess(202, {}));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAvatar(req, res) {
|
function getAvatar(req, res) {
|
||||||
res.sendFile(path.join(paths.PROFILE_ICONS_DIR, req.params.identifier));
|
assert.strictEqual(typeof req.params.identifier, 'string');
|
||||||
|
|
||||||
|
res.sendFile(users.getAvatarFileSync(req.params.identifier));
|
||||||
}
|
}
|
||||||
|
|
||||||
function changePassword(req, res, next) {
|
function changePassword(req, res, next) {
|
||||||
|
|||||||
@@ -98,11 +98,11 @@ function restore(req, res, next) {
|
|||||||
|
|
||||||
var backupConfig = req.body.backupConfig;
|
var backupConfig = req.body.backupConfig;
|
||||||
if (typeof backupConfig.provider !== 'string') return next(new HttpError(400, 'provider is required'));
|
if (typeof backupConfig.provider !== 'string') return next(new HttpError(400, 'provider is required'));
|
||||||
if ('key' in backupConfig && typeof backupConfig.key !== 'string') return next(new HttpError(400, 'key must be a string'));
|
if ('password' in backupConfig && typeof backupConfig.password !== 'string') return next(new HttpError(400, 'password must be a string'));
|
||||||
if (typeof backupConfig.format !== 'string') return next(new HttpError(400, 'format must be a string'));
|
if (typeof backupConfig.format !== 'string') return next(new HttpError(400, 'format must be a string'));
|
||||||
if ('acceptSelfSignedCerts' in backupConfig && typeof backupConfig.acceptSelfSignedCerts !== 'boolean') return next(new HttpError(400, 'format must be a boolean'));
|
if ('acceptSelfSignedCerts' in backupConfig && typeof backupConfig.acceptSelfSignedCerts !== 'boolean') return next(new HttpError(400, 'format must be a boolean'));
|
||||||
|
|
||||||
if (typeof req.body.backupId !== 'string') return next(new HttpError(400, 'backupId must be a string or null'));
|
if (typeof req.body.backupId !== 'string') return next(new HttpError(400, 'backupId must be a string'));
|
||||||
if (typeof req.body.version !== 'string') return next(new HttpError(400, 'version must be a string'));
|
if (typeof req.body.version !== 'string') return next(new HttpError(400, 'version must be a string'));
|
||||||
|
|
||||||
if ('sysinfoConfig' in req.body && typeof req.body.sysinfoConfig !== 'object') return next(new HttpError(400, 'sysinfoConfig must be an object'));
|
if ('sysinfoConfig' in req.body && typeof req.body.sysinfoConfig !== 'object') return next(new HttpError(400, 'sysinfoConfig must be an object'));
|
||||||
@@ -122,7 +122,7 @@ function getStatus(req, res, next) {
|
|||||||
|
|
||||||
// check if Cloudron is not in setup state nor activated and let appstore know of the attempt
|
// check if Cloudron is not in setup state nor activated and let appstore know of the attempt
|
||||||
if (!status.activated && !status.setup.active && !status.restore.active) {
|
if (!status.activated && !status.setup.active && !status.restore.active) {
|
||||||
appstore.trackBeginSetup(status.provider);
|
appstore.trackBeginSetup();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-3
@@ -97,16 +97,30 @@ function setBackupConfig(req, res, next) {
|
|||||||
assert.strictEqual(typeof req.body, 'object');
|
assert.strictEqual(typeof req.body, 'object');
|
||||||
|
|
||||||
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required'));
|
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required'));
|
||||||
if (typeof req.body.retentionSecs !== 'number') return next(new HttpError(400, 'retentionSecs is required'));
|
if (typeof req.body.schedulePattern !== 'string') return next(new HttpError(400, 'schedulePattern is required'));
|
||||||
if (typeof req.body.intervalSecs !== 'number') return next(new HttpError(400, 'intervalSecs is required'));
|
if ('password' in req.body && typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be a string'));
|
||||||
if ('key' in req.body && typeof req.body.key !== 'string') return next(new HttpError(400, 'key must be a string'));
|
|
||||||
if ('syncConcurrency' in req.body) {
|
if ('syncConcurrency' in req.body) {
|
||||||
if (typeof req.body.syncConcurrency !== 'number') return next(new HttpError(400, 'syncConcurrency must be a positive integer'));
|
if (typeof req.body.syncConcurrency !== 'number') return next(new HttpError(400, 'syncConcurrency must be a positive integer'));
|
||||||
if (req.body.syncConcurrency < 1) return next(new HttpError(400, 'syncConcurrency must be a positive integer'));
|
if (req.body.syncConcurrency < 1) return next(new HttpError(400, 'syncConcurrency must be a positive integer'));
|
||||||
}
|
}
|
||||||
|
if ('copyConcurrency' in req.body) {
|
||||||
|
if (typeof req.body.copyConcurrency !== 'number') return next(new HttpError(400, 'copyConcurrency must be a positive integer'));
|
||||||
|
if (req.body.copyConcurrency < 1) return next(new HttpError(400, 'copyConcurrency must be a positive integer'));
|
||||||
|
}
|
||||||
|
if ('downloadConcurrency' in req.body) {
|
||||||
|
if (typeof req.body.downloadConcurrency !== 'number') return next(new HttpError(400, 'downloadConcurrency must be a positive integer'));
|
||||||
|
if (req.body.downloadConcurrency < 1) return next(new HttpError(400, 'downloadConcurrency must be a positive integer'));
|
||||||
|
}
|
||||||
|
if ('deleteConcurrency' in req.body) {
|
||||||
|
if (typeof req.body.deleteConcurrency !== 'number') return next(new HttpError(400, 'deleteConcurrency must be a positive integer'));
|
||||||
|
if (req.body.deleteConcurrency < 1) return next(new HttpError(400, 'deleteConcurrency must be a positive integer'));
|
||||||
|
}
|
||||||
|
if ('memoryLimit' in req.body && typeof req.body.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit must be a positive integer'));
|
||||||
if (typeof req.body.format !== 'string') return next(new HttpError(400, 'format must be a string'));
|
if (typeof req.body.format !== 'string') return next(new HttpError(400, 'format must be a string'));
|
||||||
if ('acceptSelfSignedCerts' in req.body && typeof req.body.acceptSelfSignedCerts !== 'boolean') return next(new HttpError(400, 'format must be a boolean'));
|
if ('acceptSelfSignedCerts' in req.body && typeof req.body.acceptSelfSignedCerts !== 'boolean') return next(new HttpError(400, 'format must be a boolean'));
|
||||||
|
|
||||||
|
if (!req.body.retentionPolicy || typeof req.body.retentionPolicy !== 'object') return next(new HttpError(400, 'retentionPolicy is required'));
|
||||||
|
|
||||||
// testing the backup using put/del takes a bit of time at times
|
// testing the backup using put/del takes a bit of time at times
|
||||||
req.clearTimeout();
|
req.clearTimeout();
|
||||||
|
|
||||||
@@ -159,6 +173,7 @@ function setExternalLdapConfig(req, res, next) {
|
|||||||
if ('baseDn' in req.body && typeof req.body.baseDn !== 'string') return next(new HttpError(400, 'baseDn must be a string'));
|
if ('baseDn' in req.body && typeof req.body.baseDn !== 'string') return next(new HttpError(400, 'baseDn must be a string'));
|
||||||
if ('usernameField' in req.body && typeof req.body.usernameField !== 'string') return next(new HttpError(400, 'usernameField must be a string'));
|
if ('usernameField' in req.body && typeof req.body.usernameField !== 'string') return next(new HttpError(400, 'usernameField must be a string'));
|
||||||
if ('filter' in req.body && typeof req.body.filter !== 'string') return next(new HttpError(400, 'filter must be a string'));
|
if ('filter' in req.body && typeof req.body.filter !== 'string') return next(new HttpError(400, 'filter must be a string'));
|
||||||
|
if ('groupBaseDn' in req.body && typeof req.body.groupBaseDn !== 'string') return next(new HttpError(400, 'groupBaseDn must be a string'));
|
||||||
if ('bindDn' in req.body && typeof req.body.bindDn !== 'string') return next(new HttpError(400, 'bindDn must be a non empty string'));
|
if ('bindDn' in req.body && typeof req.body.bindDn !== 'string') return next(new HttpError(400, 'bindDn must be a non empty string'));
|
||||||
if ('bindPassword' in req.body && typeof req.body.bindPassword !== 'string') return next(new HttpError(400, 'bindPassword must be a string'));
|
if ('bindPassword' in req.body && typeof req.body.bindPassword !== 'string') return next(new HttpError(400, 'bindPassword must be a string'));
|
||||||
|
|
||||||
@@ -232,6 +247,27 @@ function setRegistryConfig(req, res, next) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDirectoryConfig(req, res, next) {
|
||||||
|
settings.getDirectoryConfig(function (error, directoryConfig) {
|
||||||
|
if (error) return next(BoxError.toHttpError(error));
|
||||||
|
|
||||||
|
next(new HttpSuccess(200, directoryConfig));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDirectoryConfig(req, res, next) {
|
||||||
|
assert.strictEqual(typeof req.body, 'object');
|
||||||
|
|
||||||
|
if (typeof req.body.lockUserProfiles !== 'boolean') return next(new HttpError(400, 'lockUserProfiles is required'));
|
||||||
|
if (typeof req.body.mandatory2FA !== 'boolean') return next(new HttpError(400, 'mandatory2FA is required'));
|
||||||
|
|
||||||
|
settings.setDirectoryConfig(req.body, function (error) {
|
||||||
|
if (error) return next(BoxError.toHttpError(error));
|
||||||
|
|
||||||
|
next(new HttpSuccess(200, {}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getSysinfoConfig(req, res, next) {
|
function getSysinfoConfig(req, res, next) {
|
||||||
settings.getSysinfoConfig(function (error, sysinfoConfig) {
|
settings.getSysinfoConfig(function (error, sysinfoConfig) {
|
||||||
if (error) return next(BoxError.toHttpError(error));
|
if (error) return next(BoxError.toHttpError(error));
|
||||||
@@ -268,6 +304,7 @@ function get(req, res, next) {
|
|||||||
case settings.BOX_AUTOUPDATE_PATTERN_KEY: return getBoxAutoupdatePattern(req, res, next);
|
case settings.BOX_AUTOUPDATE_PATTERN_KEY: return getBoxAutoupdatePattern(req, res, next);
|
||||||
case settings.TIME_ZONE_KEY: return getTimeZone(req, res, next);
|
case settings.TIME_ZONE_KEY: return getTimeZone(req, res, next);
|
||||||
|
|
||||||
|
case settings.DIRECTORY_CONFIG_KEY: return getDirectoryConfig(req, res, next);
|
||||||
case settings.SUPPORT_CONFIG_KEY: return getSupportConfig(req, res, next);
|
case settings.SUPPORT_CONFIG_KEY: return getSupportConfig(req, res, next);
|
||||||
|
|
||||||
default: return next(new HttpError(404, 'No such setting'));
|
default: return next(new HttpError(404, 'No such setting'));
|
||||||
@@ -289,6 +326,8 @@ function set(req, res, next) {
|
|||||||
case settings.BOX_AUTOUPDATE_PATTERN_KEY: return setBoxAutoupdatePattern(req, res, next);
|
case settings.BOX_AUTOUPDATE_PATTERN_KEY: return setBoxAutoupdatePattern(req, res, next);
|
||||||
case settings.TIME_ZONE_KEY: return setTimeZone(req, res, next);
|
case settings.TIME_ZONE_KEY: return setTimeZone(req, res, next);
|
||||||
|
|
||||||
|
case settings.DIRECTORY_CONFIG_KEY: return setDirectoryConfig(req, res, next);
|
||||||
|
|
||||||
default: return next(new HttpError(404, 'No such setting'));
|
default: return next(new HttpError(404, 'No such setting'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ function setup(done) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
function createSettings(callback) {
|
function createSettings(callback) {
|
||||||
settings.setBackupConfig({ provider: 'filesystem', backupFolder: '/tmp', format: 'tgz' }, callback);
|
settings.setBackupConfig({ provider: 'filesystem', backupFolder: '/tmp', format: 'tgz', retentionPolicy: { keepWithinSecs: 2 * 24 * 60 * 60 }, schedulePattern: '00 00 23 * * *' }, callback);
|
||||||
}
|
}
|
||||||
], done);
|
], done);
|
||||||
}
|
}
|
||||||
@@ -108,4 +108,35 @@ describe('Backups API', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('check', function () {
|
||||||
|
it('fails due to mising token', function (done) {
|
||||||
|
superagent.get(SERVER_URL + '/api/v1/backups/check')
|
||||||
|
.end(function (error, result) {
|
||||||
|
expect(result.statusCode).to.equal(401);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails due to wrong token', function (done) {
|
||||||
|
superagent.get(SERVER_URL + '/api/v1/backups/check')
|
||||||
|
.query({ access_token: token.toUpperCase() })
|
||||||
|
.end(function (error, result) {
|
||||||
|
expect(result.statusCode).to.equal(401);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('succeeds', function (done) {
|
||||||
|
superagent.get(SERVER_URL + '/api/v1/backups/check')
|
||||||
|
.query({ access_token: token })
|
||||||
|
.end(function (error, result) {
|
||||||
|
expect(result.statusCode).to.equal(200);
|
||||||
|
expect(result.body.ok).to.equal(false);
|
||||||
|
expect(result.body.message).to.not.be.empty();
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ function setup(done) {
|
|||||||
server.start.bind(server),
|
server.start.bind(server),
|
||||||
database._clear,
|
database._clear,
|
||||||
settings._setApiServerOrigin.bind(null, 'http://localhost:6060'),
|
settings._setApiServerOrigin.bind(null, 'http://localhost:6060'),
|
||||||
settings.setBackupConfig.bind(null, { provider: 'filesystem', backupFolder: '/tmp', format: 'tgz' })
|
settings.setBackupConfig.bind(null, { provider: 'filesystem', backupFolder: '/tmp', format: 'tgz', retentionPolicy: { keepWithinSecs: 10000 }, schedulePattern: '00 00 23 * * *' })
|
||||||
], done);
|
], done);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ function cleanup(done) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('Cloudron', function () {
|
describe('Cloudron API', function () {
|
||||||
|
|
||||||
describe('activate', function () {
|
describe('activate', function () {
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ describe('Groups API', function () {
|
|||||||
|
|
||||||
it('create fails due to mising token', function (done) {
|
it('create fails due to mising token', function (done) {
|
||||||
superagent.post(SERVER_URL + '/api/v1/groups')
|
superagent.post(SERVER_URL + '/api/v1/groups')
|
||||||
.send({ name: GROUP_NAME})
|
.send({ name: GROUP_NAME })
|
||||||
.end(function (error, result) {
|
.end(function (error, result) {
|
||||||
expect(result.statusCode).to.equal(401);
|
expect(result.statusCode).to.equal(401);
|
||||||
done();
|
done();
|
||||||
@@ -96,7 +96,7 @@ describe('Groups API', function () {
|
|||||||
it('create succeeds', function (done) {
|
it('create succeeds', function (done) {
|
||||||
superagent.post(SERVER_URL + '/api/v1/groups')
|
superagent.post(SERVER_URL + '/api/v1/groups')
|
||||||
.query({ access_token: token })
|
.query({ access_token: token })
|
||||||
.send({ name: GROUP_NAME})
|
.send({ name: GROUP_NAME })
|
||||||
.end(function (error, result) {
|
.end(function (error, result) {
|
||||||
expect(result.statusCode).to.equal(201);
|
expect(result.statusCode).to.equal(201);
|
||||||
groupObject = result.body;
|
groupObject = result.body;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user