Compare commits
438 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f497d5d309 | |||
| 51a165dc7a | |||
| 9d4082356b | |||
| 3b8bc47ee5 | |||
| 78752fde7a | |||
| c6fd922fcd | |||
| e90a211820 | |||
| 8529485837 | |||
| 6810d823f5 | |||
| 3e62f1913a | |||
| d23662c464 | |||
| 922c1ea317 | |||
| 258d81d7e9 | |||
| 1363e02603 | |||
| ccc65127f1 | |||
| 3b38bb5d33 | |||
| 59c51c5747 | |||
| ca17afc734 | |||
| 0b537fe163 | |||
| 2a32bf3fc7 | |||
| 57c4d47657 | |||
| 0371fe19ab | |||
| 3de8fd5d92 | |||
| ce86cb892d | |||
| 9789ae3374 | |||
| e508893dcc | |||
| 699f04c9ff | |||
| 89c82fb001 | |||
| b7fed04c12 | |||
| 0ec5714271 | |||
| 5e483e4f3a | |||
| 84374b955e | |||
| 3a25c8da9f | |||
| 5a5983cf96 | |||
| 71c44a4c44 | |||
| 41053d6857 | |||
| 4287642308 | |||
| 3934e59bd3 | |||
| 9080e5c3ab | |||
| 3d5599cdd9 | |||
| 138d01e755 | |||
| 213ce114e3 | |||
| ad8b9cfc9f | |||
| de400dd652 | |||
| 6218ee30a7 | |||
| 976f072ef4 | |||
| f4762be58b | |||
| 1b92ce08aa | |||
| 1d3d8288a9 | |||
| eec54e93bf | |||
| 77b965cada | |||
| bcc9eda66c | |||
| 3a0b9d7b3b | |||
| e511b70d8f | |||
| 25cc60e648 | |||
| d1e05dcb6f | |||
| 8cfd859711 | |||
| 7b3b826f87 | |||
| 195c9bd81f | |||
| a8928d26d1 | |||
| ef287d4436 | |||
| 6ae1de6989 | |||
| 9c810ce837 | |||
| ba913bb949 | |||
| 58487b729a | |||
| bf73cbaf97 | |||
| 1db868bf9c | |||
| d331597bff | |||
| 71648d92ae | |||
| 735485b539 | |||
| 09c8248e31 | |||
| c0b0029935 | |||
| 64af278f39 | |||
| 57dabbfc69 | |||
| 279f7a80c5 | |||
| b66fdb10f2 | |||
| 84c1703c1a | |||
| f324d50cef | |||
| 93a1e6fca8 | |||
| 4d55783ed8 | |||
| aad50fb5b2 | |||
| fb4ba5855b | |||
| fbe5f42536 | |||
| 7663360ce6 | |||
| 0a3aad0205 | |||
| cde42e5f92 | |||
| fd965072c5 | |||
| d703d1cd13 | |||
| bd9c664b1a | |||
| ae94ff1432 | |||
| b64acb412e | |||
| cbc5ec7d89 | |||
| 5401dc9e18 | |||
| 9b37597ac8 | |||
| 784c8b2bd2 | |||
| 2388fe5047 | |||
| 064eff0ac1 | |||
| b5c933494a | |||
| 8c0bd97064 | |||
| 2ca9534715 | |||
| 641704a741 | |||
| 82d88d375e | |||
| 751caa7b3b | |||
| 7e16128b11 | |||
| 008fa09877 | |||
| 045963afe5 | |||
| b799df3626 | |||
| 772df6f9af | |||
| 72cb383f2c | |||
| 625dc7c49b | |||
| 86916a94de | |||
| 71666a028b | |||
| 01e6301332 | |||
| 13050f7bff | |||
| bedcd6fccf | |||
| df8a71cd8b | |||
| a113ece22b | |||
| a63c2cfdf2 | |||
| 8f78a9dcde | |||
| 02eb362f37 | |||
| f79263a92a | |||
| cd95da6d35 | |||
| 5ab2c9afaa | |||
| e77201099d | |||
| 30a4c00f35 | |||
| e68db4ce57 | |||
| b5a83ab902 | |||
| 2c9efea733 | |||
| 9615dc1458 | |||
| f50a8482c3 | |||
| cd3dc00f2f | |||
| 65eae30a48 | |||
| fa4392df09 | |||
| f8d6fd80d5 | |||
| 88ed545830 | |||
| 4388f6e87c | |||
| 6157364e20 | |||
| 96999e399d | |||
| 6a3df679fa | |||
| 03e49c59e2 | |||
| b525b6e4fa | |||
| 5541b89cf7 | |||
| aaeed5d18b | |||
| d6c3c8a294 | |||
| d337fc6d47 | |||
| 2d897d8537 | |||
| 12b101e04f | |||
| d69af56c90 | |||
| 0cac5610c8 | |||
| d0afcf6628 | |||
| 37fa27d54f | |||
| be4fed2c19 | |||
| 47d02d8c4f | |||
| 4881d8e3a1 | |||
| cc618abf58 | |||
| 546e381325 | |||
| 9d1bb29a00 | |||
| 876d0d5873 | |||
| 2aa5c387c7 | |||
| 9ca8e49a4e | |||
| 6ceed03f6b | |||
| 4836b16030 | |||
| f9f44b18ad | |||
| d4f5b7ca34 | |||
| 9b57329f56 | |||
| 0064ac5ead | |||
| f2489c0845 | |||
| dca345b135 | |||
| 645c1b9151 | |||
| 678fca6704 | |||
| b74fae3762 | |||
| 2817ea833a | |||
| b7ed6d8463 | |||
| 005c33dbb5 | |||
| 4176317250 | |||
| bbd562f711 | |||
| a19505a708 | |||
| 1eed16bc97 | |||
| d9f88985fe | |||
| a57e33e8d1 | |||
| b4552ddb5f | |||
| 1da2450b10 | |||
| 9536b42244 | |||
| dd75cdb37e | |||
| 3b3e537797 | |||
| 0f9168052a | |||
| eb47476c83 | |||
| 7b04817874 | |||
| c7a7456ec9 | |||
| e422dd1198 | |||
| a75928d805 | |||
| fb2c5a85b6 | |||
| 4de2e381ff | |||
| 4da8c8d6db | |||
| 3c565defca | |||
| 191be658d5 | |||
| 1f209d0fb4 | |||
| ba91e1dfb2 | |||
| 6766884cd8 | |||
| b075140e76 | |||
| aa8586d273 | |||
| 9b2a3d23b2 | |||
| 6a43a4bd20 | |||
| 8c78889e88 | |||
| 873159b793 | |||
| b5823d3210 | |||
| cd99c22f64 | |||
| baa5122fcb | |||
| 5447aa7c80 | |||
| 933918ea27 | |||
| cbbcdc5df1 | |||
| 4dfa7b132d | |||
| fb5bfaa2bd | |||
| 20e206fa43 | |||
| 467fa59023 | |||
| 166c06c628 | |||
| 5ff3c8961c | |||
| 08f33f0e78 | |||
| 0c5a637203 | |||
| e3b4fdb6b1 | |||
| e730a6e282 | |||
| 722808a0e4 | |||
| eae33161c1 | |||
| f14df141f7 | |||
| f7a4330cd1 | |||
| 23474c9752 | |||
| fc08f9823e | |||
| 639bddb4b7 | |||
| f87b32fc7b | |||
| 468ad6d578 | |||
| 8b5c7d3d87 | |||
| e791084793 | |||
| 316a1ae2c5 | |||
| 71beca68dc | |||
| aae79db27a | |||
| 6f188da2a6 | |||
| 9ae4ce82a7 | |||
| 5adfa722d4 | |||
| c26dda7cc9 | |||
| b7440ee516 | |||
| e4b06b16a9 | |||
| 491af5bd9a | |||
| 9b67ab9713 | |||
| f0a62600af | |||
| dd5dfd98b7 | |||
| d5ec38c4db | |||
| f945463dbe | |||
| cf9439fb3b | |||
| 6901847c49 | |||
| c54c25c35e | |||
| 5728bce6bc | |||
| d752403ed6 | |||
| a48c08bd23 | |||
| e46bbe8546 | |||
| f5c8f18980 | |||
| 2d2270a337 | |||
| d315c53ff8 | |||
| d36b06acf7 | |||
| 2299af1dba | |||
| e25ccc5e9a | |||
| 3ea6610923 | |||
| 2d50f10fd6 | |||
| 81d0637483 | |||
| 6c4df5abf0 | |||
| 2eb0b5eedd | |||
| 0e00492f54 | |||
| b84a62eb5d | |||
| c41ed95afe | |||
| fe07013383 | |||
| 4f9cb9a8a1 | |||
| ec5129d25b | |||
| 6a781c62ec | |||
| c01ee83cd7 | |||
| cc591e399d | |||
| 7462c703f3 | |||
| 879a6b4202 | |||
| 0ae8dc1040 | |||
| 242548b36a | |||
| 252aedda25 | |||
| 3507269321 | |||
| 9a5dce33db | |||
| c4101a62ed | |||
| f52037f305 | |||
| 03bd67c4e7 | |||
| 1eef239392 | |||
| d1e14ed691 | |||
| 60a787ce3d | |||
| f96bc6d5f4 | |||
| 5d439d9e79 | |||
| 1453178693 | |||
| 510121bf54 | |||
| 2d607b394c | |||
| bd12b0e441 | |||
| 738b4e60fa | |||
| 1ae2f55c04 | |||
| 2ebdf9673d | |||
| 0427d790e5 | |||
| 90add7cf47 | |||
| 26b1f8dfdb | |||
| ba29889f54 | |||
| 9d2284add7 | |||
| 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 |
@@ -2007,3 +2007,151 @@
|
||||
* 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
|
||||
|
||||
[5.6.0]
|
||||
* Remove IP nginx configuration that redirects to dashboard after activation
|
||||
* dashboard: looks for search string in app title as well
|
||||
* Add vaapi caps for transcoding
|
||||
* Fix issue where the long mongodb database names where causing app indices of rocket.chat to overflow (> 127)
|
||||
* Do not resize swap if swap file exists. This means that users can now control how swap is allocated on their own.
|
||||
* SFTP: fix issue where parallel rebuilds would cause an error
|
||||
* backups: make part size configurable
|
||||
* mail: set max email size
|
||||
* mail: allow mail server location to be set
|
||||
* spamassassin: custom configs and wl/bl
|
||||
* Do not automatically update to unstable release
|
||||
* scheduler: reduce container churn
|
||||
* mail: add API to set banner
|
||||
* Fix bug where systemd 237 ignores --nice value in systemd-run
|
||||
* postgresql: enable uuid-ossp extension
|
||||
* firewall: add blocklist
|
||||
* HTTP URLs now redirect directly to the HTTPS of the final domain
|
||||
* linode: Add singapore region
|
||||
* ovh: add sydney region
|
||||
* s3: makes multi-part copies in parallel
|
||||
|
||||
[5.6.1]
|
||||
* Blocklists are now stored in a text file instead of json
|
||||
* regenerate nginx configs
|
||||
|
||||
[5.6.2]
|
||||
* Update docker to 19.03.12
|
||||
* Fix sorting of user listing in the UI
|
||||
* namecheap: fix crash when server returns invalid response
|
||||
* unlink ghost file automatically on successful login
|
||||
* Bump mysql addon connection limit to 200
|
||||
* Fix install issue where `/dev/dri` may not be present
|
||||
* import: when importing filesystem backups, the input box is a path
|
||||
* firewall: fix race condition where blocklist was not added in correct position in the FORWARD chain
|
||||
* services: fix issue where services where scaled up/down too fast
|
||||
* turn: realm variable was not updated properly on dashboard change
|
||||
* nginx: add splash pages for IP based browser access
|
||||
* Give services panel a separate top-level view
|
||||
* Add app state filter
|
||||
* gcs: copy concurrency was not used
|
||||
* Mention why an app update cannot be applied and provide shortcut to start the app if stopped
|
||||
* Remove version from footer into the setting view
|
||||
* Give services panel a separate top-level view
|
||||
* postgresql: set collation order explicity when creating database to C.UTF-8 (for confluence)
|
||||
* rsync: fix error while goes missing when syncing
|
||||
* Pre-select app domain by default in the redirection drop down
|
||||
* robots: preseve leading and trailing whitespaces/newlines
|
||||
|
||||
[5.6.3]
|
||||
* Fix postgres locale issue
|
||||
|
||||
[6.0.0]
|
||||
* Focal support
|
||||
* Reduce duration of self-signed certs to 800 days
|
||||
* Better backup config filename when downloading
|
||||
* branding: footer can have template variables like %YEAR% and %VERSION%
|
||||
* sftp: secure the API with a token
|
||||
* filemanager: Add extract context menu item
|
||||
* Do not download docker images if present locally
|
||||
* sftp: disable access to non-admins by default
|
||||
* postgresql: whitelist pgcrypto extension for loomio
|
||||
* filemanager: Add new file creation action and collapse new and upload actions
|
||||
* rsync: add warning to remove lifecycle rules
|
||||
* Add volume management
|
||||
* backups: adjust node's heap size based on memory limit
|
||||
* s3: diasble per-chunk timeout
|
||||
* logs: more descriptive log file names on download
|
||||
* collectd: remove collectd config when app stopped (and add it back when started)
|
||||
* Apps can optionally request an authwall to be installed in front of them
|
||||
* mailbox can now owned by a group
|
||||
* linode: enable dns provider in setup view
|
||||
* dns: apps can now use the dns port
|
||||
* httpPaths: allow apps to specify forwarding from custom paths to container ports (for OLS)
|
||||
* add elasticemail smtp relay option
|
||||
* mail: add option to fts using solr
|
||||
* mail: change the namespace separator of new installations to /
|
||||
* mail: enable acl
|
||||
* Disable THP
|
||||
* filemanager: allow download dirs as zip files
|
||||
* aws: add china region
|
||||
* security: fix issue where apps could send with any username (but valid password)
|
||||
* i18n support
|
||||
|
||||
[6.0.1]
|
||||
* app: add export route
|
||||
* mail: on location change, fix lock up when one or more domains have invalid credentials
|
||||
* mail: fix crash because of write after timeout closure
|
||||
* scaleway: fix installation issue where THP is not enabled in kernel
|
||||
|
||||
|
||||
@@ -29,9 +29,9 @@ anyone to effortlessly host web applications on their server on their own terms.
|
||||
* Trivially migrate to another server keeping your apps and data (for example, switch your
|
||||
infrastructure provider or move to a bigger server).
|
||||
|
||||
* Comprehensive [REST API](https://cloudron.io/documentation/developer/api/).
|
||||
* Comprehensive [REST API](https://docs.cloudron.io/api/).
|
||||
|
||||
* [CLI](https://cloudron.io/documentation/cli/) to configure apps.
|
||||
* [CLI](https://docs.cloudron.io/custom-apps/cli/) to configure apps.
|
||||
|
||||
* Alerts, audit logs, graphs, dns management ... and much more
|
||||
|
||||
@@ -41,15 +41,37 @@ Try our demo at https://my.demo.cloudron.io (username: cloudron password: cloudr
|
||||
|
||||
## Installing
|
||||
|
||||
[Install script](https://cloudron.io/documentation/installation/) - [Pricing](https://cloudron.io/pricing.html)
|
||||
[Install script](https://docs.cloudron.io/installation/) - [Pricing](https://cloudron.io/pricing.html)
|
||||
|
||||
**Note:** This repo is a small part of what gets installed on your server - there is
|
||||
the dashboard, database addons, graph container, base image etc. Cloudron also relies
|
||||
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.
|
||||
|
||||
## Development
|
||||
|
||||
This is the backend code of Cloudron. The frontend code is [here](https://git.cloudron.io/cloudron/dashboard).
|
||||
|
||||
The way to develop is to first install a full instance of Cloudron in a VM. Then you can use the [hotfix](https://git.cloudron.io/cloudron/cloudron-machine)
|
||||
tool to patch the VM with the latest code.
|
||||
|
||||
```
|
||||
SSH_PASSPHRASE=sshkeypassword cloudron-machine hotfix --cloudron my.example.com --release 6.0.0 --ssh-key keyname
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Please note that the Cloudron code is under a source-available license. This is not the same as an
|
||||
open source license but ensures the code is available for introspection (and hacking!).
|
||||
|
||||
## Contributions
|
||||
|
||||
Just to give some heads up, we are a bit restrictive in merging changes. We are a small team and
|
||||
would like to keep our maintenance burden low. It might be best to discuss features first in the [forum](https://forum.cloudron.io),
|
||||
to also figure out how many other people will use it to justify maintenance for a feature.
|
||||
|
||||
## Support
|
||||
|
||||
* [Documentation](https://cloudron.io/documentation/)
|
||||
* [Documentation](https://docs.cloudron.io/)
|
||||
* [Forum](https://forum.cloudron.io/)
|
||||
|
||||
|
||||
@@ -13,6 +13,9 @@ function die {
|
||||
|
||||
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
|
||||
apt-mark hold grub* >/dev/null
|
||||
apt-get -o Dpkg::Options::="--force-confdef" update -y
|
||||
@@ -26,9 +29,9 @@ debconf-set-selections <<< 'mysql-server mysql-server/root_password_again passwo
|
||||
|
||||
# this enables automatic security upgrades (https://help.ubuntu.com/community/AutomaticSecurityUpdates)
|
||||
# resolvconf is needed for unbound to work property after disabling systemd-resolved in 18.04
|
||||
ubuntu_version=$(lsb_release -rs)
|
||||
ubuntu_codename=$(lsb_release -cs)
|
||||
|
||||
gpg_package=$([[ "${ubuntu_version}" == "16.04" ]] && echo "gnupg" || echo "gpg")
|
||||
mysql_package=$([[ "${ubuntu_version}" == "20.04" ]] && echo "mysql-server-8.0" || echo "mysql-server-5.7")
|
||||
apt-get -y install \
|
||||
acl \
|
||||
build-essential \
|
||||
@@ -38,11 +41,12 @@ apt-get -y install \
|
||||
debconf-utils \
|
||||
dmsetup \
|
||||
$gpg_package \
|
||||
ipset \
|
||||
iptables \
|
||||
libpython2.7 \
|
||||
linux-generic \
|
||||
logrotate \
|
||||
mysql-server-5.7 \
|
||||
$mysql_package \
|
||||
openssh-server \
|
||||
pwgen \
|
||||
resolvconf \
|
||||
@@ -52,16 +56,11 @@ apt-get -y install \
|
||||
unbound \
|
||||
xfsprogs
|
||||
|
||||
if [[ "${ubuntu_version}" == "16.04" ]]; then
|
||||
echo "==> installing nginx for xenial for TLSv3 support"
|
||||
|
||||
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 with install deps (as opposed to dpkg -i)
|
||||
apt install -y /tmp/nginx.deb
|
||||
rm /tmp/nginx.deb
|
||||
else
|
||||
apt install -y nginx-full
|
||||
fi
|
||||
echo "==> installing nginx for xenial for TLSv3 support"
|
||||
curl -sL http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/nginx_1.18.0-2~${ubuntu_codename}_amd64.deb -o /tmp/nginx.deb
|
||||
# apt install with install deps (as opposed to dpkg -i)
|
||||
apt install -y /tmp/nginx.deb
|
||||
rm /tmp/nginx.deb
|
||||
|
||||
# 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
|
||||
@@ -86,9 +85,9 @@ mkdir -p /etc/systemd/system/docker.service.d
|
||||
echo -e "[Service]\nExecStart=\nExecStart=/usr/bin/dockerd -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs --storage-driver=overlay2" > /etc/systemd/system/docker.service.d/cloudron.conf
|
||||
|
||||
# there are 3 packages for docker - containerd, CLI and the daemon
|
||||
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/containerd.io_1.2.2-3_amd64.deb" -o /tmp/containerd.deb
|
||||
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce-cli_18.09.2~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker-ce-cli.deb
|
||||
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce_18.09.2~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker.deb
|
||||
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/containerd.io_1.2.13-2_amd64.deb" -o /tmp/containerd.deb
|
||||
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce-cli_19.03.12~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker-ce-cli.deb
|
||||
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce_19.03.12~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker.deb
|
||||
# apt install with install deps (as opposed to dpkg -i)
|
||||
apt install -y /tmp/containerd.deb /tmp/docker-ce-cli.deb /tmp/docker.deb
|
||||
rm /tmp/containerd.deb /tmp/docker-ce-cli.deb /tmp/docker.deb
|
||||
@@ -125,6 +124,8 @@ if ! apt-get install -y libcurl3-gnutls collectd collectd-utils; then
|
||||
echo "Failed to install collectd. Presumably because of http://mailman.verplant.org/pipermail/collectd/2015-March/006491.html"
|
||||
sed -e 's/^FQDNLookup true/FQDNLookup false/' -i /etc/collectd/collectd.conf
|
||||
fi
|
||||
# https://bugs.launchpad.net/ubuntu/+source/collectd/+bug/1872281
|
||||
[[ "${ubuntu_version}" == "20.04" ]] && echo -e "\nLD_PRELOAD=/usr/lib/python3.8/config-3.8-x86_64-linux-gnu/libpython3.8.so" >> /etc/default/collectd
|
||||
|
||||
echo "==> Configuring host"
|
||||
sed -e 's/^#NTP=/NTP=0.ubuntu.pool.ntp.org 1.ubuntu.pool.ntp.org 2.ubuntu.pool.ntp.org 3.ubuntu.pool.ntp.org/' -i /etc/systemd/timesyncd.conf
|
||||
@@ -133,13 +134,15 @@ timedatectl set-ntp 1
|
||||
timedatectl set-timezone UTC
|
||||
|
||||
echo "==> Adding sshd configuration warning"
|
||||
sed -e '/Port 22/ i # NOTE: Cloudron only supports moving SSH to port 202. See https://cloudron.io/documentation/security/#securing-ssh-access' -i /etc/ssh/sshd_config
|
||||
sed -e '/Port 22/ i # NOTE: Cloudron only supports moving SSH to port 202. See https://docs.cloudron.io/security/#securing-ssh-access' -i /etc/ssh/sshd_config
|
||||
|
||||
# https://bugs.launchpad.net/ubuntu/+source/base-files/+bug/1701068
|
||||
echo "==> Disabling motd news"
|
||||
sed -i 's/^ENABLED=.*/ENABLED=0/' /etc/default/motd-news
|
||||
if [ -f "/etc/default/motd-news" ]; then
|
||||
sed -i 's/^ENABLED=.*/ENABLED=0/' /etc/default/motd-news
|
||||
fi
|
||||
|
||||
# Disable bind for good measure (on online.net, kimsufi servers these are pre-installed and conflicts with unbound)
|
||||
# Disable bind for good measure (on online.net, kimsufi servers these are pre-installed)
|
||||
systemctl stop bind9 || true
|
||||
systemctl disable bind9 || true
|
||||
|
||||
@@ -151,7 +154,7 @@ systemctl disable dnsmasq || true
|
||||
systemctl stop postfix || true
|
||||
systemctl disable postfix || true
|
||||
|
||||
# on ubuntu 18.04, this is the default. this requires resolvconf for DNS to work further after the disable
|
||||
# on ubuntu 18.04 and 20.04, this is the default. this requires resolvconf for DNS to work further after the disable
|
||||
systemctl stop systemd-resolved || true
|
||||
systemctl disable systemd-resolved || true
|
||||
|
||||
@@ -160,4 +163,3 @@ systemctl disable systemd-resolved || true
|
||||
ip6=$([[ -s /proc/net/if_inet6 ]] && echo "yes" || echo "no")
|
||||
echo -e "server:\n\tinterface: 127.0.0.1\n\tdo-ip6: ${ip6}" > /etc/unbound/unbound.conf.d/cloudron-network.conf
|
||||
systemctl restart unbound
|
||||
|
||||
|
||||
@@ -2,57 +2,65 @@
|
||||
|
||||
'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'),
|
||||
constants = require('./src/constants.js'),
|
||||
dockerProxy = require('./src/dockerproxy.js'),
|
||||
fs = require('fs'),
|
||||
ldap = require('./src/ldap.js'),
|
||||
paths = require('./src/paths.js'),
|
||||
proxyAuth = require('./src/proxyauth.js'),
|
||||
server = require('./src/server.js');
|
||||
|
||||
console.log();
|
||||
console.log('==========================================');
|
||||
console.log(` Cloudron ${constants.VERSION} `);
|
||||
console.log('==========================================');
|
||||
console.log();
|
||||
const NOOP_CALLBACK = function () { };
|
||||
|
||||
function setupLogging(callback) {
|
||||
if (process.env.BOX_ENV === 'test') return callback();
|
||||
|
||||
var logfileStream = fs.createWriteStream(paths.BOX_LOG_FILE, { flags:'a' });
|
||||
process.stdout.write = process.stderr.write = logfileStream.write.bind(logfileStream);
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
async.series([
|
||||
server.start,
|
||||
setupLogging,
|
||||
server.start, // do this first since it also inits the database
|
||||
proxyAuth.start,
|
||||
ldap.start,
|
||||
dockerProxy.start
|
||||
], function (error) {
|
||||
if (error) {
|
||||
console.error('Error starting server', error);
|
||||
console.log('Error starting server', error);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('Cloudron is up and running');
|
||||
});
|
||||
|
||||
var NOOP_CALLBACK = function () { };
|
||||
|
||||
process.on('SIGINT', function () {
|
||||
console.log('Received SIGINT. Shutting down.');
|
||||
|
||||
server.stop(NOOP_CALLBACK);
|
||||
ldap.stop(NOOP_CALLBACK);
|
||||
dockerProxy.stop(NOOP_CALLBACK);
|
||||
setTimeout(process.exit.bind(process), 3000);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', function () {
|
||||
console.log('Received SIGTERM. Shutting down.');
|
||||
|
||||
server.stop(NOOP_CALLBACK);
|
||||
ldap.stop(NOOP_CALLBACK);
|
||||
dockerProxy.stop(NOOP_CALLBACK);
|
||||
setTimeout(process.exit.bind(process), 3000);
|
||||
|
||||
// require those here so that logging handler is already setup
|
||||
require('supererror');
|
||||
const debug = require('debug')('box:box');
|
||||
|
||||
process.on('SIGINT', function () {
|
||||
debug('Received SIGINT. Shutting down.');
|
||||
|
||||
proxyAuth.stop(NOOP_CALLBACK);
|
||||
server.stop(NOOP_CALLBACK);
|
||||
ldap.stop(NOOP_CALLBACK);
|
||||
dockerProxy.stop(NOOP_CALLBACK);
|
||||
setTimeout(process.exit.bind(process), 3000);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', function () {
|
||||
debug('Received SIGTERM. Shutting down.');
|
||||
|
||||
proxyAuth.stop(NOOP_CALLBACK);
|
||||
server.stop(NOOP_CALLBACK);
|
||||
ldap.stop(NOOP_CALLBACK);
|
||||
dockerProxy.stop(NOOP_CALLBACK);
|
||||
setTimeout(process.exit.bind(process), 3000);
|
||||
});
|
||||
|
||||
process.on('uncaughtException', function (error) {
|
||||
console.error((error && error.stack) ? error.stack : error);
|
||||
setTimeout(process.exit.bind(process, 1), 3000);
|
||||
});
|
||||
|
||||
console.log(`Cloudron is up and running. Logs are at ${paths.BOX_LOG_FILE}`); // this goes to journalctl
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
'use strict';
|
||||
|
||||
const async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT value FROM settings WHERE name="admin_domain"', function (error, results) {
|
||||
if (error || results.length === 0) return callback(error);
|
||||
|
||||
const adminDomain = results[0].value;
|
||||
|
||||
async.series([
|
||||
db.runSql.bind(db, 'INSERT INTO settings (name, value) VALUES (?, ?)', [ 'mail_domain', adminDomain ]),
|
||||
db.runSql.bind(db, 'INSERT INTO settings (name, value) VALUES (?, ?)', [ 'mail_fqdn', `my.${adminDomain}` ])
|
||||
], callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'DELETE FROM settings WHERE name="mail_domain"'),
|
||||
db.runSql.bind(db, 'DELETE FROM settings WHERE name="mail_fqdn"'),
|
||||
], callback);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('SELECT * FROM settings WHERE name=?', ['app_autoupdate_pattern'], function (error, results) {
|
||||
if (error || results.length === 0) return callback(error); // will use defaults from box code
|
||||
|
||||
var updatePattern = results[0].value; // use app auto update patter for the box as well
|
||||
|
||||
async.series([
|
||||
db.runSql.bind(db, 'START TRANSACTION;'),
|
||||
db.runSql.bind(db, 'DELETE FROM settings WHERE name=? OR name=?', ['app_autoupdate_pattern', 'box_autoupdate_pattern']),
|
||||
db.runSql.bind(db, 'INSERT settings (name, value) VALUES(?, ?)', ['autoupdate_pattern', updatePattern]),
|
||||
db.runSql.bind(db, 'COMMIT')
|
||||
], callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE mail ADD COLUMN bannerJson TEXT', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE mail DROP COLUMN bannerJson', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
'use strict';
|
||||
|
||||
const OLD_FIREWALL_CONFIG_JSON = '/home/yellowtent/boxdata/firewall-config.json';
|
||||
const PORTS_FILE = '/home/yellowtent/boxdata/firewall/ports.json';
|
||||
const BLOCKLIST_FILE = '/home/yellowtent/boxdata/firewall/blocklist.txt';
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
exports.up = function (db, callback) {
|
||||
if (!fs.existsSync(OLD_FIREWALL_CONFIG_JSON)) return callback();
|
||||
|
||||
try {
|
||||
const dataJson = fs.readFileSync(OLD_FIREWALL_CONFIG_JSON, 'utf8');
|
||||
const data = JSON.parse(dataJson);
|
||||
fs.writeFileSync(BLOCKLIST_FILE, data.blocklist.join('\n') + '\n', 'utf8');
|
||||
fs.writeFileSync(PORTS_FILE, JSON.stringify({ allowed_tcp_ports: data.allowed_tcp_ports }, null, 4), 'utf8');
|
||||
fs.unlinkSync(OLD_FIREWALL_CONFIG_JSON);
|
||||
} catch (error) {
|
||||
console.log('Error migrating old firewall config', error);
|
||||
}
|
||||
|
||||
callback();
|
||||
};
|
||||
|
||||
exports.down = function (db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
var cmd1 = 'CREATE TABLE volumes(' +
|
||||
'id VARCHAR(128) NOT NULL UNIQUE,' +
|
||||
'name VARCHAR(256) NOT NULL UNIQUE,' +
|
||||
'hostPath VARCHAR(1024) NOT NULL UNIQUE,' +
|
||||
'creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,' +
|
||||
'PRIMARY KEY (id)) CHARACTER SET utf8 COLLATE utf8_bin';
|
||||
|
||||
var cmd2 = 'CREATE TABLE appMounts(' +
|
||||
'appId VARCHAR(128) NOT NULL,' +
|
||||
'volumeId VARCHAR(128) NOT NULL,' +
|
||||
'readOnly BOOLEAN DEFAULT 1,' +
|
||||
'UNIQUE KEY appMounts_appId_volumeId (appId, volumeId),' +
|
||||
'FOREIGN KEY(appId) REFERENCES apps(id),' +
|
||||
'FOREIGN KEY(volumeId) REFERENCES volumes(id)) CHARACTER SET utf8 COLLATE utf8_bin;';
|
||||
|
||||
db.runSql(cmd1, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
db.runSql(cmd2, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN bindsJson', callback);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('DROP TABLE appMounts', function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
db.runSql('DROP TABLE volumes', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps ADD COLUMN proxyAuth BOOLEAN DEFAULT 0', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN proxyAuth', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE mailboxes ADD COLUMN ownerType VARCHAR(16)'),
|
||||
db.runSql.bind(db, 'UPDATE mailboxes SET ownerType=?', [ 'user' ]),
|
||||
db.runSql.bind(db, 'ALTER TABLE mailboxes MODIFY ownerType VARCHAR(16) NOT NULL'),
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE mailboxes DROP COLUMN ownerType', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE apps DROP COLUMN httpPort')
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
'use strict';
|
||||
|
||||
const async = require('async'),
|
||||
iputils = require('../src/iputils.js');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps ADD COLUMN containerIp VARCHAR(16) UNIQUE', function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
let baseIp = iputils.intFromIp('172.18.16.0');
|
||||
|
||||
db.all('SELECT * FROM apps', function (error, apps) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.eachSeries(apps, function (app, iteratorDone) {
|
||||
const nextIp = iputils.ipFromInt(++baseIp);
|
||||
db.runSql('UPDATE apps SET containerIp=? WHERE id=?', [ nextIp, app.id ], iteratorDone);
|
||||
}, callback);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN containerIp', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT * FROM settings WHERE name=?', ['platform_config'], function (error, results) {
|
||||
let value;
|
||||
if (error || results.length === 0) {
|
||||
value = { sftp: { requireAdmin: true } };
|
||||
} else {
|
||||
value = JSON.parse(results[0].value);
|
||||
if (!value.sftp) value.sftp = {};
|
||||
value.sftp.requireAdmin = true;
|
||||
}
|
||||
|
||||
// existing installations may not even have the key. so use REPLACE instead of UPDATE
|
||||
db.runSql('REPLACE INTO settings (name, value) VALUES (?, ?)', [ 'platform_config', JSON.stringify(value) ], callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
+21
-5
@@ -21,7 +21,7 @@ CREATE TABLE IF NOT EXISTS users(
|
||||
password VARCHAR(1024) NOT NULL,
|
||||
salt 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 "",
|
||||
fallbackEmail VARCHAR(512) DEFAULT "",
|
||||
twoFactorAuthenticationSecret VARCHAR(128) DEFAULT "",
|
||||
@@ -65,9 +65,6 @@ CREATE TABLE IF NOT EXISTS apps(
|
||||
healthTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the app last responded
|
||||
containerId VARCHAR(128),
|
||||
manifestJson TEXT,
|
||||
httpPort INTEGER, // this is the nginx proxy port and not manifest.httpPort
|
||||
location VARCHAR(128) NOT NULL,
|
||||
domain VARCHAR(128) NOT NULL,
|
||||
accessRestrictionJson TEXT, // { users: [ ], groups: [ ] }
|
||||
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the app was installed
|
||||
updateTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the last app update was done
|
||||
@@ -87,7 +84,8 @@ CREATE TABLE IF NOT EXISTS apps(
|
||||
dataDir VARCHAR(256) UNIQUE,
|
||||
taskId INTEGER, // current task
|
||||
errorJson TEXT,
|
||||
bindsJson TEXT, // bind mounts
|
||||
servicesConfigJson TEXT, // app services configuration
|
||||
containerIp VARCHAR(16) UNIQUE, // this is not-null because of ip allocation fails, user can 'repair'
|
||||
|
||||
FOREIGN KEY(mailboxDomain) REFERENCES domains(domain),
|
||||
FOREIGN KEY(taskId) REFERENCES tasks(id),
|
||||
@@ -162,6 +160,7 @@ CREATE TABLE IF NOT EXISTS mail(
|
||||
mailFromValidation BOOLEAN DEFAULT 1,
|
||||
catchAllJson TEXT,
|
||||
relayJson TEXT,
|
||||
bannerJson TEXT,
|
||||
|
||||
dkimSelector VARCHAR(128) NOT NULL DEFAULT "cloudron",
|
||||
|
||||
@@ -181,6 +180,7 @@ CREATE TABLE IF NOT EXISTS mailboxes(
|
||||
name VARCHAR(128) NOT NULL,
|
||||
type VARCHAR(16) NOT NULL, /* 'mailbox', 'alias', 'list' */
|
||||
ownerId VARCHAR(128) NOT NULL, /* user id */
|
||||
ownerType VARCHAR(16) NOT NULL,
|
||||
aliasName VARCHAR(128), /* the target name type is an alias */
|
||||
aliasDomain VARCHAR(128), /* the target domain */
|
||||
membersJson TEXT, /* members of a group. fully qualified */
|
||||
@@ -237,4 +237,20 @@ CREATE TABLE IF NOT EXISTS appPasswords(
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS volumes(
|
||||
id VARCHAR(128) NOT NULL UNIQUE,
|
||||
name VARCHAR(256) NOT NULL UNIQUE,
|
||||
hostPath VARCHAR(1024) NOT NULL UNIQUE,
|
||||
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS appMounts(
|
||||
appId VARCHAR(128) NOT NULL,
|
||||
volumeId VARCHAR(128) NOT NULL,
|
||||
readOnly BOOLEAN DEFAULT 1,
|
||||
UNIQUE KEY appMounts_appId_volumeId (appId, volumeId),
|
||||
FOREIGN KEY(appId) REFERENCES apps(id),
|
||||
FOREIGN KEY(volumeId) REFERENCES volumes(id));
|
||||
|
||||
CHARACTER SET utf8 COLLATE utf8_bin;
|
||||
|
||||
Generated
+419
-323
File diff suppressed because it is too large
Load Diff
+22
-18
@@ -14,41 +14,45 @@
|
||||
"node": ">=4.0.0 <=4.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google-cloud/dns": "^1.1.0",
|
||||
"@google-cloud/dns": "^1.2.9",
|
||||
"@google-cloud/storage": "^2.5.0",
|
||||
"@sindresorhus/df": "git+https://github.com/cloudron-io/df.git#type",
|
||||
"async": "^2.6.3",
|
||||
"aws-sdk": "^2.685.0",
|
||||
"aws-sdk": "^2.759.0",
|
||||
"basic-auth": "^2.0.1",
|
||||
"body-parser": "^1.19.0",
|
||||
"cloudron-manifestformat": "^5.4.0",
|
||||
"cloudron-manifestformat": "^5.9.0",
|
||||
"connect": "^3.7.0",
|
||||
"connect-lastmile": "^2.0.0",
|
||||
"connect-timeout": "^1.9.0",
|
||||
"cookie-parser": "^1.4.5",
|
||||
"cookie-session": "^1.4.0",
|
||||
"cron": "^1.8.2",
|
||||
"db-migrate": "^0.11.11",
|
||||
"db-migrate-mysql": "^1.1.10",
|
||||
"debug": "^4.1.1",
|
||||
"db-migrate-mysql": "^2.1.1",
|
||||
"debug": "^4.2.0",
|
||||
"dockerode": "^2.5.8",
|
||||
"ejs": "^2.6.1",
|
||||
"ejs-cli": "^2.2.0",
|
||||
"ejs-cli": "^2.2.1",
|
||||
"express": "^4.17.1",
|
||||
"ipaddr.js": "^2.0.0",
|
||||
"js-yaml": "^3.14.0",
|
||||
"json": "^9.0.6",
|
||||
"ldapjs": "^1.0.2",
|
||||
"lodash": "^4.17.15",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"ldapjs": "^2.2.0",
|
||||
"lodash": "^4.17.20",
|
||||
"lodash.chunk": "^4.2.0",
|
||||
"mime": "^2.4.6",
|
||||
"moment": "^2.26.0",
|
||||
"moment": "^2.29.0",
|
||||
"moment-timezone": "^0.5.31",
|
||||
"morgan": "^1.10.0",
|
||||
"multiparty": "^4.2.1",
|
||||
"multiparty": "^4.2.2",
|
||||
"mustache-express": "^1.3.0",
|
||||
"mysql": "^2.18.1",
|
||||
"nodemailer": "^6.4.6",
|
||||
"nodemailer": "^6.4.11",
|
||||
"nodemailer-smtp-transport": "^2.7.4",
|
||||
"once": "^1.4.0",
|
||||
"parse-links": "^0.1.0",
|
||||
"pretty-bytes": "^5.3.0",
|
||||
"pretty-bytes": "^5.4.1",
|
||||
"progress-stream": "^2.0.0",
|
||||
"proxy-middleware": "^0.15.0",
|
||||
"qrcode": "^1.4.4",
|
||||
@@ -61,22 +65,22 @@
|
||||
"showdown": "^1.9.1",
|
||||
"speakeasy": "^2.0.0",
|
||||
"split": "^1.0.1",
|
||||
"superagent": "^5.2.2",
|
||||
"superagent": "^5.3.1",
|
||||
"supererror": "^0.7.2",
|
||||
"tar-fs": "github:cloudron-io/tar-fs#ignore_stat_error",
|
||||
"tar-stream": "^2.1.2",
|
||||
"tar-stream": "^2.1.4",
|
||||
"tldjs": "^2.3.1",
|
||||
"underscore": "^1.10.2",
|
||||
"underscore": "^1.11.0",
|
||||
"uuid": "^3.4.0",
|
||||
"validator": "^11.0.0",
|
||||
"ws": "^7.3.0",
|
||||
"ws": "^7.3.1",
|
||||
"xml2js": "^0.4.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
"expect.js": "*",
|
||||
"hock": "^1.4.1",
|
||||
"js2xmlparser": "^4.0.1",
|
||||
"mocha": "^6.1.4",
|
||||
"mocha": "^6.2.3",
|
||||
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
|
||||
"nock": "^10.0.6",
|
||||
"node-sass": "^4.14.1",
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
set -eu
|
||||
|
||||
readonly SOURCE_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly source_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly DATA_DIR="${HOME}/.cloudron_test"
|
||||
readonly DEFAULT_TESTS="./src/test/*-test.js ./src/routes/test/*-test.js"
|
||||
|
||||
! "${SOURCE_dir}/src/test/checkInstall" && exit 1
|
||||
! "${source_dir}/src/test/checkInstall" && exit 1
|
||||
|
||||
# cleanup old data dirs some of those docker container data requires sudo to be removed
|
||||
echo "=> Provide root password to purge any leftover data in ${DATA_DIR} and load apparmor profile:"
|
||||
@@ -22,19 +22,26 @@ fi
|
||||
mkdir -p ${DATA_DIR}
|
||||
cd ${DATA_DIR}
|
||||
mkdir -p appsdata
|
||||
mkdir -p boxdata/profileicons boxdata/appicons boxdata/mail boxdata/certs boxdata/mail/dkim/localhost boxdata/mail/dkim/foobar.com
|
||||
mkdir -p platformdata/addons/mail platformdata/nginx/cert platformdata/nginx/applications platformdata/collectd/collectd.conf.d platformdata/addons platformdata/logrotate.d platformdata/backup platformdata/logs/tasks
|
||||
mkdir -p boxdata/profileicons boxdata/appicons boxdata/mail boxdata/certs boxdata/mail/dkim/localhost boxdata/mail/dkim/foobar.com boxdata/sftp/ssh
|
||||
mkdir -p platformdata/addons/mail/banner platformdata/nginx/cert platformdata/nginx/applications platformdata/collectd/collectd.conf.d platformdata/addons platformdata/logrotate.d platformdata/backup platformdata/logs/tasks
|
||||
|
||||
# translations
|
||||
mkdir -p box/dashboard/dist/translation
|
||||
cp -r ${source_dir}/../dashboard/dist/translation/* box/dashboard/dist/translation
|
||||
|
||||
# put cert
|
||||
echo "=> Generating a localhost selfsigned cert"
|
||||
openssl req -x509 -newkey rsa:2048 -keyout platformdata/nginx/cert/host.key -out platformdata/nginx/cert/host.cert -days 3650 -subj '/CN=localhost' -nodes -config <(cat /etc/ssl/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:*.localhost"))
|
||||
|
||||
# generate legacy key format for sftp
|
||||
ssh-keygen -m PEM -t rsa -f boxdata/sftp/ssh/ssh_host_rsa_key -q -N ""
|
||||
|
||||
# clear out any containers
|
||||
echo "=> Delete all docker containers first"
|
||||
docker ps -qa | xargs --no-run-if-empty docker rm -f
|
||||
|
||||
# create docker network (while the infra code does this, most tests skip infra setup)
|
||||
docker network create --subnet=172.18.0.0/16 cloudron || true
|
||||
docker network create --subnet=172.18.0.0/16 --ip-range=172.18.0.0/20 cloudron || true
|
||||
|
||||
# create the same mysql server version to test with
|
||||
OUT=`docker inspect mysql-server` || true
|
||||
@@ -59,7 +66,7 @@ echo "=> Ensure database"
|
||||
mysql -h"${MYSQL_IP}" -uroot -ppassword -e 'CREATE DATABASE IF NOT EXISTS box'
|
||||
|
||||
echo "=> Run database migrations"
|
||||
cd "${SOURCE_dir}"
|
||||
cd "${source_dir}"
|
||||
BOX_ENV=test DATABASE_URL=mysql://root:password@${MYSQL_IP}/box node_modules/.bin/db-migrate up
|
||||
|
||||
echo "=> Run tests with mocha"
|
||||
|
||||
+4
-10
@@ -53,7 +53,7 @@ eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--help) echo "See https://cloudron.io/documentation/installation/ on how to install Cloudron"; exit 0;;
|
||||
--help) echo "See https://docs.cloudron.io/installation/ on how to install Cloudron"; exit 0;;
|
||||
--provider) provider="$2"; shift 2;;
|
||||
--version) requestedVersion="$2"; shift 2;;
|
||||
--env)
|
||||
@@ -80,8 +80,8 @@ fi
|
||||
|
||||
# Only --help works with mismatched ubuntu
|
||||
ubuntu_version=$(lsb_release -rs)
|
||||
if [[ "${ubuntu_version}" != "16.04" && "${ubuntu_version}" != "18.04" ]]; then
|
||||
echo "Cloudron requires Ubuntu 16.04 or 18.04" > /dev/stderr
|
||||
if [[ "${ubuntu_version}" != "16.04" && "${ubuntu_version}" != "18.04" && "${ubuntu_version}" != "20.04" ]]; then
|
||||
echo "Cloudron requires Ubuntu 16.04, 18.04 or 20.04" > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -106,12 +106,6 @@ if [[ "${initBaseImage}" == "true" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=> Ensure required apt sources"
|
||||
if ! add-apt-repository universe &>> "${LOG_FILE}"; then
|
||||
echo "Could not add required apt sources (for nginx-full). See ${LOG_FILE}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=> Updating apt and installing script dependencies"
|
||||
if ! apt-get update &>> "${LOG_FILE}"; then
|
||||
echo "Could not update package repositories. See ${LOG_FILE}"
|
||||
@@ -184,7 +178,7 @@ done
|
||||
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>'
|
||||
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}After reboot, visit https://${ip} and accept the self-signed certificate to finish setup.${DONE}\n"
|
||||
|
||||
if [[ "${rebootServer}" == "true" ]]; then
|
||||
systemctl stop box mysql # sometimes mysql ends up having corrupt privilege tables
|
||||
|
||||
@@ -37,12 +37,12 @@ while true; do
|
||||
# fall through
|
||||
;&
|
||||
--owner-login)
|
||||
admin_username=$(mysql -NB -uroot -ppassword -e "SELECT username FROM box.users WHERE role='owner' LIMIT 1" 2>/dev/null)
|
||||
admin_username=$(mysql -NB -uroot -ppassword -e "SELECT username FROM box.users WHERE role='owner' AND username IS NOT NULL ORDER BY createdAt LIMIT 1" 2>/dev/null)
|
||||
admin_password=$(pwgen -1s 12)
|
||||
ghost_file=/home/yellowtent/platformdata/cloudron_ghost.json
|
||||
printf '{"%s":"%s"}\n' "${admin_username}" "${admin_password}" > "${ghost_file}"
|
||||
chown yellowtent:yellowtent "${ghost_file}" && chmod o-r,g-r "${ghost_file}"
|
||||
echo "Login as ${admin_username} / ${admin_password} . Remove ${ghost_file} when done."
|
||||
echo "Login as ${admin_username} / ${admin_password} . This password may only be used once. ${ghost_file} will be automatically removed after use."
|
||||
exit 0
|
||||
;;
|
||||
--) break;;
|
||||
@@ -57,7 +57,7 @@ if [[ "`df --output="avail" / | sed -n 2p`" -lt "10240" ]]; then
|
||||
echo ""
|
||||
df -h
|
||||
echo ""
|
||||
echo "To recover from a full disk, follow the guide at https://cloudron.io/documentation/troubleshooting/#recovery-after-disk-full"
|
||||
echo "To recover from a full disk, follow the guide at https://docs.cloudron.io/troubleshooting/#recovery-after-disk-full"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -94,7 +94,7 @@ echo -e $LINE"Backup stats (possibly misleading)"$LINE >> $OUT
|
||||
du -hcsL /var/backups/* &>> $OUT || true
|
||||
|
||||
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
|
||||
tail -n 100 /home/yellowtent/platformdata/logs/box.log &>> $OUT
|
||||
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
# This script downloads new translation data from weblate at https://translate.cloudron.io
|
||||
|
||||
OUT="/home/yellowtent/box/dashboard/dist/translation"
|
||||
|
||||
# We require root
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
echo "This script should be run as root. Run with sudo"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=> Downloading new translation files..."
|
||||
curl https://translate.cloudron.io/download/cloudron/dashboard/?format=zip -o /tmp/lang.zip
|
||||
|
||||
echo "=> Unpacking..."
|
||||
unzip -jo /tmp/lang.zip -d $OUT
|
||||
chown -R yellowtent:yellowtent $OUT
|
||||
# unzip put very restrictive permissions
|
||||
chmod ua+r $OUT/*
|
||||
|
||||
echo "=> Cleanup..."
|
||||
rm /tmp/lang.zip
|
||||
|
||||
echo "=> Done"
|
||||
|
||||
echo ""
|
||||
echo "Reload the dashboard to see the new translations"
|
||||
echo ""
|
||||
+20
-9
@@ -27,11 +27,11 @@ echo "==> installer: Updating from $(cat $box_src_dir/VERSION) to $(cat $box_src
|
||||
|
||||
echo "==> installer: updating docker"
|
||||
|
||||
if [[ $(docker version --format {{.Client.Version}}) != "18.09.2" ]]; then
|
||||
if [[ $(docker version --format {{.Client.Version}}) != "19.03.12" ]]; then
|
||||
# there are 3 packages for docker - containerd, CLI and the daemon
|
||||
$curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/containerd.io_1.2.2-3_amd64.deb" -o /tmp/containerd.deb
|
||||
$curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce-cli_18.09.2~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker-ce-cli.deb
|
||||
$curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce_18.09.2~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker.deb
|
||||
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/containerd.io_1.2.13-2_amd64.deb" -o /tmp/containerd.deb
|
||||
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce-cli_19.03.12~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker-ce-cli.deb
|
||||
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce_19.03.12~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker.deb
|
||||
|
||||
echo "==> installer: Waiting for all dpkg tasks to finish..."
|
||||
while fuser /var/lib/dpkg/lock; do
|
||||
@@ -57,15 +57,26 @@ if [[ $(docker version --format {{.Client.Version}}) != "18.09.2" ]]; then
|
||||
rm /tmp/containerd.deb /tmp/docker-ce-cli.deb /tmp/docker.deb
|
||||
fi
|
||||
|
||||
readonly nginx_version=$(nginx -v)
|
||||
if [[ "${nginx_version}" != *"1.14."* && "${ubuntu_version}" == "16.04" ]]; then
|
||||
echo "==> installer: installing nginx for xenial for TLSv3 support"
|
||||
curl -sL http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/nginx_1.14.0-1~xenial_amd64.deb -o /tmp/nginx.deb
|
||||
readonly nginx_version=$(nginx -v 2>&1)
|
||||
if [[ "${nginx_version}" != *"1.18."* ]]; then
|
||||
echo "==> installer: installing nginx 1.18"
|
||||
curl -sL http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/nginx_1.18.0-2~${ubuntu_codename}_amd64.deb -o /tmp/nginx.deb
|
||||
# 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
|
||||
rm /tmp/nginx.deb
|
||||
fi
|
||||
|
||||
if ! which ipset; then
|
||||
echo "==> installer: installing ipset"
|
||||
apt install -y ipset
|
||||
fi
|
||||
|
||||
# Only used for the cloudron-translation-update script
|
||||
if ! which unzip; then
|
||||
echo "==> installer: installing unzip"
|
||||
apt install -y unzip
|
||||
fi
|
||||
|
||||
echo "==> installer: updating node"
|
||||
if [[ "$(node --version)" != "v10.18.1" ]]; then
|
||||
mkdir -p /usr/local/node-10.18.1
|
||||
@@ -124,7 +135,7 @@ if ! id "${user}" 2>/dev/null; then
|
||||
fi
|
||||
|
||||
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
|
||||
fi
|
||||
|
||||
|
||||
+30
-8
@@ -19,6 +19,7 @@ readonly json="$(realpath ${script_dir}/../node_modules/.bin/json)"
|
||||
readonly ubuntu_version=$(lsb_release -rs)
|
||||
|
||||
cp -f "${script_dir}/../scripts/cloudron-support" /usr/bin/cloudron-support
|
||||
cp -f "${script_dir}/../scripts/cloudron-translation-update" /usr/bin/cloudron-translation-update
|
||||
|
||||
# this needs to match the cloudron/base:2.0.0 gid
|
||||
if ! getent group media; then
|
||||
@@ -31,7 +32,8 @@ systemctl enable apparmor
|
||||
systemctl restart apparmor
|
||||
|
||||
usermod ${USER} -a -G docker
|
||||
docker network create --subnet=172.18.0.0/16 cloudron || true
|
||||
# unbound (which starts after box code) relies on this interface to exist. dockerproxy also relies on this.
|
||||
docker network create --subnet=172.18.0.0/16 --ip-range=172.18.0.0/20 cloudron || true
|
||||
|
||||
mkdir -p "${BOX_DATA_DIR}"
|
||||
mkdir -p "${APPS_DATA_DIR}"
|
||||
@@ -44,7 +46,7 @@ mkdir -p "${PLATFORM_DATA_DIR}/mysql"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/postgresql"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/mongodb"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/redis"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/addons/mail"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/addons/mail/banner"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/collectd/collectd.conf.d"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/logrotate.d"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/acme"
|
||||
@@ -57,11 +59,13 @@ mkdir -p "${PLATFORM_DATA_DIR}/logs/backup" \
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/update"
|
||||
|
||||
mkdir -p "${BOX_DATA_DIR}/appicons"
|
||||
mkdir -p "${BOX_DATA_DIR}/firewall"
|
||||
mkdir -p "${BOX_DATA_DIR}/profileicons"
|
||||
mkdir -p "${BOX_DATA_DIR}/certs"
|
||||
mkdir -p "${BOX_DATA_DIR}/acme" # acme keys
|
||||
mkdir -p "${BOX_DATA_DIR}/mail/dkim"
|
||||
mkdir -p "${BOX_DATA_DIR}/well-known" # .well-known documents
|
||||
mkdir -p "${BOX_DATA_DIR}/sftp/ssh" # sftp keys
|
||||
|
||||
# ensure backups folder exists and is writeable
|
||||
mkdir -p /var/backups
|
||||
@@ -102,10 +106,11 @@ echo "==> Adding systemd services"
|
||||
cp -r "${script_dir}/start/systemd/." /etc/systemd/system/
|
||||
[[ "${ubuntu_version}" == "16.04" ]] && sed -e 's/MemoryMax/MemoryLimit/g' -i /etc/systemd/system/box.service
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now cloudron-syslog
|
||||
systemctl enable unbound
|
||||
systemctl enable cloudron-syslog
|
||||
systemctl enable cloudron.target
|
||||
systemctl enable box
|
||||
systemctl enable cloudron-firewall
|
||||
systemctl enable --now cloudron-disable-thp
|
||||
|
||||
# update firewall rules
|
||||
systemctl restart cloudron-firewall
|
||||
@@ -127,6 +132,12 @@ echo "==> Configuring collectd"
|
||||
rm -rf /etc/collectd /var/log/collectd.log
|
||||
ln -sfF "${PLATFORM_DATA_DIR}/collectd" /etc/collectd
|
||||
cp "${script_dir}/start/collectd/collectd.conf" "${PLATFORM_DATA_DIR}/collectd/collectd.conf"
|
||||
if [[ "${ubuntu_version}" == "20.04" ]]; then
|
||||
# https://bugs.launchpad.net/ubuntu/+source/collectd/+bug/1872281
|
||||
if ! grep -q LD_PRELOAD /etc/default/collectd; then
|
||||
echo -e "\nLD_PRELOAD=/usr/lib/python3.8/config-3.8-x86_64-linux-gnu/libpython3.8.so" >> /etc/default/collectd
|
||||
fi
|
||||
fi
|
||||
systemctl restart collectd
|
||||
|
||||
echo "==> Configuring logrotate"
|
||||
@@ -184,6 +195,10 @@ fi
|
||||
|
||||
readonly mysql_root_password="password"
|
||||
mysqladmin -u root -ppassword password password # reset default root password
|
||||
if [[ "${ubuntu_version}" == "20.04" ]]; then
|
||||
# mysql 8 added a new caching_sha2_password scheme which mysqljs does not support
|
||||
mysql -u root -p${mysql_root_password} -e "ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '${mysql_root_password}';"
|
||||
fi
|
||||
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
|
||||
@@ -205,13 +220,20 @@ else
|
||||
cp "${BOX_DATA_DIR}/dhparams.pem" "${PLATFORM_DATA_DIR}/addons/mail/dhparams.pem"
|
||||
fi
|
||||
|
||||
if [[ ! -f "${BOX_DATA_DIR}/sftp/ssh/ssh_host_rsa_key" ]]; then
|
||||
# the key format in Ubuntu 20 changed, so we create keys in legacy format. for older ubuntu, just re-use the host keys
|
||||
# see https://github.com/proftpd/proftpd/issues/793
|
||||
if [[ "${ubuntu_version}" == "20.04" ]]; then
|
||||
ssh-keygen -m PEM -t rsa -f "${BOX_DATA_DIR}/sftp/ssh/ssh_host_rsa_key" -q -N ""
|
||||
else
|
||||
cp /etc/ssh/ssh_host_rsa_key* ${BOX_DATA_DIR}/sftp/ssh
|
||||
fi
|
||||
fi
|
||||
|
||||
# old installations used to create appdata/<app>/redis which is now part of old backups and prevents restore
|
||||
echo "==> Cleaning up stale redis directories"
|
||||
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"
|
||||
# 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
|
||||
@@ -227,7 +249,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
|
||||
|
||||
echo "==> Starting Cloudron"
|
||||
systemctl start cloudron.target
|
||||
systemctl start box
|
||||
|
||||
sleep 2 # give systemd sometime to start the processes
|
||||
|
||||
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu
|
||||
|
||||
echo "==> Disabling THP"
|
||||
|
||||
# https://docs.couchbase.com/server/current/install/thp-disable.html
|
||||
if [[ -d /sys/kernel/mm/transparent_hugepage ]]; then
|
||||
echo "never" > /sys/kernel/mm/transparent_hugepage/enabled
|
||||
echo "never" > /sys/kernel/mm/transparent_hugepage/defrag
|
||||
else
|
||||
echo "==> kernel does not have THP"
|
||||
fi
|
||||
|
||||
@@ -6,11 +6,29 @@ echo "==> Setting up firewall"
|
||||
iptables -t filter -N CLOUDRON || true
|
||||
iptables -t filter -F CLOUDRON # empty any existing rules
|
||||
|
||||
# NOTE: keep these in sync with src/apps.js validatePortBindings
|
||||
# allow ssh, http, https, ping, dns
|
||||
iptables -t filter -I CLOUDRON -m state --state RELATED,ESTABLISHED -j ACCEPT
|
||||
# ssh is allowed alternately on port 202
|
||||
iptables -A CLOUDRON -p tcp -m tcp -m multiport --dports 22,25,80,202,443,587,993,4190 -j ACCEPT
|
||||
# first setup any user IP block lists
|
||||
ipset create cloudron_blocklist hash:net || true
|
||||
/home/yellowtent/box/src/scripts/setblocklist.sh
|
||||
|
||||
iptables -t filter -A CLOUDRON -m set --match-set cloudron_blocklist src -j DROP
|
||||
# the DOCKER-USER chain is not cleared on docker restart
|
||||
if ! iptables -t filter -C DOCKER-USER -m set --match-set cloudron_blocklist src -j DROP; then
|
||||
iptables -t filter -I DOCKER-USER 1 -m set --match-set cloudron_blocklist src -j DROP
|
||||
fi
|
||||
|
||||
# allow related and establisted connections
|
||||
iptables -t filter -A CLOUDRON -m state --state RELATED,ESTABLISHED -j ACCEPT
|
||||
iptables -t filter -A CLOUDRON -p tcp -m tcp -m multiport --dports 22,25,80,202,443 -j ACCEPT # 202 is the alternate ssh port
|
||||
|
||||
# whitelist any user ports
|
||||
ports_json="/home/yellowtent/boxdata/firewall/ports.json"
|
||||
if allowed_tcp_ports=$(node -e "console.log(JSON.parse(fs.readFileSync('${ports_json}', 'utf8')).allowed_tcp_ports.join(','))" 2>/dev/null); then
|
||||
[[ -n "${allowed_tcp_ports}" ]] && iptables -A CLOUDRON -p tcp -m tcp -m multiport --dports "${allowed_tcp_ports}" -j ACCEPT
|
||||
fi
|
||||
|
||||
if allowed_udp_ports=$(node -e "console.log(JSON.parse(fs.readFileSync('${ports_json}', 'utf8')).allowed_udp_ports.join(','))" 2>/dev/null); then
|
||||
[[ -n "${allowed_tcp_ports}" ]] && iptables -A CLOUDRON -p udp -m udp -m multiport --dports "${allowed_tcp_ports}" -j ACCEPT
|
||||
fi
|
||||
|
||||
# turn and stun service
|
||||
iptables -t filter -A CLOUDRON -p tcp -m multiport --dports 3478,5349 -j ACCEPT
|
||||
@@ -52,8 +70,6 @@ for port in 22 202; do
|
||||
iptables -A CLOUDRON_RATELIMIT -p tcp --dport ${port} -m state --state NEW -m recent --update --name "public-${port}" --seconds 10 --hitcount 5 -j CLOUDRON_RATELIMIT_LOG
|
||||
done
|
||||
|
||||
# TODO: move docker platform rules to platform.js so it can be specialized to rate limit only when destination is the mail container
|
||||
|
||||
# docker translates (dnat) 25, 587, 993, 4190 in the PREROUTING step
|
||||
for port in 2525 4190 9993; do
|
||||
iptables -A CLOUDRON_RATELIMIT -p tcp --syn ! -s 172.18.0.0/16 -d 172.18.0.0/16 --dport ${port} -m connlimit --connlimit-above 50 -j CLOUDRON_RATELIMIT_LOG
|
||||
@@ -69,12 +85,10 @@ for port in 3306 5432 6379 27017; do
|
||||
iptables -A CLOUDRON_RATELIMIT -p tcp --syn -s 172.18.0.0/16 -d 172.18.0.0/16 --dport ${port} -m connlimit --connlimit-above 5000 -j CLOUDRON_RATELIMIT_LOG
|
||||
done
|
||||
|
||||
# For ssh, http, https
|
||||
if ! iptables -t filter -C INPUT -j CLOUDRON_RATELIMIT 2>/dev/null; then
|
||||
iptables -t filter -I INPUT 1 -j CLOUDRON_RATELIMIT
|
||||
fi
|
||||
|
||||
# For smtp, imap etc routed via docker/nat
|
||||
# Workaroud issue where Docker insists on adding itself first in FORWARD table
|
||||
# Workaround issue where Docker insists on adding itself first in FORWARD table
|
||||
iptables -D FORWARD -j CLOUDRON_RATELIMIT || true
|
||||
iptables -I FORWARD 1 -j CLOUDRON_RATELIMIT
|
||||
|
||||
@@ -14,8 +14,8 @@ if [[ -z "$(ls -A /home/yellowtent/boxdata/mail/dkim)" ]]; then
|
||||
printf "\t\t\t-------------------\n"
|
||||
|
||||
printf '\n\e[1;32m%-6s\e[m\n\n' "Visit https://${ip} on your browser and accept the self-signed certificate to finish setup."
|
||||
printf "Cloudron overview - https://cloudron.io/documentation/ \n"
|
||||
printf "Cloudron setup - https://cloudron.io/documentation/installation/#setup \n"
|
||||
printf "Cloudron overview - https://docs.cloudron.io/ \n"
|
||||
printf "Cloudron setup - https://docs.cloudron.io/installation/#setup \n"
|
||||
else
|
||||
printf "\t\t\tNOTE TO CLOUDRON ADMINS\n"
|
||||
printf "\t\t\t-----------------------\n"
|
||||
@@ -23,7 +23,7 @@ else
|
||||
printf "Cloudron relies on and may break your installation. Ubuntu security updates\n"
|
||||
printf "are automatically installed on this server every night.\n"
|
||||
printf "\n"
|
||||
printf "Read more at https://cloudron.io/documentation/security/#os-updates\n"
|
||||
printf "Read more at https://docs.cloudron.io/security/#os-updates\n"
|
||||
fi
|
||||
|
||||
printf "\nFor help and more information, visit https://forum.cloudron.io\n\n"
|
||||
|
||||
@@ -4,6 +4,11 @@ set -eu -o pipefail
|
||||
|
||||
readonly APPS_SWAP_FILE="/apps.swap"
|
||||
|
||||
if [[ -f "${APPS_SWAP_FILE}" ]]; then
|
||||
echo "Swap file already exists at /apps.swap . Skipping"
|
||||
exit
|
||||
fi
|
||||
|
||||
# all sizes are in mb
|
||||
readonly physical_memory=$(LC_ALL=C free -m | awk '/Mem:/ { print $2 }')
|
||||
readonly swap_size=$((${physical_memory} > 4096 ? 4096 : ${physical_memory})) # min(RAM, 4GB) if you change this, fix enoughResourcesAvailable() in client.js
|
||||
|
||||
@@ -121,7 +121,7 @@ LoadPlugin memory
|
||||
#LoadPlugin netlink
|
||||
#LoadPlugin network
|
||||
#LoadPlugin nfs
|
||||
LoadPlugin nginx
|
||||
#LoadPlugin nginx
|
||||
#LoadPlugin notify_desktop
|
||||
#LoadPlugin notify_email
|
||||
#LoadPlugin ntpd
|
||||
@@ -149,7 +149,7 @@ LoadPlugin nginx
|
||||
#LoadPlugin statsd
|
||||
LoadPlugin swap
|
||||
#LoadPlugin table
|
||||
LoadPlugin tail
|
||||
#LoadPlugin tail
|
||||
#LoadPlugin tail_csv
|
||||
#LoadPlugin tcpconns
|
||||
#LoadPlugin teamspeak2
|
||||
@@ -197,42 +197,11 @@ LoadPlugin write_graphite
|
||||
IgnoreSelected false
|
||||
</Plugin>
|
||||
|
||||
<Plugin nginx>
|
||||
URL "http://127.0.0.1/nginx_status"
|
||||
</Plugin>
|
||||
|
||||
<Plugin swap>
|
||||
ReportByDevice false
|
||||
ReportBytes true
|
||||
</Plugin>
|
||||
|
||||
<Plugin "tail">
|
||||
<File "/var/log/nginx/error.log">
|
||||
Instance "nginx"
|
||||
<Match>
|
||||
Regex ".*"
|
||||
DSType "CounterInc"
|
||||
Type counter
|
||||
Instance "errors"
|
||||
</Match>
|
||||
</File>
|
||||
<File "/var/log/nginx/access.log">
|
||||
Instance "nginx"
|
||||
<Match>
|
||||
Regex ".*"
|
||||
DSType "CounterInc"
|
||||
Type counter
|
||||
Instance "requests"
|
||||
</Match>
|
||||
<Match>
|
||||
Regex " \".*\" [0-9]+ [0-9]+ ([0-9]+)"
|
||||
DSType GaugeAverage
|
||||
Type delay
|
||||
Instance "response"
|
||||
</Match>
|
||||
</File>
|
||||
</Plugin>
|
||||
|
||||
<Plugin python>
|
||||
# https://blog.dbrgn.ch/2017/3/10/write-a-collectd-python-plugin/
|
||||
ModulePath "/home/yellowtent/box/setup/start/collectd/"
|
||||
|
||||
@@ -6,7 +6,7 @@ disks = []
|
||||
|
||||
def init():
|
||||
global disks
|
||||
lines = [s.split() for s in subprocess.check_output(["df", "--type=ext4", "--output=source,target,size,used,avail"]).splitlines()]
|
||||
lines = [s.split() for s in subprocess.check_output(["df", "--type=ext4", "--output=source,target,size,used,avail"]).decode('utf-8').splitlines()]
|
||||
disks = lines[1:] # strip header
|
||||
collectd.info('custom df plugin initialized with %s' % disks)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
performance_schema=OFF
|
||||
max_connections=50
|
||||
# on ec2, without this we get a sporadic connection drop when doing the initial migration
|
||||
max_allowed_packet=32M
|
||||
max_allowed_packet=64M
|
||||
|
||||
# https://mathiasbynens.be/notes/mysql-utf8mb4
|
||||
character-set-server = utf8mb4
|
||||
|
||||
@@ -6,7 +6,7 @@ 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;
|
||||
worker_rlimit_nofile 8192;
|
||||
|
||||
pid /run/nginx.pid;
|
||||
|
||||
@@ -43,23 +43,5 @@ http {
|
||||
# zones for rate limiting
|
||||
limit_req_zone $binary_remote_addr zone=admin_login:10m rate=10r/s; # 10 request a second
|
||||
|
||||
|
||||
# default http server that returns 404 for any domain we are not listening on
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
server_name does_not_match_anything;
|
||||
|
||||
# acme challenges (for app installation and re-configure when the vhost config does not exist)
|
||||
location /.well-known/acme-challenge/ {
|
||||
default_type text/plain;
|
||||
alias /home/yellowtent/platformdata/acme/;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 404;
|
||||
}
|
||||
}
|
||||
|
||||
include applications/*.conf;
|
||||
}
|
||||
|
||||
@@ -50,3 +50,15 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restartdocker.s
|
||||
Defaults!/home/yellowtent/box/src/scripts/restartunbound.sh env_keep="HOME BOX_ENV"
|
||||
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
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/setblocklist.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/setblocklist.sh
|
||||
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
[Unit]
|
||||
Description=Cloudron Admin
|
||||
OnFailure=crashnotifier@%n.service
|
||||
StopWhenUnneeded=true
|
||||
; journald crashes result in a EPIPE in node. Cannot ignore it as it results in loss of logs.
|
||||
BindsTo=systemd-journald.service
|
||||
After=mysql.service nginx.service
|
||||
; As cloudron-resize-fs is a one-shot, the Wants= automatically ensures that the service *finishes*
|
||||
Wants=cloudron-resize-fs.service
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
[Service]
|
||||
Type=idle
|
||||
WorkingDirectory=/home/yellowtent/box
|
||||
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=/bin/sh -c 'echo "Logging to /home/yellowtent/platformdata/logs/box.log"; exec /usr/bin/node /home/yellowtent/box/box.js >> /home/yellowtent/platformdata/logs/box.log 2>&1'
|
||||
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmile" "BOX_ENV=cloudron" "NODE_ENV=production"
|
||||
ExecStart=/home/yellowtent/box/box.js
|
||||
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box:*,connect-lastmile,-box:ldap" "BOX_ENV=cloudron" "NODE_ENV=production"
|
||||
; kill apptask processes as well
|
||||
KillMode=control-group
|
||||
; Do not kill this process on OOM. Children inherit this score. Do not set it to -1000 so that MemoryMax can keep working
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# https://docs.mongodb.com/manual/tutorial/transparent-huge-pages/
|
||||
[Unit]
|
||||
Description=Disable Transparent Huge Pages (THP)
|
||||
DefaultDependencies=no
|
||||
After=sysinit.target local-fs.target
|
||||
Before=docker.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart="/home/yellowtent/box/setup/start/cloudron-disable-thp.sh"
|
||||
RemainAfterExit=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=basic.target
|
||||
|
||||
@@ -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
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[Unit]
|
||||
Description=Unbound DNS Resolver
|
||||
After=network.target
|
||||
After=network.target docker.service
|
||||
|
||||
[Service]
|
||||
PIDFile=/run/unbound.pid
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
server:
|
||||
interface: 0.0.0.0
|
||||
port: 53
|
||||
interface: 127.0.0.1
|
||||
interface: 172.18.0.1
|
||||
do-ip6: no
|
||||
access-control: 127.0.0.1 allow
|
||||
access-control: 172.18.0.1/16 allow
|
||||
|
||||
+1
-1
@@ -4,4 +4,4 @@ set -eu -o pipefail
|
||||
|
||||
echo "Stopping cloudron"
|
||||
|
||||
systemctl stop cloudron.target
|
||||
systemctl stop box
|
||||
|
||||
+222
-107
@@ -1,29 +1,31 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
getServices: getServices,
|
||||
getService: getService,
|
||||
configureService: configureService,
|
||||
getServiceLogs: getServiceLogs,
|
||||
restartService: restartService,
|
||||
getServices,
|
||||
getService,
|
||||
getServicesConfig,
|
||||
configureService,
|
||||
getServiceLogs,
|
||||
restartService,
|
||||
rebuildService,
|
||||
|
||||
startAppServices,
|
||||
stopAppServices,
|
||||
|
||||
startServices: startServices,
|
||||
updateServiceConfig: updateServiceConfig,
|
||||
startServices,
|
||||
updateServiceConfig,
|
||||
|
||||
setupAddons: setupAddons,
|
||||
teardownAddons: teardownAddons,
|
||||
backupAddons: backupAddons,
|
||||
restoreAddons: restoreAddons,
|
||||
clearAddons: clearAddons,
|
||||
setupAddons,
|
||||
teardownAddons,
|
||||
backupAddons,
|
||||
restoreAddons,
|
||||
clearAddons,
|
||||
|
||||
getEnvironment: getEnvironment,
|
||||
getMountsSync: getMountsSync,
|
||||
getContainerNamesSync: getContainerNamesSync,
|
||||
getEnvironment,
|
||||
getMountsSync,
|
||||
getContainerNamesSync,
|
||||
|
||||
getContainerDetails: getContainerDetails,
|
||||
getContainerDetails,
|
||||
|
||||
SERVICE_STATUS_STARTING: 'starting', // container up, waiting for healthcheck
|
||||
SERVICE_STATUS_ACTIVE: 'active',
|
||||
@@ -115,6 +117,13 @@ var ADDONS = {
|
||||
restore: restorePostgreSql,
|
||||
clear: clearPostgreSql,
|
||||
},
|
||||
proxyAuth: {
|
||||
setup: setupProxyAuth,
|
||||
teardown: teardownProxyAuth,
|
||||
backup: NOOP,
|
||||
restore: NOOP,
|
||||
clear: NOOP
|
||||
},
|
||||
recvmail: {
|
||||
setup: setupRecvMail,
|
||||
teardown: teardownRecvMail,
|
||||
@@ -174,7 +183,7 @@ const SERVICES = {
|
||||
mongodb: {
|
||||
status: containerStatus.bind(null, 'mongodb', 'CLOUDRON_MONGODB_TOKEN'),
|
||||
restart: restartContainer.bind(null, 'mongodb'),
|
||||
defaultMemoryLimit: (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 200 * 1024 * 1024
|
||||
defaultMemoryLimit: (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 256 * 1024 * 1024
|
||||
},
|
||||
mysql: {
|
||||
status: containerStatus.bind(null, 'mysql', 'CLOUDRON_MYSQL_TOKEN'),
|
||||
@@ -277,7 +286,7 @@ function restartContainer(name, callback) {
|
||||
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) console.error(`Unable to rebuild service ${name}`, error); });
|
||||
return rebuildService(name, function (error) { if (error) debug(`restartContainer: Unable to rebuild service ${name}`, error); });
|
||||
}
|
||||
if (error) return callback(error);
|
||||
|
||||
@@ -317,7 +326,7 @@ function containerStatus(containerName, tokenEnvName, callback) {
|
||||
if (error && (error.reason === BoxError.NOT_FOUND || error.reason === BoxError.INACTIVE)) return callback(null, { status: exports.SERVICE_STATUS_STOPPED });
|
||||
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, timeout: 3000 }, function (error, response) {
|
||||
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 ${containerName}. Status code: ${response.statusCode} message: ${response.body.message}` });
|
||||
|
||||
@@ -327,7 +336,8 @@ function containerStatus(containerName, tokenEnvName, callback) {
|
||||
var tmp = {
|
||||
status: addonDetails.state.Running ? exports.SERVICE_STATUS_ACTIVE : exports.SERVICE_STATUS_STOPPED,
|
||||
memoryUsed: result.memory_stats.usage,
|
||||
memoryPercent: parseInt(100 * result.memory_stats.usage / result.memory_stats.limit)
|
||||
memoryPercent: parseInt(100 * result.memory_stats.usage / result.memory_stats.limit),
|
||||
healthcheck: response.body
|
||||
};
|
||||
|
||||
callback(null, tmp);
|
||||
@@ -396,6 +406,7 @@ function getService(id, callback) {
|
||||
memoryUsed: 0,
|
||||
memoryPercent: 0,
|
||||
error: null,
|
||||
healthcheck: null,
|
||||
config: {
|
||||
// If a property is not set then we cannot change it through the api, see below
|
||||
// memory: 0,
|
||||
@@ -410,16 +421,15 @@ function getService(id, callback) {
|
||||
tmp.memoryUsed = result.memoryUsed;
|
||||
tmp.memoryPercent = result.memoryPercent;
|
||||
tmp.error = result.error || null;
|
||||
tmp.healthcheck = result.healthcheck || null;
|
||||
|
||||
getServicesConfig(id, function (error, service, servicesConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const serviceConfig = servicesConfig[name];
|
||||
tmp.config = Object.assign({}, serviceConfig);
|
||||
|
||||
if (serviceConfig && serviceConfig.memory && serviceConfig.memorySwap) {
|
||||
tmp.config.memory = serviceConfig.memory;
|
||||
tmp.config.memorySwap = serviceConfig.memorySwap;
|
||||
} else if (service.defaultMemoryLimit) {
|
||||
if ((!tmp.config.memory || !tmp.config.memorySwap) && service.defaultMemoryLimit) {
|
||||
tmp.config.memory = service.defaultMemoryLimit;
|
||||
tmp.config.memorySwap = tmp.config.memory * 2;
|
||||
}
|
||||
@@ -449,10 +459,10 @@ function configureService(id, data, callback) {
|
||||
|
||||
// if not specified we clear the entry and use defaults
|
||||
if (!data.memory || !data.memorySwap) {
|
||||
delete servicesConfig[name];
|
||||
delete servicesConfig[name].memory;
|
||||
delete servicesConfig[name].memorySwap;
|
||||
} else {
|
||||
servicesConfig[name].memory = data.memory;
|
||||
servicesConfig[name].memorySwap = data.memorySwap;
|
||||
servicesConfig[name] = data;
|
||||
}
|
||||
|
||||
if (instance) {
|
||||
@@ -613,7 +623,7 @@ function waitForContainer(containerName, tokenEnvName, callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.retry({ times: 10, interval: 15000 }, function (retryCallback) {
|
||||
request.get(`https://${result.ip}:3000/healthcheck?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
|
||||
request.get(`https://${result.ip}:3000/healthcheck?access_token=${result.token}`, { json: true, rejectUnauthorized: false, timeout: 3000 }, function (error, response) {
|
||||
if (error) return retryCallback(new BoxError(BoxError.ADDONS_ERROR, `Network error waiting for ${containerName}: ${error.message}`));
|
||||
if (response.statusCode !== 200 || !response.body.status) return retryCallback(new BoxError(BoxError.ADDONS_ERROR, `Error waiting for ${containerName}. Status code: ${response.statusCode} message: ${response.body.message}`));
|
||||
|
||||
@@ -733,10 +743,10 @@ function importDatabase(addon, callback) {
|
||||
|
||||
debug(`importDatabase: Importing ${addon}`);
|
||||
|
||||
appdb.getAll(function (error, apps) {
|
||||
appdb.getAll(function (error, allApps) {
|
||||
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
|
||||
|
||||
debug(`importDatabase: Importing addon ${addon} of app ${app.id}`);
|
||||
@@ -749,14 +759,57 @@ function importDatabase(addon, callback) {
|
||||
// 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);
|
||||
});
|
||||
}, 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function updateServiceConfig(platformConfig, callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
debug('updateServiceConfig: %j', platformConfig);
|
||||
assert.strictEqual(typeof platformConfig, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
async.eachSeries([ 'mysql', 'postgresql', 'mail', 'mongodb', 'graphite' ], function iterator(serviceName, iteratorCallback) {
|
||||
const containerConfig = platformConfig[serviceName];
|
||||
@@ -770,7 +823,11 @@ function updateServiceConfig(platformConfig, callback) {
|
||||
}
|
||||
|
||||
const args = `update --memory ${memory} --memory-swap ${memorySwap} ${serviceName}`.split(' ');
|
||||
shell.spawn(`update${serviceName}`, '/usr/bin/docker', args, { }, iteratorCallback);
|
||||
// scale back db containers, if possible. this is retried because updating memory constraints can fail
|
||||
// with failed to write to memory.memsw.limit_in_bytes: write /sys/fs/cgroup/memory/docker/xx/memory.memsw.limit_in_bytes: device or resource busy
|
||||
async.retry({ times: 10, interval: 60 * 1000 }, function (retryCallback) {
|
||||
shell.spawn(`updateServiceConfig(${serviceName})`, '/usr/bin/docker', args, { }, retryCallback);
|
||||
}, iteratorCallback);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
@@ -815,7 +872,7 @@ function startServices(existingInfra, callback) {
|
||||
} else {
|
||||
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.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));
|
||||
@@ -932,7 +989,7 @@ function setupTurn(app, options, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
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 = [
|
||||
{ name: 'CLOUDRON_STUN_SERVER', value: settings.adminFqdn() },
|
||||
@@ -981,6 +1038,7 @@ function setupEmail(app, options, callback) {
|
||||
{ name: `${envPrefix}MAIL_SIEVE_PORT`, value: '4190' },
|
||||
{ name: `${envPrefix}MAIL_DOMAIN`, value: app.domain },
|
||||
{ name: `${envPrefix}MAIL_DOMAINS`, value: mailInDomains },
|
||||
{ name: 'CLOUDRON_MAIL_SERVER_HOST', value: settings.mailFqdn() },
|
||||
{ name: `${envPrefix}LDAP_MAILBOXES_BASE_DN`, value: 'ou=mailboxes,dc=cloudron' }
|
||||
];
|
||||
|
||||
@@ -1011,6 +1069,7 @@ function setupLdap(app, options, callback) {
|
||||
|
||||
var env = [
|
||||
{ name: `${envPrefix}LDAP_SERVER`, value: '172.18.0.1' },
|
||||
{ name: 'CLOUDRON_LDAP_HOST', value: '172.18.0.1' }, // to keep things in sync with the database _HOST vars
|
||||
{ name: `${envPrefix}LDAP_PORT`, value: '' + constants.LDAP_PORT },
|
||||
{ name: `${envPrefix}LDAP_URL`, value: 'ldap://172.18.0.1:' + constants.LDAP_PORT },
|
||||
{ name: `${envPrefix}LDAP_USERS_BASE_DN`, value: 'ou=users,dc=cloudron' },
|
||||
@@ -1126,16 +1185,16 @@ function startMysql(existingInfra, callback) {
|
||||
const dataDir = paths.PLATFORM_DATA_DIR;
|
||||
const rootPassword = hat(8 * 128);
|
||||
const cloudronToken = hat(8 * 128);
|
||||
const memoryLimit = 4 * 256;
|
||||
|
||||
const upgrading = existingInfra.version !== 'none' && requiresUpgrade(existingInfra.images.mysql.tag, tag);
|
||||
|
||||
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) {
|
||||
if (error) return callback(error);
|
||||
|
||||
// memory options are applied dynamically. import requires all the memory we can get
|
||||
const cmd = `docker run --restart=always -d --name="mysql" \
|
||||
--hostname mysql \
|
||||
--net cloudron \
|
||||
@@ -1144,8 +1203,6 @@ function startMysql(existingInfra, callback) {
|
||||
--log-opt syslog-address=udp://127.0.0.1:2514 \
|
||||
--log-opt syslog-format=rfc5424 \
|
||||
--log-opt tag=mysql \
|
||||
-m ${memoryLimit}m \
|
||||
--memory-swap ${memoryLimit * 2}m \
|
||||
--dns 172.18.0.1 \
|
||||
--dns-search=. \
|
||||
-e CLOUDRON_MYSQL_TOKEN=${cloudronToken} \
|
||||
@@ -1155,7 +1212,11 @@ function startMysql(existingInfra, callback) {
|
||||
--label isCloudronManaged=true \
|
||||
--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);
|
||||
|
||||
waitForContainer('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error) {
|
||||
@@ -1343,16 +1404,16 @@ function startPostgresql(existingInfra, callback) {
|
||||
const dataDir = paths.PLATFORM_DATA_DIR;
|
||||
const rootPassword = hat(8 * 128);
|
||||
const cloudronToken = hat(8 * 128);
|
||||
const memoryLimit = 4 * 256;
|
||||
|
||||
const upgrading = existingInfra.version !== 'none' && requiresUpgrade(existingInfra.images.postgresql.tag, tag);
|
||||
|
||||
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) {
|
||||
if (error) return callback(error);
|
||||
|
||||
// memory options are applied dynamically. import requires all the memory we can get
|
||||
const cmd = `docker run --restart=always -d --name="postgresql" \
|
||||
--hostname postgresql \
|
||||
--net cloudron \
|
||||
@@ -1361,8 +1422,6 @@ function startPostgresql(existingInfra, callback) {
|
||||
--log-opt syslog-address=udp://127.0.0.1:2514 \
|
||||
--log-opt syslog-format=rfc5424 \
|
||||
--log-opt tag=postgresql \
|
||||
-m ${memoryLimit}m \
|
||||
--memory-swap ${memoryLimit * 2}m \
|
||||
--dns 172.18.0.1 \
|
||||
--dns-search=. \
|
||||
-e CLOUDRON_POSTGRESQL_ROOT_PASSWORD="${rootPassword}" \
|
||||
@@ -1371,7 +1430,11 @@ function startPostgresql(existingInfra, callback) {
|
||||
--label isCloudronManaged=true \
|
||||
--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);
|
||||
|
||||
waitForContainer('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error) {
|
||||
@@ -1399,7 +1462,8 @@ function setupPostgreSql(app, options, callback) {
|
||||
const data = {
|
||||
database: database,
|
||||
username: username,
|
||||
password: error ? hat(4 * 128) : existingPassword
|
||||
password: error ? hat(4 * 128) : existingPassword,
|
||||
locale: options.locale || 'C'
|
||||
};
|
||||
|
||||
getContainerDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
|
||||
@@ -1433,13 +1497,14 @@ function clearPostgreSql(app, options, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const { database, username } = postgreSqlNames(app.id);
|
||||
const locale = options.locale || 'C';
|
||||
|
||||
debugApp(app, 'Clearing postgresql');
|
||||
|
||||
getContainerDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
|
||||
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}&locale=${locale}`, { json: true, rejectUnauthorized: false }, function (error, response) {
|
||||
if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Network error clearing postgresql: ${error.message}`));
|
||||
if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error clearing postgresql. Status code: ${response.statusCode} message: ${response.body.message}`));
|
||||
|
||||
@@ -1527,9 +1592,7 @@ function startTurn(existingInfra, callback) {
|
||||
const memoryLimit = 256;
|
||||
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. note that this runs on the host network!
|
||||
const cmd = `docker run --restart=always -d --name="turn" \
|
||||
--hostname turn \
|
||||
--net host \
|
||||
@@ -1546,7 +1609,11 @@ function startTurn(existingInfra, callback) {
|
||||
--label isCloudronManaged=true \
|
||||
--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) {
|
||||
@@ -1557,16 +1624,16 @@ function startMongodb(existingInfra, callback) {
|
||||
const dataDir = paths.PLATFORM_DATA_DIR;
|
||||
const rootPassword = hat(8 * 128);
|
||||
const cloudronToken = hat(8 * 128);
|
||||
const memoryLimit = 4 * 256;
|
||||
|
||||
const upgrading = existingInfra.version !== 'none' && requiresUpgrade(existingInfra.images.mongodb.tag, tag);
|
||||
|
||||
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) {
|
||||
if (error) return callback(error);
|
||||
|
||||
// memory options are applied dynamically. import requires all the memory we can get
|
||||
const cmd = `docker run --restart=always -d --name="mongodb" \
|
||||
--hostname mongodb \
|
||||
--net cloudron \
|
||||
@@ -1575,8 +1642,6 @@ function startMongodb(existingInfra, callback) {
|
||||
--log-opt syslog-address=udp://127.0.0.1:2514 \
|
||||
--log-opt syslog-format=rfc5424 \
|
||||
--log-opt tag=mongodb \
|
||||
-m ${memoryLimit}m \
|
||||
--memory-swap ${memoryLimit * 2}m \
|
||||
--dns 172.18.0.1 \
|
||||
--dns-search=. \
|
||||
-e CLOUDRON_MONGODB_ROOT_PASSWORD="${rootPassword}" \
|
||||
@@ -1585,7 +1650,11 @@ function startMongodb(existingInfra, callback) {
|
||||
--label isCloudronManaged=true \
|
||||
--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);
|
||||
|
||||
waitForContainer('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error) {
|
||||
@@ -1608,37 +1677,41 @@ function setupMongoDb(app, options, callback) {
|
||||
appdb.getAddonConfigByName(app.id, 'mongodb', '%MONGODB_PASSWORD', function (error, existingPassword) {
|
||||
if (error && error.reason !== BoxError.NOT_FOUND) return callback(error);
|
||||
|
||||
const data = {
|
||||
database: app.id,
|
||||
username: app.id,
|
||||
password: error ? hat(4 * 128) : existingPassword,
|
||||
oplog: !!options.oplog
|
||||
};
|
||||
appdb.getAddonConfigByName(app.id, 'mongodb', '%MONGODB_DATABASE', function (error, database) {
|
||||
database = database || hat(8 * 8); // 16 bytes. keep this short, so as to not overflow the 127 byte index length in MongoDB < 4.4
|
||||
|
||||
getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
|
||||
if (error) return callback(error);
|
||||
const data = {
|
||||
database: database,
|
||||
username: app.id,
|
||||
password: error ? hat(4 * 128) : existingPassword,
|
||||
oplog: !!options.oplog
|
||||
};
|
||||
|
||||
request.post(`https://${result.ip}:3000/databases?access_token=${result.token}`, { rejectUnauthorized: false, json: data }, function (error, response) {
|
||||
if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Network error setting up mongodb: ${error.message}`));
|
||||
if (response.statusCode !== 201) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error setting up mongodb. Status code: ${response.statusCode} message: ${response.body.message}`));
|
||||
getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
|
||||
request.post(`https://${result.ip}:3000/databases?access_token=${result.token}`, { rejectUnauthorized: false, json: data }, function (error, response) {
|
||||
if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Network error setting up mongodb: ${error.message}`));
|
||||
if (response.statusCode !== 201) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error setting up mongodb. Status code: ${response.statusCode} message: ${response.body.message}`));
|
||||
|
||||
var env = [
|
||||
{ name: `${envPrefix}MONGODB_URL`, value : `mongodb://${data.username}:${data.password}@mongodb:27017/${data.database}` },
|
||||
{ name: `${envPrefix}MONGODB_USERNAME`, value : data.username },
|
||||
{ name: `${envPrefix}MONGODB_PASSWORD`, value: data.password },
|
||||
{ name: `${envPrefix}MONGODB_HOST`, value : 'mongodb' },
|
||||
{ name: `${envPrefix}MONGODB_PORT`, value : '27017' },
|
||||
{ name: `${envPrefix}MONGODB_DATABASE`, value : data.database }
|
||||
];
|
||||
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
|
||||
|
||||
if (options.oplog) {
|
||||
env.push({ name: `${envPrefix}MONGODB_OPLOG_URL`, value : `mongodb://${data.username}:${data.password}@mongodb:27017/local?authSource=${data.database}` });
|
||||
}
|
||||
var env = [
|
||||
{ name: `${envPrefix}MONGODB_URL`, value : `mongodb://${data.username}:${data.password}@mongodb:27017/${data.database}` },
|
||||
{ name: `${envPrefix}MONGODB_USERNAME`, value : data.username },
|
||||
{ name: `${envPrefix}MONGODB_PASSWORD`, value: data.password },
|
||||
{ name: `${envPrefix}MONGODB_HOST`, value : 'mongodb' },
|
||||
{ name: `${envPrefix}MONGODB_PORT`, value : '27017' },
|
||||
{ name: `${envPrefix}MONGODB_DATABASE`, value : data.database }
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting mongodb addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'mongodb', env, callback);
|
||||
if (options.oplog) {
|
||||
env.push({ name: `${envPrefix}MONGODB_OPLOG_URL`, value : `mongodb://${data.username}:${data.password}@mongodb:27017/local?authSource=${data.database}` });
|
||||
}
|
||||
|
||||
debugApp(app, 'Setting mongodb addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'mongodb', env, callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1649,16 +1722,18 @@ function clearMongodb(app, options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'Clearing mongodb');
|
||||
|
||||
getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
|
||||
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) {
|
||||
if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Network error clearing mongodb: ${error.message}`));
|
||||
if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error clearing mongodb. Status code: ${response.statusCode} message: ${response.body.message}`));
|
||||
appdb.getAddonConfigByName(app.id, 'mongodb', '%MONGODB_DATABASE', function (error, database) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback();
|
||||
request.post(`https://${result.ip}:3000/databases/${database}/clear?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
|
||||
if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Network error clearing mongodb: ${error.message}`));
|
||||
if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error clearing mongodb. Status code: ${response.statusCode} message: ${response.body.message}`));
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1668,16 +1743,19 @@ function teardownMongoDb(app, options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'Tearing down mongodb');
|
||||
|
||||
getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
|
||||
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) {
|
||||
if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error tearing down mongodb: ${error.message}`));
|
||||
if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error tearing down mongodb. Status code: ${response.statusCode} message: ${response.body.message}`));
|
||||
appdb.getAddonConfigByName(app.id, 'mongodb', '%MONGODB_DATABASE', function (error, database) {
|
||||
if (error && error.reason === BoxError.NOT_FOUND) return callback(null);
|
||||
if (error) return callback(error);
|
||||
|
||||
appdb.unsetAddonConfig(app.id, 'mongodb', callback);
|
||||
request.delete(`https://${result.ip}:3000/databases/${database}?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
|
||||
if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error tearing down mongodb: ${error.message}`));
|
||||
if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error tearing down mongodb. Status code: ${response.statusCode} message: ${response.body.message}`));
|
||||
|
||||
appdb.unsetAddonConfig(app.id, 'mongodb', callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1692,8 +1770,12 @@ function backupMongoDb(app, options, callback) {
|
||||
getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const url = `https://${result.ip}:3000/databases/${app.id}/backup?access_token=${result.token}`;
|
||||
pipeRequestToFile(url, dumpPath('mongodb', app.id), callback);
|
||||
appdb.getAddonConfigByName(app.id, 'mongodb', '%MONGODB_DATABASE', function (error, database) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const url = `https://${result.ip}:3000/databases/${database}/backup?access_token=${result.token}`;
|
||||
pipeRequestToFile(url, dumpPath('mongodb', app.id), callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1709,20 +1791,47 @@ function restoreMongoDb(app, options, callback) {
|
||||
getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const readStream = fs.createReadStream(dumpPath('mongodb', app.id));
|
||||
readStream.on('error', (error) => callback(new BoxError(BoxError.FS_ERROR, `Error reading input stream when restoring mongodb: ${error.message}`)));
|
||||
appdb.getAddonConfigByName(app.id, 'mongodb', '%MONGODB_DATABASE', function (error, database) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const restoreReq = request.post(`https://${result.ip}:3000/databases/${app.id}/restore?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
|
||||
if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error restoring mongodb: ${error.message}`));
|
||||
if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error restoring mongodb. Status code: ${response.statusCode} message: ${response.body.message}`));
|
||||
const readStream = fs.createReadStream(dumpPath('mongodb', app.id));
|
||||
readStream.on('error', (error) => callback(new BoxError(BoxError.FS_ERROR, `Error reading input stream when restoring mongodb: ${error.message}`)));
|
||||
|
||||
callback(null);
|
||||
const restoreReq = request.post(`https://${result.ip}:3000/databases/${database}/restore?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
|
||||
if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error restoring mongodb: ${error.message}`));
|
||||
if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error restoring mongodb. Status code: ${response.statusCode} message: ${response.body.message}`));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
|
||||
readStream.pipe(restoreReq);
|
||||
});
|
||||
|
||||
readStream.pipe(restoreReq);
|
||||
});
|
||||
}
|
||||
|
||||
function setupProxyAuth(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'Setting up proxyAuth');
|
||||
|
||||
const enabled = app.sso && app.manifest.addons && app.manifest.addons.proxyAuth;
|
||||
|
||||
if (!enabled) return callback();
|
||||
|
||||
const env = [ { name: 'CLOUDRON_PROXY_AUTH', value: '1' } ];
|
||||
appdb.setAddonConfig(app.id, 'proxyauth', env, callback);
|
||||
}
|
||||
|
||||
function teardownProxyAuth(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
appdb.unsetAddonConfig(app.id, 'proxyauth', callback);
|
||||
}
|
||||
|
||||
function startRedis(existingInfra, callback) {
|
||||
assert.strictEqual(typeof existingInfra, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -1736,13 +1845,19 @@ function startRedis(existingInfra, callback) {
|
||||
async.eachSeries(allApps, function iterator (app, iteratorCallback) {
|
||||
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) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (!upgrading) return callback();
|
||||
|
||||
importDatabase('redis', callback); // setupRedis currently starts the app container
|
||||
importDatabase('redis', callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+62
-87
@@ -1,28 +1,27 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
get: get,
|
||||
getByHttpPort: getByHttpPort,
|
||||
getByContainerId: getByContainerId,
|
||||
add: add,
|
||||
exists: exists,
|
||||
del: del,
|
||||
update: update,
|
||||
getAll: getAll,
|
||||
getPortBindings: getPortBindings,
|
||||
delPortBinding: delPortBinding,
|
||||
get,
|
||||
add,
|
||||
exists,
|
||||
del,
|
||||
update,
|
||||
getAll,
|
||||
getPortBindings,
|
||||
delPortBinding,
|
||||
|
||||
setAddonConfig: setAddonConfig,
|
||||
getAddonConfig: getAddonConfig,
|
||||
getAddonConfigByAppId: getAddonConfigByAppId,
|
||||
getAddonConfigByName: getAddonConfigByName,
|
||||
unsetAddonConfig: unsetAddonConfig,
|
||||
unsetAddonConfigByAppId: unsetAddonConfigByAppId,
|
||||
getAppIdByAddonConfigValue: getAppIdByAddonConfigValue,
|
||||
setAddonConfig,
|
||||
getAddonConfig,
|
||||
getAddonConfigByAppId,
|
||||
getAddonConfigByName,
|
||||
unsetAddonConfig,
|
||||
unsetAddonConfigByAppId,
|
||||
getAppIdByAddonConfigValue,
|
||||
getByIpAddress,
|
||||
|
||||
setHealth: setHealth,
|
||||
setTask: setTask,
|
||||
getAppStoreIds: getAppStoreIds,
|
||||
setHealth,
|
||||
setTask,
|
||||
getAppStoreIds,
|
||||
|
||||
// subdomain table types
|
||||
SUBDOMAIN_TYPE_PRIMARY: 'primary',
|
||||
@@ -39,10 +38,9 @@ var assert = require('assert'),
|
||||
util = require('util');
|
||||
|
||||
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.accessRestrictionJson', 'apps.memoryLimit', 'apps.cpuShares',
|
||||
'apps.label', 'apps.tagsJson', 'apps.taskId', 'apps.reverseProxyConfigJson', 'apps.servicesConfigJson', 'apps.bindsJson',
|
||||
'apps.sso', 'apps.debugModeJson', 'apps.enableBackup',
|
||||
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.accessRestrictionJson', 'apps.memoryLimit', 'apps.cpuShares',
|
||||
'apps.label', 'apps.tagsJson', 'apps.taskId', 'apps.reverseProxyConfigJson', 'apps.servicesConfigJson',
|
||||
'apps.sso', 'apps.debugModeJson', 'apps.enableBackup', 'apps.proxyAuth', 'apps.containerIp',
|
||||
'apps.creationTime', 'apps.updateTime', 'apps.mailboxName', 'apps.mailboxDomain', 'apps.enableAutomaticUpdate',
|
||||
'apps.dataDir', 'apps.ts', 'apps.healthTime' ].join(',');
|
||||
|
||||
@@ -89,6 +87,7 @@ function postProcess(result) {
|
||||
result.sso = !!result.sso; // make it bool
|
||||
result.enableBackup = !!result.enableBackup; // make it bool
|
||||
result.enableAutomaticUpdate = !!result.enableAutomaticUpdate; // make it bool
|
||||
result.proxyAuth = !!result.proxyAuth;
|
||||
|
||||
assert(result.debugModeJson === null || typeof result.debugModeJson === 'string');
|
||||
result.debugMode = safe.JSON.parse(result.debugModeJson);
|
||||
@@ -98,10 +97,6 @@ function postProcess(result) {
|
||||
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.forEach(function (d) {
|
||||
delete d.appId;
|
||||
@@ -116,24 +111,35 @@ function postProcess(result) {
|
||||
if (envNames[i]) result.env[envNames[i]] = envValues[i];
|
||||
}
|
||||
|
||||
let volumeIds = JSON.parse(result.volumeIds);
|
||||
delete result.volumeIds;
|
||||
let volumeReadOnlys = JSON.parse(result.volumeReadOnlys);
|
||||
delete result.volumeReadOnlys;
|
||||
|
||||
result.mounts = volumeIds[0] === null ? [] : volumeIds.map((v, idx) => { return { volumeId: v, readOnly: !!volumeReadOnlys[idx] }; }); // NOTE: volumeIds is [null] when volumes of an app is empty
|
||||
|
||||
result.error = safe.JSON.parse(result.errorJson);
|
||||
delete result.errorJson;
|
||||
|
||||
result.taskId = result.taskId ? String(result.taskId) : null;
|
||||
}
|
||||
|
||||
// each query simply join apps table with another table by id. we then join the full result together
|
||||
const PB_QUERY = 'SELECT id, GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes FROM apps LEFT JOIN appPortBindings ON apps.id = appPortBindings.appId GROUP BY apps.id';
|
||||
const ENV_QUERY = 'SELECT id, JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues FROM apps LEFT JOIN appEnvVars ON apps.id = appEnvVars.appId GROUP BY apps.id';
|
||||
const SUBDOMAIN_QUERY = `SELECT id, subdomains.subdomain AS location, subdomains.domain AS domain FROM apps LEFT JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = '${exports.SUBDOMAIN_TYPE_PRIMARY}' GROUP BY apps.id`;
|
||||
const MOUNTS_QUERY = 'SELECT id, JSON_ARRAYAGG(appMounts.volumeId) AS volumeIds, JSON_ARRAYAGG(appMounts.readOnly) AS volumeReadOnlys FROM apps LEFT JOIN appMounts ON apps.id = appMounts.appId GROUP BY apps.id';
|
||||
const APPS_QUERY = `SELECT ${APPS_FIELDS_PREFIXED}, hostPorts, environmentVariables, portTypes, envNames, envValues, location, domain, volumeIds, volumeReadOnlys FROM apps`
|
||||
+ ` LEFT JOIN (${PB_QUERY}) AS q1 on q1.id = apps.id`
|
||||
+ ` LEFT JOIN (${ENV_QUERY}) AS q2 on q2.id = apps.id`
|
||||
+ ` LEFT JOIN (${SUBDOMAIN_QUERY}) AS q3 on q3.id = apps.id`
|
||||
+ ` LEFT JOIN (${MOUNTS_QUERY}) AS q4 on q4.id = apps.id`;
|
||||
|
||||
function get(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes, '
|
||||
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
|
||||
+ ' FROM apps'
|
||||
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
|
||||
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
|
||||
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
|
||||
+ ' WHERE apps.id = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, id ], function (error, result) {
|
||||
database.query(`${APPS_QUERY} WHERE apps.id = ?`, [ id ], function (error, result) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
|
||||
|
||||
@@ -149,18 +155,11 @@ function get(id, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getByHttpPort(httpPort, callback) {
|
||||
assert.strictEqual(typeof httpPort, 'number');
|
||||
function getByIpAddress(ip, callback) {
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes,'
|
||||
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
|
||||
+ ' FROM apps'
|
||||
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
|
||||
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
|
||||
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
|
||||
+ ' WHERE httpPort = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, httpPort ], function (error, result) {
|
||||
database.query(`${APPS_QUERY} WHERE apps.containerIp = ?`, [ ip ], function (error, result) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
|
||||
|
||||
@@ -168,32 +167,7 @@ function getByHttpPort(httpPort, callback) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
result[0].alternateDomains = alternateDomains;
|
||||
postProcess(result[0]);
|
||||
|
||||
callback(null, result[0]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getByContainerId(containerId, callback) {
|
||||
assert.strictEqual(typeof containerId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes,'
|
||||
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
|
||||
+ ' FROM apps'
|
||||
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
|
||||
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
|
||||
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
|
||||
+ ' WHERE containerId = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, containerId ], function (error, result) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
|
||||
|
||||
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ result[0].id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
result[0].alternateDomains = alternateDomains;
|
||||
postProcess(result[0]);
|
||||
|
||||
callback(null, result[0]);
|
||||
@@ -204,14 +178,7 @@ function getByContainerId(containerId, callback) {
|
||||
function getAll(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes,'
|
||||
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
|
||||
+ ' FROM apps'
|
||||
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
|
||||
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
|
||||
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
|
||||
+ ' GROUP BY apps.id ORDER BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY ], function (error, results) {
|
||||
database.query(`${APPS_QUERY} ORDER BY apps.id`, [ ], function (error, results) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE type = ?', [ exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
|
||||
@@ -358,12 +325,13 @@ function del(id, callback) {
|
||||
{ query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] },
|
||||
{ query: 'DELETE FROM appEnvVars WHERE appId = ?', args: [ id ] },
|
||||
{ query: 'DELETE FROM appPasswords WHERE identifier = ?', args: [ id ] },
|
||||
{ query: 'DELETE FROM appMounts WHERE appId = ?', args: [ id ] },
|
||||
{ query: 'DELETE FROM apps WHERE id = ?', args: [ id ] }
|
||||
];
|
||||
|
||||
database.transaction(queries, function (error, results) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (results[4].affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
|
||||
if (results[5].affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -423,22 +391,29 @@ function updateWithConstraints(id, app, constraints, callback) {
|
||||
}
|
||||
|
||||
if ('location' in app && 'domain' in app) { // must be updated together as they are unique together
|
||||
queries.push({ query: 'UPDATE subdomains SET subdomain = ?, domain = ? WHERE appId = ? AND type = ?', args: [ app.location, app.domain, id, exports.SUBDOMAIN_TYPE_PRIMARY ]});
|
||||
queries.push({ query: 'DELETE FROM subdomains WHERE appId = ?', args: [ id ]}); // all locations of an app must be updated together
|
||||
queries.push({ query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, app.domain, app.location, exports.SUBDOMAIN_TYPE_PRIMARY ]});
|
||||
|
||||
if ('alternateDomains' in app) {
|
||||
app.alternateDomains.forEach(function (d) {
|
||||
queries.push({ query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_REDIRECT ]});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if ('alternateDomains' in app) {
|
||||
queries.push({ query: 'DELETE FROM subdomains WHERE appId = ? AND type = ?', args: [ id, exports.SUBDOMAIN_TYPE_REDIRECT ]});
|
||||
app.alternateDomains.forEach(function (d) {
|
||||
queries.push({ query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_REDIRECT ]});
|
||||
if ('mounts' in app) {
|
||||
queries.push({ query: 'DELETE FROM appMounts WHERE appId = ?', args: [ id ]});
|
||||
app.mounts.forEach(function (m) {
|
||||
queries.push({ query: 'INSERT INTO appMounts (appId, volumeId, readOnly) VALUES (?, ?, ?)', args: [ id, m.volumeId, m.readOnly ]});
|
||||
});
|
||||
}
|
||||
|
||||
var fields = [ ], values = [ ];
|
||||
for (var p in app) {
|
||||
if (p === 'manifest' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode' || p === 'error' || p === 'reverseProxyConfig' || p === 'servicesConfig' || p === 'binds') {
|
||||
if (p === 'manifest' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode' || p === 'error' || p === 'reverseProxyConfig' || p === 'servicesConfig') {
|
||||
fields.push(`${p}Json = ?`);
|
||||
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' && p !== 'mounts') {
|
||||
fields.push(p + ' = ?');
|
||||
values.push(app[p]);
|
||||
}
|
||||
|
||||
+5
-13
@@ -14,7 +14,7 @@ var appdb = require('./appdb.js'),
|
||||
util = require('util');
|
||||
|
||||
exports = module.exports = {
|
||||
run: run
|
||||
run
|
||||
};
|
||||
|
||||
const HEALTHCHECK_INTERVAL = 10 * 1000; // every 10 seconds. this needs to be small since the UI makes only healthy apps clickable
|
||||
@@ -79,21 +79,13 @@ function checkAppHealth(app, callback) {
|
||||
const manifest = app.manifest;
|
||||
|
||||
docker.inspect(app.containerId, function (error, data) {
|
||||
if (error || !data || !data.State) {
|
||||
debugApp(app, 'Error inspecting container');
|
||||
return setHealth(app, apps.HEALTH_ERROR, callback);
|
||||
}
|
||||
|
||||
if (data.State.Running !== true) {
|
||||
debugApp(app, 'exited');
|
||||
return setHealth(app, apps.HEALTH_DEAD, callback);
|
||||
}
|
||||
if (error || !data || !data.State) return setHealth(app, apps.HEALTH_ERROR, callback);
|
||||
if (data.State.Running !== true) return setHealth(app, apps.HEALTH_DEAD, callback);
|
||||
|
||||
// non-appstore apps may not have healthCheckPath
|
||||
if (!manifest.healthCheckPath) return setHealth(app, apps.HEALTH_HEALTHY, callback);
|
||||
|
||||
// poll through docker network instead of nginx to bypass any potential oauth proxy
|
||||
var healthCheckUrl = 'http://127.0.0.1:' + app.httpPort + manifest.healthCheckPath;
|
||||
const healthCheckUrl = `http://${app.containerIp}:${manifest.httpPort}${manifest.healthCheckPath}`;
|
||||
superagent
|
||||
.get(healthCheckUrl)
|
||||
.set('Host', app.fqdn) // required for some apache configs with rewrite rules
|
||||
@@ -103,7 +95,7 @@ function checkAppHealth(app, callback) {
|
||||
.end(function (error, res) {
|
||||
if (error && !error.response) {
|
||||
setHealth(app, apps.HEALTH_UNHEALTHY, callback);
|
||||
} else if (res.statusCode >= 400) { // 2xx and 3xx are ok
|
||||
} else if (res.statusCode >= 403) { // 2xx and 3xx are ok. even 401 and 403 are ok for now (for WP sites)
|
||||
setHealth(app, apps.HEALTH_UNHEALTHY, callback);
|
||||
} else {
|
||||
setHealth(app, apps.HEALTH_HEALTHY, callback);
|
||||
|
||||
+110
-110
@@ -1,70 +1,71 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
hasAccessTo: hasAccessTo,
|
||||
removeInternalFields: removeInternalFields,
|
||||
removeRestrictedFields: removeRestrictedFields,
|
||||
hasAccessTo,
|
||||
removeInternalFields,
|
||||
removeRestrictedFields,
|
||||
|
||||
get: get,
|
||||
getByContainerId: getByContainerId,
|
||||
getByIpAddress: getByIpAddress,
|
||||
getByFqdn: getByFqdn,
|
||||
getAll: getAll,
|
||||
getAllByUser: getAllByUser,
|
||||
install: install,
|
||||
uninstall: uninstall,
|
||||
get,
|
||||
getByIpAddress,
|
||||
getByFqdn,
|
||||
getAll,
|
||||
getAllByUser,
|
||||
install,
|
||||
uninstall,
|
||||
|
||||
setAccessRestriction: setAccessRestriction,
|
||||
setLabel: setLabel,
|
||||
setIcon: setIcon,
|
||||
setTags: setTags,
|
||||
setMemoryLimit: setMemoryLimit,
|
||||
setCpuShares: setCpuShares,
|
||||
setBinds: setBinds,
|
||||
setAutomaticBackup: setAutomaticBackup,
|
||||
setAutomaticUpdate: setAutomaticUpdate,
|
||||
setReverseProxyConfig: setReverseProxyConfig,
|
||||
setCertificate: setCertificate,
|
||||
setDebugMode: setDebugMode,
|
||||
setEnvironment: setEnvironment,
|
||||
setMailbox: setMailbox,
|
||||
setLocation: setLocation,
|
||||
setDataDir: setDataDir,
|
||||
repair: repair,
|
||||
setAccessRestriction,
|
||||
setLabel,
|
||||
setIcon,
|
||||
setTags,
|
||||
setMemoryLimit,
|
||||
setCpuShares,
|
||||
setMounts,
|
||||
setAutomaticBackup,
|
||||
setAutomaticUpdate,
|
||||
setReverseProxyConfig,
|
||||
setCertificate,
|
||||
setDebugMode,
|
||||
setEnvironment,
|
||||
setMailbox,
|
||||
setLocation,
|
||||
setDataDir,
|
||||
repair,
|
||||
|
||||
restore: restore,
|
||||
importApp: importApp,
|
||||
clone: clone,
|
||||
restore,
|
||||
importApp,
|
||||
exportApp,
|
||||
clone,
|
||||
|
||||
update: update,
|
||||
update,
|
||||
|
||||
backup: backup,
|
||||
listBackups: listBackups,
|
||||
backup,
|
||||
listBackups,
|
||||
|
||||
getLogs: getLogs,
|
||||
getLocalLogfilePaths,
|
||||
getLogs,
|
||||
|
||||
start: start,
|
||||
stop: stop,
|
||||
restart: restart,
|
||||
start,
|
||||
stop,
|
||||
restart,
|
||||
|
||||
exec: exec,
|
||||
exec,
|
||||
|
||||
checkManifestConstraints: checkManifestConstraints,
|
||||
downloadManifest: downloadManifest,
|
||||
checkManifestConstraints,
|
||||
downloadManifest,
|
||||
|
||||
canAutoupdateApp: canAutoupdateApp,
|
||||
autoupdateApps: autoupdateApps,
|
||||
canAutoupdateApp,
|
||||
autoupdateApps,
|
||||
|
||||
restoreInstalledApps: restoreInstalledApps,
|
||||
configureInstalledApps: configureInstalledApps,
|
||||
schedulePendingTasks: schedulePendingTasks,
|
||||
restartAppsUsingAddons: restartAppsUsingAddons,
|
||||
restoreInstalledApps,
|
||||
configureInstalledApps,
|
||||
schedulePendingTasks,
|
||||
restartAppsUsingAddons,
|
||||
|
||||
getDataDir: getDataDir,
|
||||
getIconPath: getIconPath,
|
||||
getDataDir,
|
||||
getIconPath,
|
||||
|
||||
downloadFile: downloadFile,
|
||||
uploadFile: uploadFile,
|
||||
downloadFile,
|
||||
uploadFile,
|
||||
|
||||
PORT_TYPE_TCP: 'tcp',
|
||||
PORT_TYPE_UDP: 'udp',
|
||||
@@ -154,7 +155,6 @@ function validatePortBindings(portBindings, manifest) {
|
||||
const RESERVED_PORTS = [
|
||||
22, /* ssh */
|
||||
25, /* smtp */
|
||||
53, /* dns */
|
||||
80, /* http */
|
||||
143, /* imap */
|
||||
202, /* alternate ssh */
|
||||
@@ -167,7 +167,7 @@ function validatePortBindings(portBindings, manifest) {
|
||||
2004, /* graphite (lo) */
|
||||
2514, /* cloudron-syslog (lo) */
|
||||
constants.PORT, /* app server (lo) */
|
||||
constants.SYSADMIN_PORT, /* sysadmin app server (lo) */
|
||||
constants.AUTHWALL_PORT, /* protected sites */
|
||||
constants.INTERNAL_SMTP_PORT, /* internal smtp port (lo) */
|
||||
constants.LDAP_PORT,
|
||||
3306, /* mysql (lo) */
|
||||
@@ -191,7 +191,7 @@ function validatePortBindings(portBindings, manifest) {
|
||||
if (!Number.isInteger(hostPort)) return new BoxError(BoxError.BAD_FIELD, `${hostPort} is not an integer`, { field: 'portBindings', portName: portName });
|
||||
if (RESERVED_PORTS.indexOf(hostPort) !== -1) return new BoxError(BoxError.BAD_FIELD, `Port ${hostPort} is reserved.`, { field: 'portBindings', portName: portName });
|
||||
if (RESERVED_PORT_RANGES.find(range => (hostPort >= range[0] && hostPort <= range[1]))) return new BoxError(BoxError.BAD_FIELD, `Port ${hostPort} is reserved.`, { field: 'portBindings', portName: portName });
|
||||
if (hostPort <= 1023 || hostPort > 65535) return new BoxError(BoxError.BAD_FIELD, `${hostPort} is not in permitted range`, { field: 'portBindings', portName: portName });
|
||||
if (hostPort !== 53 && (hostPort <= 1023 || hostPort > 65535)) return new BoxError(BoxError.BAD_FIELD, `${hostPort} is not in permitted range`, { field: 'portBindings', portName: portName }); // dns 53 is special and adblocker apps can use them
|
||||
}
|
||||
|
||||
// it is OK if there is no 1-1 mapping between values in manifest.tcpPorts and portBindings. missing values implies
|
||||
@@ -334,20 +334,6 @@ function validateEnv(env) {
|
||||
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) {
|
||||
if (dataDir === null) return null;
|
||||
|
||||
@@ -398,7 +384,7 @@ function getDuplicateErrorDetails(errorMessage, locations, domainObjectMap, port
|
||||
|
||||
// check if any of the port bindings conflict
|
||||
for (let portName in portBindings) {
|
||||
if (portBindings[portName] === parseInt(match[1])) return new BoxError(BoxError.ALREADY_EXISTS, `Port ${match[1]} is reserved`, { portName });
|
||||
if (portBindings[portName] === parseInt(match[1])) return new BoxError(BoxError.ALREADY_EXISTS, `Port ${match[1]} is in use`, { portName });
|
||||
}
|
||||
|
||||
if (match[2] === 'dataDir') {
|
||||
@@ -420,7 +406,7 @@ function removeInternalFields(app) {
|
||||
'location', 'domain', 'fqdn', 'mailboxName', 'mailboxDomain',
|
||||
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'cpuShares',
|
||||
'sso', 'debugMode', 'reverseProxyConfig', 'enableBackup', 'creationTime', 'updateTime', 'ts', 'tags',
|
||||
'label', 'alternateDomains', 'env', 'enableAutomaticUpdate', 'dataDir', 'binds');
|
||||
'label', 'alternateDomains', 'env', 'enableAutomaticUpdate', 'dataDir', 'mounts');
|
||||
}
|
||||
|
||||
// non-admins can only see these
|
||||
@@ -512,25 +498,6 @@ function get(appId, callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
appdb.get(appId, function (error, app) {
|
||||
if (error && error.reason === BoxError.NOT_FOUND) return callback(new BoxError(BoxError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(error);
|
||||
|
||||
postProcess(app, domainObjectMap);
|
||||
|
||||
callback(null, app);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getByContainerId(containerId, callback) {
|
||||
assert.strictEqual(typeof containerId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getDomainObjectMap(function (error, domainObjectMap) {
|
||||
if (error) return callback(error);
|
||||
|
||||
appdb.getByContainerId(containerId, function (error, app) {
|
||||
if (error && error.reason === BoxError.NOT_FOUND) return callback(new BoxError(BoxError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(error);
|
||||
|
||||
postProcess(app, domainObjectMap);
|
||||
@@ -548,16 +515,15 @@ function getByIpAddress(ip, callback) {
|
||||
// this is only used by the ldap test. the apps tests still uses proper docker
|
||||
if (constants.TEST && exports._MOCK_GET_BY_IP_APP_ID) return get(exports._MOCK_GET_BY_IP_APP_ID, callback);
|
||||
|
||||
docker.getContainerIdByIp(ip, function (error, containerId) {
|
||||
appdb.getByIpAddress(ip, function (error, app) {
|
||||
if (error) return callback(error);
|
||||
|
||||
docker.inspect(containerId, function (error, result) {
|
||||
getDomainObjectMap(function (error, domainObjectMap) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const appId = safe.query(result, 'Config.Labels.appId', null);
|
||||
if (!appId) return callback(new BoxError(BoxError.NOT_FOUND, 'No such app'));
|
||||
postProcess(app, domainObjectMap);
|
||||
|
||||
get(appId, callback);
|
||||
callback(null, app);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -779,7 +745,7 @@ function install(data, auditSource, callback) {
|
||||
|
||||
if ('sso' in data && !('optionalSso' in manifest)) return callback(new BoxError(BoxError.BAD_FIELD, 'sso can only be specified for apps with optionalSso'));
|
||||
// if sso was unspecified, enable it by default if possible
|
||||
if (sso === null) sso = !!manifest.addons['ldap'] || !!manifest.addons['oauth'];
|
||||
if (sso === null) sso = !!manifest.addons['ldap'] || !!manifest.addons['proxyAuth'];
|
||||
|
||||
error = validateEnv(env);
|
||||
if (error) return callback(error);
|
||||
@@ -993,9 +959,9 @@ function setCpuShares(app, cpuShares, auditSource, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function setBinds(app, binds, auditSource, callback) {
|
||||
function setMounts(app, mounts, auditSource, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert(binds && typeof binds === 'object');
|
||||
assert(Array.isArray(mounts));
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -1003,17 +969,15 @@ function setBinds(app, binds, auditSource, callback) {
|
||||
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 }
|
||||
values: { mounts }
|
||||
};
|
||||
addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task, function (error, result) {
|
||||
if (error && error.reason === BoxError.ALREADY_EXISTS) return callback(new BoxError(BoxError.CONFLICT, 'Duplicate mount points'));
|
||||
if (error) return callback(error);
|
||||
|
||||
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, binds, taskId: result.taskId });
|
||||
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, mounts, taskId: result.taskId });
|
||||
|
||||
callback(null, { taskId: result.taskId });
|
||||
});
|
||||
@@ -1365,6 +1329,19 @@ function update(app, data, auditSource, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getLocalLogfilePaths(app) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
|
||||
const appId = app.id;
|
||||
|
||||
var filePaths = [];
|
||||
filePaths.push(path.join(paths.LOG_DIR, appId, 'apptask.log'));
|
||||
filePaths.push(path.join(paths.LOG_DIR, appId, 'app.log'));
|
||||
if (app.manifest.addons && app.manifest.addons.redis) filePaths.push(path.join(paths.LOG_DIR, `redis-${appId}/app.log`));
|
||||
|
||||
return filePaths;
|
||||
}
|
||||
|
||||
function getLogs(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert(options && typeof options === 'object');
|
||||
@@ -1384,11 +1361,8 @@ function getLogs(app, options, callback) {
|
||||
|
||||
var args = [ '--lines=' + lines ];
|
||||
if (follow) args.push('--follow', '--retry', '--quiet'); // same as -F. to make it work if file doesn't exist, --quiet to not output file headers, which are no logs
|
||||
args.push(path.join(paths.LOG_DIR, appId, 'apptask.log'));
|
||||
args.push(path.join(paths.LOG_DIR, appId, 'app.log'));
|
||||
if (app.manifest.addons && app.manifest.addons.redis) args.push(path.join(paths.LOG_DIR, `redis-${appId}/app.log`));
|
||||
|
||||
var cp = spawn('/usr/bin/tail', args);
|
||||
var cp = spawn('/usr/bin/tail', args.concat(getLocalLogfilePaths(app)));
|
||||
|
||||
var transformStream = split(function mapper(line) {
|
||||
if (format !== 'json') return line + '\n';
|
||||
@@ -1576,6 +1550,26 @@ function importApp(app, data, auditSource, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function exportApp(app, data, auditSource, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const appId = app.id;
|
||||
|
||||
let error = checkAppState(app, exports.ISTATE_PENDING_BACKUP);
|
||||
if (error) return callback(error);
|
||||
|
||||
const task = {
|
||||
args: { snapshotOnly: true },
|
||||
values: {}
|
||||
};
|
||||
addTask(appId, exports.ISTATE_PENDING_BACKUP, task, (error, result) => {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, { taskId: result.taskId });
|
||||
});
|
||||
}
|
||||
|
||||
function purchaseApp(data, callback) {
|
||||
assert.strictEqual(typeof data, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -2022,11 +2016,17 @@ function restartAppsUsingAddons(changedAddons, callback) {
|
||||
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
|
||||
// stop apps before updating the databases because postgres will "lock" them preventing import
|
||||
docker.stopContainers(app.id, function (error) {
|
||||
if (error) debug(`restartAppsUsingAddons: error stopping ${app.fqdn}`, error);
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
+54
-133
@@ -19,8 +19,6 @@ exports = module.exports = {
|
||||
getSubscription: getSubscription,
|
||||
isFreePlan: isFreePlan,
|
||||
|
||||
sendAliveStatus: sendAliveStatus,
|
||||
|
||||
getAppUpdate: getAppUpdate,
|
||||
getBoxUpdate: getBoxUpdate,
|
||||
|
||||
@@ -33,21 +31,16 @@ var apps = require('./apps.js'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
constants = require('./constants.js'),
|
||||
debug = require('debug')('box:appstore'),
|
||||
domains = require('./domains.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
groups = require('./groups.js'),
|
||||
mail = require('./mail.js'),
|
||||
os = require('os'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
settings = require('./settings.js'),
|
||||
superagent = require('superagent'),
|
||||
users = require('./users.js'),
|
||||
support = require('./support.js'),
|
||||
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
|
||||
// Keep in sync with appstore/routes/cloudrons.js
|
||||
let gFeatures = {
|
||||
@@ -56,7 +49,10 @@ let gFeatures = {
|
||||
externalLdap: false,
|
||||
privateDockerRegistry: false,
|
||||
branding: false,
|
||||
support: false
|
||||
support: false,
|
||||
directoryConfig: false,
|
||||
mailboxMaxCount: 5,
|
||||
emailPremium: false
|
||||
};
|
||||
|
||||
// attempt to load feature cache in case appstore would be down
|
||||
@@ -232,110 +228,6 @@ function unpurchaseApp(appId, data, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function sendAliveStatus(callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
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(),
|
||||
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(options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -351,7 +243,7 @@ function getBoxUpdate(options, callback) {
|
||||
automatic: options.automatic
|
||||
};
|
||||
|
||||
superagent.get(url).query(query).timeout(10 * 1000).end(function (error, result) {
|
||||
superagent.get(url).query(query).timeout(30 * 1000).end(function (error, result) {
|
||||
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 === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
|
||||
@@ -394,7 +286,7 @@ function getAppUpdate(app, options, callback) {
|
||||
automatic: options.automatic
|
||||
};
|
||||
|
||||
superagent.get(url).query(query).timeout(10 * 1000).end(function (error, result) {
|
||||
superagent.get(url).query(query).timeout(30 * 1000).end(function (error, result) {
|
||||
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 === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
|
||||
@@ -459,8 +351,8 @@ function trackBeginSetup() {
|
||||
const url = `${settings.apiServerOrigin()}/api/v1/helper/setup_begin`;
|
||||
|
||||
superagent.post(url).send({}).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return console.error(error.message);
|
||||
if (result.statusCode !== 200) return console.error(error.message);
|
||||
if (error && !error.response) return debug(`trackBeginSetup: ${error.message}`);
|
||||
if (result.statusCode !== 200) return debug(`trackBeginSetup: ${result.statusCode} ${error.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -471,8 +363,8 @@ function trackFinishedSetup(domain) {
|
||||
const url = `${settings.apiServerOrigin()}/api/v1/helper/setup_finished`;
|
||||
|
||||
superagent.post(url).send({ domain }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return console.error(error.message);
|
||||
if (result.statusCode !== 200) return console.error(error.message);
|
||||
if (error && !error.response) return debug(`trackFinishedSetup: ${error.message}`);
|
||||
if (result.statusCode !== 200) return debug(`trackFinishedSetup: ${result.statusCode} ${error.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -516,26 +408,55 @@ function createTicket(info, auditSource, callback) {
|
||||
apps.get(info.appId, callback);
|
||||
}
|
||||
|
||||
function enableSshIfNeeded(callback) {
|
||||
if (!info.enableSshSupport) return callback();
|
||||
|
||||
support.enableRemoteSupport(true, auditSource, function (error) {
|
||||
// ensure we can at least get the ticket through
|
||||
if (error) debug('Unable to enable SSH support.', error);
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
getCloudronToken(function (error, token) {
|
||||
if (error) return callback(error);
|
||||
|
||||
collectAppInfoIfNeeded(function (error, result) {
|
||||
enableSshIfNeeded(function (error) {
|
||||
if (error) return callback(error);
|
||||
if (result) info.app = result;
|
||||
|
||||
let url = settings.apiServerOrigin() + '/api/v1/ticket';
|
||||
collectAppInfoIfNeeded(function (error, app) {
|
||||
if (error) return callback(error);
|
||||
if (app) info.app = app;
|
||||
|
||||
info.supportEmail = constants.SUPPORT_EMAIL; // destination address for tickets
|
||||
info.supportEmail = constants.SUPPORT_EMAIL; // destination address for tickets
|
||||
|
||||
superagent.post(url).query({ accessToken: token }).send(info).timeout(10 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||
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('Bad response: %s %s', result.statusCode, result.text)));
|
||||
var req = superagent.post(`${settings.apiServerOrigin()}/api/v1/ticket`)
|
||||
.query({ accessToken: token })
|
||||
.timeout(30 * 1000);
|
||||
|
||||
eventlog.add(eventlog.ACTION_SUPPORT_TICKET, auditSource, info);
|
||||
// either send as JSON through body or as multipart, depending on attachments
|
||||
if (info.app) {
|
||||
req.field('infoJSON', JSON.stringify(info));
|
||||
|
||||
callback(null, { message: `An email for sent to ${constants.SUPPORT_EMAIL}. We will get back shortly!` });
|
||||
apps.getLocalLogfilePaths(info.app).forEach(function (filePath) {
|
||||
var logs = safe.child_process.execSync(`tail --lines=1000 ${filePath}`);
|
||||
if (logs) req.attach(path.basename(filePath), logs, path.basename(filePath));
|
||||
});
|
||||
} else {
|
||||
req.send(info);
|
||||
}
|
||||
|
||||
req.end(function (error, result) {
|
||||
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 === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
|
||||
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
|
||||
|
||||
eventlog.add(eventlog.ACTION_SUPPORT_TICKET, auditSource, info);
|
||||
|
||||
callback(null, { message: `An email was sent to ${constants.SUPPORT_EMAIL}. We will get back shortly!` });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -551,7 +472,7 @@ function getApps(callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const url = `${settings.apiServerOrigin()}/api/v1/apps`;
|
||||
superagent.get(url).query({ accessToken: token, boxVersion: constants.VERSION, unstable: unstable }).timeout(10 * 1000).end(function (error, result) {
|
||||
superagent.get(url).query({ accessToken: token, boxVersion: constants.VERSION, unstable: unstable }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
|
||||
@@ -586,7 +507,7 @@ function getAppVersion(appId, version, callback) {
|
||||
let url = `${settings.apiServerOrigin()}/api/v1/apps/${appId}`;
|
||||
if (version !== 'latest') url += `/versions/${version}`;
|
||||
|
||||
superagent.get(url).query({ accessToken: token }).timeout(10 * 1000).end(function (error, result) {
|
||||
superagent.get(url).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||
if (result.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||
|
||||
+49
-36
@@ -6,7 +6,6 @@ exports = module.exports = {
|
||||
run: run,
|
||||
|
||||
// exported for testing
|
||||
_reserveHttpPort: reserveHttpPort,
|
||||
_configureReverseProxy: configureReverseProxy,
|
||||
_unconfigureReverseProxy: unconfigureReverseProxy,
|
||||
_createAppDir: createAppDir,
|
||||
@@ -17,8 +16,6 @@ exports = module.exports = {
|
||||
_waitForDnsPropagation: waitForDnsPropagation
|
||||
};
|
||||
|
||||
require('supererror')({ splatchError: true });
|
||||
|
||||
var addons = require('./addons.js'),
|
||||
appdb = require('./appdb.js'),
|
||||
apps = require('./apps.js'),
|
||||
@@ -36,8 +33,8 @@ var addons = require('./addons.js'),
|
||||
ejs = require('ejs'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
fs = require('fs'),
|
||||
iputils = require('./iputils.js'),
|
||||
manifestFormat = require('cloudron-manifestformat'),
|
||||
net = require('net'),
|
||||
os = require('os'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
@@ -91,22 +88,16 @@ function updateApp(app, values, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function reserveHttpPort(app, callback) {
|
||||
function allocateContainerIp(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
let server = net.createServer();
|
||||
server.listen(0, function () {
|
||||
let port = server.address().port;
|
||||
|
||||
updateApp(app, { httpPort: port }, function (error) {
|
||||
server.close(function (/* closeError */) {
|
||||
if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Failed to allocate http port ${port}: ${error.message}`));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
async.retry({ times: 10 }, function (retryCallback) {
|
||||
const iprange = iputils.intFromIp('172.18.20.255') - iputils.intFromIp('172.18.16.1');
|
||||
let rnd = Math.floor(Math.random() * iprange);
|
||||
const containerIp = iputils.ipFromInt(iputils.intFromIp('172.18.16.1') + rnd);
|
||||
updateApp(app, { containerIp }, retryCallback);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function configureReverseProxy(app, callback) {
|
||||
@@ -352,7 +343,7 @@ function registerSubdomains(app, overwrite, callback) {
|
||||
|
||||
const allDomains = [ { subdomain: app.location, domain: app.domain }].concat(app.alternateDomains);
|
||||
|
||||
debug(`registerSubdomain: Will register ${JSON.stringify(allDomains)}`);
|
||||
debugApp(app, `registerSubdomain: Will register ${JSON.stringify(allDomains)}`);
|
||||
|
||||
async.eachSeries(allDomains, function (domain, iteratorDone) {
|
||||
async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
|
||||
@@ -372,7 +363,7 @@ function registerSubdomains(app, overwrite, callback) {
|
||||
|
||||
domains.upsertDnsRecords(domain.subdomain, domain.domain, 'A', [ ip ], function (error) {
|
||||
if (error && (error.reason === BoxError.BUSY || error.reason === BoxError.EXTERNAL_ERROR)) {
|
||||
debug('registerSubdomains: Upsert error. Will retry.', error.message);
|
||||
debugApp(app, 'registerSubdomains: Upsert error. Will retry.', error.message);
|
||||
return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain })); // try again
|
||||
}
|
||||
|
||||
@@ -403,7 +394,7 @@ function unregisterSubdomains(app, allDomains, callback) {
|
||||
domains.removeDnsRecords(domain.subdomain, domain.domain, 'A', [ ip ], function (error) {
|
||||
if (error && error.reason === BoxError.NOT_FOUND) return retryCallback(null, null);
|
||||
if (error && (error.reason === BoxError.SBUSY || error.reason === BoxError.EXTERNAL_ERROR)) {
|
||||
debug('registerSubdomains: Remove error. Will retry.', error.message);
|
||||
debugApp(app, 'registerSubdomains: Remove error. Will retry.', error.message);
|
||||
return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain })); // try again
|
||||
}
|
||||
|
||||
@@ -453,7 +444,7 @@ function moveDataDir(app, targetDir, callback) {
|
||||
let resolvedSourceDir = apps.getDataDir(app, app.dataDir);
|
||||
let resolvedTargetDir = apps.getDataDir(app, targetDir);
|
||||
|
||||
debug(`moveDataDir: migrating data from ${resolvedSourceDir} to ${resolvedTargetDir}`);
|
||||
debugApp(app, `moveDataDir: migrating data from ${resolvedSourceDir} to ${resolvedTargetDir}`);
|
||||
|
||||
if (resolvedSourceDir === resolvedTargetDir) return callback();
|
||||
|
||||
@@ -486,6 +477,8 @@ function downloadImage(manifest, callback) {
|
||||
}
|
||||
|
||||
function startApp(app, callback){
|
||||
debugApp(app, 'startApp: starting container');
|
||||
|
||||
if (app.runState === apps.RSTATE_STOPPED) return callback();
|
||||
|
||||
docker.startContainer(app.id, callback);
|
||||
@@ -534,7 +527,8 @@ function install(app, args, progressCallback, callback) {
|
||||
docker.deleteImage(oldManifest, done);
|
||||
},
|
||||
|
||||
reserveHttpPort.bind(null, app),
|
||||
// allocating container ip here, lets the users "repair" an app if allocation fails at appdb.add time
|
||||
allocateContainerIp.bind(null, app),
|
||||
|
||||
progressCallback.bind(null, { percent: 20, message: 'Downloading icon' }),
|
||||
downloadIcon.bind(null, app),
|
||||
@@ -604,7 +598,7 @@ function backup(app, args, progressCallback, callback) {
|
||||
|
||||
async.series([
|
||||
progressCallback.bind(null, { percent: 10, message: 'Backing up' }),
|
||||
backups.backupApp.bind(null, app, { /* options */ }, (progress) => {
|
||||
backups.backupApp.bind(null, app, { snapshotOnly: !!args.snapshotOnly }, (progress) => {
|
||||
progressCallback({ percent: 30, message: progress.message });
|
||||
}),
|
||||
|
||||
@@ -740,7 +734,8 @@ function migrateDataDir(app, args, progressCallback, callback) {
|
||||
debugApp(app, 'error migrating data dir : %s', error);
|
||||
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
|
||||
}
|
||||
callback(null);
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -755,7 +750,6 @@ function configure(app, args, progressCallback, callback) {
|
||||
progressCallback.bind(null, { percent: 10, message: 'Cleaning up old install' }),
|
||||
unconfigureReverseProxy.bind(null, app),
|
||||
deleteContainers.bind(null, app, { managedOnly: true }),
|
||||
reserveHttpPort.bind(null, app),
|
||||
|
||||
progressCallback.bind(null, { percent: 20, message: 'Downloading icon' }),
|
||||
downloadIcon.bind(null, app),
|
||||
@@ -785,11 +779,11 @@ function configure(app, args, progressCallback, callback) {
|
||||
debugApp(app, 'error reconfiguring : %s', error);
|
||||
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
|
||||
}
|
||||
callback(null);
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
// nginx configuration is skipped because app.httpPort is expected to be available
|
||||
function update(app, args, progressCallback, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof args, 'object');
|
||||
@@ -801,7 +795,8 @@ function update(app, args, progressCallback, callback) {
|
||||
|
||||
// app does not want these addons anymore
|
||||
// FIXME: this does not handle option changes (like multipleDatabases)
|
||||
var unusedAddons = _.omit(app.manifest.addons, Object.keys(updateConfig.manifest.addons));
|
||||
const unusedAddons = _.omit(app.manifest.addons, Object.keys(updateConfig.manifest.addons));
|
||||
const httpPathsChanged = app.manifest.httpPaths !== updateConfig.manifest.httpPaths;
|
||||
|
||||
async.series([
|
||||
// this protects against the theoretical possibility of an app being marked for update from
|
||||
@@ -852,7 +847,7 @@ function update(app, args, progressCallback, callback) {
|
||||
if (newTcpPorts[portName] || newUdpPorts[portName]) return callback(null); // port still in use
|
||||
|
||||
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) debugApp(app, 'update: portbinding does not exist in database', error);
|
||||
else if (error) return next(error);
|
||||
|
||||
// also delete from app object for further processing (the db is updated in the next step)
|
||||
@@ -868,14 +863,22 @@ function update(app, args, progressCallback, callback) {
|
||||
progressCallback.bind(null, { percent: 45, message: 'Downloading icon' }),
|
||||
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),
|
||||
|
||||
progressCallback.bind(null, { percent: 80, message: 'Creating container' }),
|
||||
progressCallback.bind(null, { percent: 70, message: 'Creating container' }),
|
||||
createContainer.bind(null, app),
|
||||
|
||||
startApp.bind(null, app),
|
||||
|
||||
// needed for httpPaths changes
|
||||
progressCallback.bind(null, { percent: 90, message: 'Configuring reverse proxy' }),
|
||||
function (next) {
|
||||
if (!httpPathsChanged) return next();
|
||||
|
||||
configureReverseProxy(app, next);
|
||||
},
|
||||
|
||||
progressCallback.bind(null, { percent: 100, message: 'Done' }),
|
||||
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null, updateTime: new Date() })
|
||||
], function seriesDone(error) {
|
||||
@@ -904,8 +907,11 @@ function start(app, args, progressCallback, callback) {
|
||||
progressCallback.bind(null, { percent: 35, message: 'Starting container' }),
|
||||
docker.startContainer.bind(null, app.id),
|
||||
|
||||
progressCallback.bind(null, { percent: 60, message: 'Adding collectd profile' }),
|
||||
addCollectdProfile.bind(null, app),
|
||||
|
||||
// stopped apps do not renew certs. currently, we don't do DNS to not overwrite existing user settings
|
||||
progressCallback.bind(null, { percent: 60, message: 'Configuring reverse proxy' }),
|
||||
progressCallback.bind(null, { percent: 80, message: 'Configuring reverse proxy' }),
|
||||
configureReverseProxy.bind(null, app),
|
||||
|
||||
progressCallback.bind(null, { percent: 100, message: 'Done' }),
|
||||
@@ -932,6 +938,9 @@ function stop(app, args, progressCallback, callback) {
|
||||
progressCallback.bind(null, { percent: 50, message: 'Stopping app services' }),
|
||||
addons.stopAppServices.bind(null, app),
|
||||
|
||||
progressCallback.bind(null, { percent: 80, message: 'Removing collectd profile' }),
|
||||
removeCollectdProfile.bind(null, app),
|
||||
|
||||
progressCallback.bind(null, { percent: 100, message: 'Done' }),
|
||||
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
|
||||
], function seriesDone(error) {
|
||||
@@ -978,16 +987,18 @@ function uninstall(app, args, progressCallback, callback) {
|
||||
progressCallback.bind(null, { percent: 30, message: 'Teardown 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' }),
|
||||
|
||||
progressCallback.bind(null, { percent: 50, message: 'Deleting app data directory' }),
|
||||
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),
|
||||
|
||||
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)),
|
||||
|
||||
progressCallback.bind(null, { percent: 70, message: 'Cleanup icon' }),
|
||||
progressCallback.bind(null, { percent: 80, message: 'Cleanup icon' }),
|
||||
removeIcon.bind(null, app),
|
||||
|
||||
progressCallback.bind(null, { percent: 90, message: 'Cleanup logs' }),
|
||||
@@ -1043,6 +1054,8 @@ function run(appId, args, progressCallback, callback) {
|
||||
return stop(app, args, progressCallback, callback);
|
||||
case apps.ISTATE_PENDING_RESTART:
|
||||
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:
|
||||
debugApp(app, 'apptask launched with invalid command');
|
||||
return callback(new BoxError(BoxError.INTERNAL_ERROR, 'Unknown install command in apptask:' + app.installationState));
|
||||
|
||||
+11
-2
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
scheduleTask: scheduleTask
|
||||
scheduleTask
|
||||
};
|
||||
|
||||
let assert = require('assert'),
|
||||
@@ -12,6 +12,8 @@ let assert = require('assert'),
|
||||
safe = require('safetydance'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
scheduler = require('./scheduler.js'),
|
||||
sftp = require('./sftp.js'),
|
||||
tasks = require('./tasks.js');
|
||||
|
||||
let gActiveTasks = { }; // indexed by app id
|
||||
@@ -68,11 +70,18 @@ function scheduleTask(appId, taskId, callback) {
|
||||
|
||||
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) {
|
||||
scheduler.suspendJobs(appId);
|
||||
|
||||
// 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);
|
||||
|
||||
delete gActiveTasks[appId];
|
||||
locker.unlock(locker.OP_APPTASK); // unlock event will trigger next task
|
||||
|
||||
// post app task hooks
|
||||
sftp.rebuild(error => { if (error) debug('Unable to rebuild sftp:', error); });
|
||||
scheduler.resumeJobs(appId);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+77
-68
@@ -9,7 +9,6 @@ exports = module.exports = {
|
||||
get: get,
|
||||
|
||||
startBackupTask: startBackupTask,
|
||||
ensureBackup: ensureBackup,
|
||||
|
||||
restore: restore,
|
||||
|
||||
@@ -57,6 +56,7 @@ var addons = require('./addons.js'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
collectd = require('./collectd.js'),
|
||||
constants = require('./constants.js'),
|
||||
CronJob = require('cron').CronJob,
|
||||
crypto = require('crypto'),
|
||||
database = require('./database.js'),
|
||||
DataLayout = require('./datalayout.js'),
|
||||
@@ -105,6 +105,7 @@ function api(provider) {
|
||||
case 'exoscale-sos': return require('./storage/s3.js');
|
||||
case 'wasabi': return require('./storage/s3.js');
|
||||
case 'scaleway-objectstorage': return require('./storage/s3.js');
|
||||
case 'backblaze-b2': return require('./storage/s3.js');
|
||||
case 'linode-objectstorage': return require('./storage/s3.js');
|
||||
case 'ovh-objectstorage': return require('./storage/s3.js');
|
||||
case 'noop': return require('./storage/noop.js');
|
||||
@@ -142,8 +143,8 @@ function testConfig(backupConfig, callback) {
|
||||
|
||||
if (backupConfig.format !== 'tgz' && backupConfig.format !== 'rsync') return callback(new BoxError(BoxError.BAD_FIELD, 'unknown format', { field: 'format' }));
|
||||
|
||||
// remember to adjust the cron ensureBackup task interval accordingly
|
||||
if (backupConfig.intervalSecs < 6 * 60 * 60) return callback(new BoxError(BoxError.BAD_FIELD, 'Interval must be atleast 6 hours', { field: 'intervalSecs' }));
|
||||
const job = safe.safeCall(function () { return new CronJob(backupConfig.schedulePattern); });
|
||||
if (!job) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid schedule pattern', { field: 'schedulePattern' }));
|
||||
|
||||
if ('password' in backupConfig) {
|
||||
if (typeof backupConfig.password !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'password must be a string', { field: 'password' }));
|
||||
@@ -379,9 +380,11 @@ function createReadStream(sourceFile, encryption) {
|
||||
|
||||
stream.on('error', function (error) {
|
||||
debug(`createReadStream: read stream error at ${sourceFile}`, error);
|
||||
ps.emit('error', new BoxError(BoxError.FS_ERROR, `Error reading ${sourceFile}: ${error.message}`));
|
||||
ps.emit('error', new BoxError(BoxError.FS_ERROR, `Error reading ${sourceFile}: ${error.message} ${error.code}`));
|
||||
});
|
||||
|
||||
stream.on('open', () => ps.emit('open'));
|
||||
|
||||
if (encryption) {
|
||||
let encryptStream = new EncryptStream(encryption);
|
||||
|
||||
@@ -514,18 +517,19 @@ function sync(backupConfig, backupId, dataLayout, progressCallback, callback) {
|
||||
progressCallback({ message: `Adding ${task.path}` + (retryCount > 1 ? ` (Try ${retryCount})` : '') });
|
||||
debug(`Adding ${task.path} position ${task.position} try ${retryCount}`);
|
||||
var stream = createReadStream(dataLayout.toLocalPath('./' + task.path), backupConfig.encryption);
|
||||
stream.on('error', function (error) {
|
||||
debug(`read stream error for ${task.path}: ${error.message}`);
|
||||
retryCallback();
|
||||
}); // ignore error if file disappears
|
||||
stream.on('error', (error) => retryCallback(error.message.includes('ENOENT') ? null : error)); // ignore error if file disappears
|
||||
stream.on('progress', function (progress) {
|
||||
const transferred = Math.round(progress.transferred/1024/1024), speed = Math.round(progress.speed/1024/1024);
|
||||
if (!transferred && !speed) return progressCallback({ message: `Uploading ${task.path}` }); // 0M@0MBps looks wrong
|
||||
progressCallback({ message: `Uploading ${task.path}: ${transferred}M@${speed}MBps` }); // 0M@0MBps looks wrong
|
||||
});
|
||||
api(backupConfig.provider).upload(backupConfig, backupFilePath, stream, function (error) {
|
||||
debug(error ? `Error uploading ${task.path} try ${retryCount}: ${error.message}` : `Uploaded ${task.path}`);
|
||||
retryCallback(error);
|
||||
// only create the destination path when we have confirmation that the source is available. otherwise, we end up with
|
||||
// files owned as 'root' and the cp later will fail
|
||||
stream.on('open', function () {
|
||||
api(backupConfig.provider).upload(backupConfig, backupFilePath, stream, function (error) {
|
||||
debug(error ? `Error uploading ${task.path} try ${retryCount}: ${error.message}` : `Uploaded ${task.path}`);
|
||||
retryCallback(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
}, iteratorCallback);
|
||||
@@ -847,15 +851,23 @@ function runBackupUpload(uploadConfig, progressCallback, callback) {
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const { backupId, format, dataLayout, progressTag } = uploadConfig;
|
||||
const { backupId, backupConfig, dataLayout, progressTag } = uploadConfig;
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
assert.strictEqual(typeof format, 'string');
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof progressTag, 'string');
|
||||
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
|
||||
|
||||
let result = ''; // the script communicates error result as a string
|
||||
|
||||
shell.sudo(`backup-${backupId}`, [ BACKUP_UPLOAD_CMD, backupId, format, dataLayout.toString() ], { preserveEnv: true, ipc: true }, function (error) {
|
||||
// https://stackoverflow.com/questions/48387040/node-js-recommended-max-old-space-size
|
||||
const envCopy = Object.assign({}, process.env);
|
||||
if (backupConfig.memoryLimit && backupConfig.memoryLimit >= 2*1024*1024*1024) {
|
||||
const heapSize = Math.min((backupConfig.memoryLimit/1024/1024) - 256, 8192);
|
||||
debug(`runBackupUpload: adjusting heap size to ${heapSize}M`);
|
||||
envCopy.NODE_OPTIONS = `--max-old-space-size=${heapSize}`;
|
||||
}
|
||||
|
||||
shell.sudo(`backup-${backupId}`, [ BACKUP_UPLOAD_CMD, backupId, backupConfig.format, dataLayout.toString() ], { env: envCopy, preserveEnv: true, ipc: true }, function (error) {
|
||||
if (error && (error.code === null /* signal */ || (error.code !== 0 && error.code !== 50))) { // backuptask crashed
|
||||
return callback(new BoxError(BoxError.INTERNAL_ERROR, 'Backuptask crashed'));
|
||||
} else if (error && error.code === 50) { // exited with error
|
||||
@@ -900,9 +912,13 @@ function snapshotBox(progressCallback, callback) {
|
||||
|
||||
progressCallback({ message: 'Snapshotting box' });
|
||||
|
||||
const startTime = new Date();
|
||||
|
||||
database.exportToFile(`${paths.BOX_DATA_DIR}/box.mysqldump`, function (error) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
debug(`snapshotBox: took ${(new Date() - startTime)/1000} seconds`);
|
||||
|
||||
return callback();
|
||||
});
|
||||
}
|
||||
@@ -912,8 +928,6 @@ function uploadBoxSnapshot(backupConfig, progressCallback, callback) {
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var startTime = new Date();
|
||||
|
||||
snapshotBox(progressCallback, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
@@ -922,26 +936,29 @@ function uploadBoxSnapshot(backupConfig, progressCallback, callback) {
|
||||
|
||||
const uploadConfig = {
|
||||
backupId: 'snapshot/box',
|
||||
format: backupConfig.format,
|
||||
backupConfig,
|
||||
dataLayout: new DataLayout(boxDataDir, []),
|
||||
progressTag: 'box'
|
||||
};
|
||||
|
||||
progressCallback({ message: 'Uploading box snapshot' });
|
||||
|
||||
const startTime = new Date();
|
||||
|
||||
runBackupUpload(uploadConfig, progressCallback, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('uploadBoxSnapshot: time: %s secs', (new Date() - startTime)/1000);
|
||||
debug(`uploadBoxSnapshot: took ${(new Date() - startTime)/1000} seconds`);
|
||||
|
||||
setSnapshotInfo('box', { timestamp: new Date().toISOString(), format: backupConfig.format }, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function rotateBoxBackup(backupConfig, tag, appBackupIds, progressCallback, callback) {
|
||||
function rotateBoxBackup(backupConfig, tag, options, appBackupIds, progressCallback, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof tag, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert(Array.isArray(appBackupIds));
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -972,7 +989,7 @@ function rotateBoxBackup(backupConfig, tag, appBackupIds, progressCallback, call
|
||||
copy.on('done', function (copyBackupError) {
|
||||
const state = copyBackupError ? exports.BACKUP_STATE_ERROR : exports.BACKUP_STATE_NORMAL;
|
||||
|
||||
backupdb.update(backupId, { state }, function (error) {
|
||||
backupdb.update(backupId, { preserveSecs: options.preserveSecs || 0, state }, function (error) {
|
||||
if (copyBackupError) return callback(copyBackupError);
|
||||
if (error) return callback(error);
|
||||
|
||||
@@ -984,9 +1001,10 @@ function rotateBoxBackup(backupConfig, tag, appBackupIds, progressCallback, call
|
||||
});
|
||||
}
|
||||
|
||||
function backupBoxWithAppBackupIds(appBackupIds, tag, progressCallback, callback) {
|
||||
function backupBoxWithAppBackupIds(appBackupIds, tag, options, progressCallback, callback) {
|
||||
assert(Array.isArray(appBackupIds));
|
||||
assert.strictEqual(typeof tag, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -996,7 +1014,7 @@ function backupBoxWithAppBackupIds(appBackupIds, tag, progressCallback, callback
|
||||
uploadBoxSnapshot(backupConfig, progressCallback, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
rotateBoxBackup(backupConfig, tag, appBackupIds, progressCallback, callback);
|
||||
rotateBoxBackup(backupConfig, tag, options, appBackupIds, progressCallback, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1020,6 +1038,7 @@ function snapshotApp(app, progressCallback, callback) {
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const startTime = new Date();
|
||||
progressCallback({ message: `Snapshotting app ${app.fqdn}` });
|
||||
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/config.json'), JSON.stringify(app))) {
|
||||
@@ -1029,6 +1048,8 @@ function snapshotApp(app, progressCallback, callback) {
|
||||
addons.backupAddons(app, app.manifest.addons, function (error) {
|
||||
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
|
||||
|
||||
debugApp(app, `snapshotApp: took ${(new Date() - startTime)/1000} seconds`);
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
@@ -1041,6 +1062,8 @@ function rotateAppBackup(backupConfig, app, tag, options, progressCallback, call
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const startTime = new Date();
|
||||
|
||||
var snapshotInfo = getSnapshotInfo(app.id);
|
||||
|
||||
var manifest = snapshotInfo.restoreConfig ? snapshotInfo.restoreConfig.manifest : snapshotInfo.manifest; // compat
|
||||
@@ -1073,7 +1096,7 @@ function rotateAppBackup(backupConfig, app, tag, options, progressCallback, call
|
||||
if (copyBackupError) return callback(copyBackupError);
|
||||
if (error) return callback(error);
|
||||
|
||||
debug(`Rotated app backup of ${app.id} successfully to id ${backupId}`);
|
||||
debug(`Rotated app backup of ${app.id} successfully to id ${backupId}. Took ${(new Date() - startTime)/1000} seconds`);
|
||||
|
||||
callback(null, backupId);
|
||||
});
|
||||
@@ -1087,8 +1110,6 @@ function uploadAppSnapshot(backupConfig, app, progressCallback, callback) {
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var startTime = new Date();
|
||||
|
||||
snapshotApp(app, progressCallback, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
@@ -1102,15 +1123,17 @@ function uploadAppSnapshot(backupConfig, app, progressCallback, callback) {
|
||||
|
||||
const uploadConfig = {
|
||||
backupId,
|
||||
format: backupConfig.format,
|
||||
backupConfig,
|
||||
dataLayout,
|
||||
progressTag: app.fqdn
|
||||
};
|
||||
|
||||
const startTime = new Date();
|
||||
|
||||
runBackupUpload(uploadConfig, progressCallback, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debugApp(app, 'uploadAppSnapshot: %s done time: %s secs', backupId, (new Date() - startTime)/1000);
|
||||
debugApp(app, `uploadAppSnapshot: ${backupId} done. ${(new Date() - startTime)/1000} seconds`);
|
||||
|
||||
setSnapshotInfo(app.id, { timestamp: new Date().toISOString(), manifest: app.manifest, format: backupConfig.format }, callback);
|
||||
});
|
||||
@@ -1152,6 +1175,8 @@ function backupApp(app, options, progressCallback, callback) {
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (options.snapshotOnly) return snapshotApp(app, progressCallback, callback);
|
||||
|
||||
const tag = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,'');
|
||||
|
||||
debug(`backupApp - Backing up ${app.fqdn} with tag ${tag}`);
|
||||
@@ -1160,7 +1185,8 @@ function backupApp(app, options, progressCallback, callback) {
|
||||
}
|
||||
|
||||
// this function expects you to have a lock. Unlike other progressCallback this also has a progress field
|
||||
function backupBoxAndApps(progressCallback, callback) {
|
||||
function backupBoxAndApps(options, progressCallback, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -1181,13 +1207,14 @@ function backupBoxAndApps(progressCallback, callback) {
|
||||
return iteratorCallback(null, null); // nothing to backup
|
||||
}
|
||||
|
||||
backupAppWithTag(app, tag, { /* options */ }, (progress) => progressCallback({ percent: percent, message: progress.message }), function (error, backupId) {
|
||||
const startTime = new Date();
|
||||
backupAppWithTag(app, tag, options, (progress) => progressCallback({ percent: percent, message: progress.message }), function (error, backupId) {
|
||||
if (error) {
|
||||
debugApp(app, 'Unable to backup', error);
|
||||
return iteratorCallback(error);
|
||||
}
|
||||
|
||||
debugApp(app, 'Backed up');
|
||||
debugApp(app, `Backed up. Took ${(new Date() - startTime)/1000} seconds`);
|
||||
|
||||
iteratorCallback(null, backupId || null); // clear backupId if is in BAD_STATE and never backed up
|
||||
});
|
||||
@@ -1199,7 +1226,7 @@ function backupBoxAndApps(progressCallback, callback) {
|
||||
progressCallback({ percent: percent, message: 'Backing up system data' });
|
||||
percent += step;
|
||||
|
||||
backupBoxWithAppBackupIds(backupIds, tag, (progress) => progressCallback({ percent: percent, message: progress.message }), callback);
|
||||
backupBoxWithAppBackupIds(backupIds, tag, options, (progress) => progressCallback({ percent: percent, message: progress.message }), callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1208,49 +1235,30 @@ function startBackupTask(auditSource, callback) {
|
||||
let error = locker.lock(locker.OP_FULL_BACKUP);
|
||||
if (error) return callback(new BoxError(BoxError.BAD_STATE, `Cannot backup now: ${error.message}`));
|
||||
|
||||
tasks.add(tasks.TASK_BACKUP, [ ], function (error, taskId) {
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
eventlog.add(eventlog.ACTION_BACKUP_START, auditSource, { taskId });
|
||||
const memoryLimit = 'memoryLimit' in backupConfig ? Math.max(backupConfig.memoryLimit/1024/1024, 400) : 400;
|
||||
|
||||
tasks.startTask(taskId, { timeout: 12 * 60 * 60 * 1000 /* 12 hours */ }, function (error, backupId) {
|
||||
locker.unlock(locker.OP_FULL_BACKUP);
|
||||
|
||||
const errorMessage = error ? error.message : '';
|
||||
const timedOut = error ? error.code === tasks.ETIMEOUT : false;
|
||||
|
||||
eventlog.add(eventlog.ACTION_BACKUP_FINISH, auditSource, { taskId, errorMessage, timedOut, backupId });
|
||||
});
|
||||
|
||||
callback(null, taskId);
|
||||
});
|
||||
}
|
||||
|
||||
function ensureBackup(auditSource, callback) {
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
|
||||
debug('ensureBackup: %j', auditSource);
|
||||
|
||||
getByIdentifierAndStatePaged(exports.BACKUP_IDENTIFIER_BOX, exports.BACKUP_STATE_NORMAL, 1, 1, function (error, backups) {
|
||||
if (error) {
|
||||
debug('Unable to list backups', error);
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
tasks.add(tasks.TASK_BACKUP, [ { /* options */ } ], function (error, taskId) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (backups.length !== 0 && (new Date() - new Date(backups[0].creationTime) < (backupConfig.intervalSecs - 3600) * 1000)) { // adjust 1 hour
|
||||
debug('Previous backup was %j, no need to backup now', backups[0]);
|
||||
return callback(null);
|
||||
}
|
||||
eventlog.add(eventlog.ACTION_BACKUP_START, auditSource, { taskId });
|
||||
|
||||
startBackupTask(auditSource, callback);
|
||||
tasks.startTask(taskId, { timeout: 24 * 60 * 60 * 1000 /* 24 hours */, nice: 15, memoryLimit }, function (error, backupId) {
|
||||
locker.unlock(locker.OP_FULL_BACKUP);
|
||||
|
||||
const errorMessage = error ? error.message : '';
|
||||
const timedOut = error ? error.code === tasks.ETIMEOUT : false;
|
||||
|
||||
eventlog.add(eventlog.ACTION_BACKUP_FINISH, auditSource, { taskId, errorMessage, timedOut, backupId });
|
||||
});
|
||||
|
||||
callback(null, taskId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// backups must be descending in creationTime
|
||||
function applyBackupRetentionPolicy(backups, policy, referencedBackupIds) {
|
||||
assert(Array.isArray(backups));
|
||||
assert.strictEqual(typeof policy, 'object');
|
||||
@@ -1288,12 +1296,13 @@ function applyBackupRetentionPolicy(backups, policy, referencedBackupIds) {
|
||||
|
||||
let lastPeriod = null, keptSoFar = 0;
|
||||
for (const backup of backups) {
|
||||
if (backup.discardReason || backup.keepReason) continue; // already kept or discarded for some reason
|
||||
if (backup.discardReason) continue; // already discarded for some reason
|
||||
if (backup.keepReason && backup.keepReason !== 'reference') continue; // kept for some other reason
|
||||
const period = moment(backup.creationTime).format(KEEP_FORMATS[format]);
|
||||
if (period === lastPeriod) continue; // already kept for this period
|
||||
|
||||
lastPeriod = period;
|
||||
backup.keepReason = format;
|
||||
backup.keepReason = backup.keepReason ? `${backup.keepReason}+${format}` : format;
|
||||
if (++keptSoFar === n) break;
|
||||
}
|
||||
}
|
||||
@@ -1527,9 +1536,9 @@ function checkConfiguration(callback) {
|
||||
|
||||
let message = '';
|
||||
if (backupConfig.provider === 'noop') {
|
||||
message = 'Cloudron backups are disabled. Please ensure this server is backed up using alternate means. See https://cloudron.io/documentation/backups/#storage-providers for more information.';
|
||||
message = 'Cloudron backups are disabled. Please ensure this server is backed up using alternate means. See https://docs.cloudron.io/backups/#storage-providers for more information.';
|
||||
} else if (backupConfig.provider === 'filesystem' && !backupConfig.externalDisk) {
|
||||
message = 'Cloudron backups are currently on the same disk as the Cloudron server instance. This is dangerous and can lead to complete data loss if the disk fails. See https://cloudron.io/documentation/backups/#storage-providers for storing backups in an external location.';
|
||||
message = 'Cloudron backups are currently on the same disk as the Cloudron server instance. This is dangerous and can lead to complete data loss if the disk fails. See https://docs.cloudron.io/backups/#storage-providers for storing backups in an external location.';
|
||||
}
|
||||
|
||||
callback(null, message);
|
||||
|
||||
@@ -50,6 +50,7 @@ BoxError.FS_ERROR = 'FileSystem Error';
|
||||
BoxError.INACTIVE = 'Inactive';
|
||||
BoxError.INTERNAL_ERROR = 'Internal Error';
|
||||
BoxError.INVALID_CREDENTIALS = 'Invalid Credentials';
|
||||
BoxError.IPTABLES_ERROR = 'IPTables Error';
|
||||
BoxError.LICENSE_ERROR = 'License Error';
|
||||
BoxError.LOGROTATE_ERROR = 'Logrotate Error';
|
||||
BoxError.MAIL_ERROR = 'Mail Error';
|
||||
@@ -92,6 +93,7 @@ BoxError.toHttpError = function (error) {
|
||||
case BoxError.MAIL_ERROR:
|
||||
case BoxError.DOCKER_ERROR:
|
||||
case BoxError.ADDONS_ERROR:
|
||||
case BoxError.IPTABLES_ERROR:
|
||||
return new HttpError(424, error);
|
||||
case BoxError.DATABASE_ERROR:
|
||||
case BoxError.INTERNAL_ERROR:
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
renderFooter
|
||||
};
|
||||
|
||||
const assert = require('assert'),
|
||||
constants = require('./constants.js');
|
||||
|
||||
function renderFooter(footer) {
|
||||
assert.strictEqual(typeof footer, 'string');
|
||||
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
return footer.replace(/%YEAR%/g, year)
|
||||
.replace(/%VERSION%/g, constants.VERSION);
|
||||
}
|
||||
|
||||
+2
-2
@@ -185,8 +185,8 @@ Acme2.prototype.newOrder = function (domain, callback) {
|
||||
|
||||
this.sendSignedRequest(this.directory.newOrder, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(error);
|
||||
if (result.statusCode === 403) return callback(new BoxError(BoxError.ACCESS_DENIED, `Forbidden sending signed request: ${result.body.detail}`));
|
||||
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
|
||||
if (result.statusCode === 403) return callback(new BoxError(BoxError.ACCESS_DENIED, `Forbidden sending new order: ${result.body.detail}`));
|
||||
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to send new order. Expecting 201, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
debug('newOrder: created order %s %j', domain, result.body);
|
||||
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
|
||||
+107
-63
@@ -1,24 +1,24 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
initialize: initialize,
|
||||
uninitialize: uninitialize,
|
||||
getConfig: getConfig,
|
||||
getLogs: getLogs,
|
||||
initialize,
|
||||
uninitialize,
|
||||
getConfig,
|
||||
getLogs,
|
||||
|
||||
reboot: reboot,
|
||||
isRebootRequired: isRebootRequired,
|
||||
reboot,
|
||||
isRebootRequired,
|
||||
|
||||
onActivated: onActivated,
|
||||
onActivated,
|
||||
|
||||
prepareDashboardDomain: prepareDashboardDomain,
|
||||
setDashboardDomain: setDashboardDomain,
|
||||
setDashboardAndMailDomain: setDashboardAndMailDomain,
|
||||
renewCerts: renewCerts,
|
||||
setupDnsAndCert,
|
||||
|
||||
setupDashboard: setupDashboard,
|
||||
prepareDashboardDomain,
|
||||
setDashboardDomain,
|
||||
updateDashboardDomain,
|
||||
renewCerts,
|
||||
|
||||
runSystemChecks: runSystemChecks,
|
||||
runSystemChecks
|
||||
};
|
||||
|
||||
var addons = require('./addons.js'),
|
||||
@@ -29,6 +29,7 @@ var addons = require('./addons.js'),
|
||||
auditSource = require('./auditsource.js'),
|
||||
backups = require('./backups.js'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
branding = require('./branding.js'),
|
||||
constants = require('./constants.js'),
|
||||
cron = require('./cron.js'),
|
||||
debug = require('debug')('box:cloudron'),
|
||||
@@ -46,6 +47,7 @@ var addons = require('./addons.js'),
|
||||
shell = require('./shell.js'),
|
||||
spawn = require('child_process').spawn,
|
||||
split = require('split'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
tasks = require('./tasks.js'),
|
||||
users = require('./users.js');
|
||||
|
||||
@@ -66,7 +68,7 @@ function uninitialize(callback) {
|
||||
|
||||
async.series([
|
||||
cron.stopJobs,
|
||||
platform.stop
|
||||
platform.stopAllTasks
|
||||
], callback);
|
||||
}
|
||||
|
||||
@@ -78,7 +80,16 @@ function onActivated(callback) {
|
||||
// 2. the restore code path can run without sudo (since mail/ is non-root)
|
||||
async.series([
|
||||
platform.start,
|
||||
cron.startJobs
|
||||
cron.startJobs,
|
||||
function checkBackupConfiguration(done) {
|
||||
backups.checkConfiguration(function (error, message) {
|
||||
if (error) return done(error);
|
||||
notifications.alert(notifications.ALERT_BACKUP_CONFIG, 'Backup configuration is unsafe', message, done);
|
||||
});
|
||||
},
|
||||
// disable responding to api calls via IP to not leak domain info. this is carefully placed as the last item, so it buys
|
||||
// the UI some time to query the dashboard domain in the restore code path
|
||||
(done) => setTimeout(() => reverseProxy.writeDefaultConfig({ activated :true }, done), 30000)
|
||||
], callback);
|
||||
}
|
||||
|
||||
@@ -103,24 +114,49 @@ function notifyUpdate(callback) {
|
||||
|
||||
// each of these tasks can fail. we will add some routes to fix/re-run them
|
||||
function runStartupTasks() {
|
||||
// configure nginx to be reachable by IP
|
||||
reverseProxy.writeDefaultConfig(NOOP_CALLBACK);
|
||||
const tasks = [
|
||||
// stop all the systemd tasks
|
||||
platform.stopAllTasks,
|
||||
|
||||
// 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) {
|
||||
if (error) return console.error('Failed to read backup config.', error);
|
||||
backups.configureCollectd(backupConfig, 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
|
||||
function (callback) {
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
// always generate webadmin config since we have no versioning mechanism for the ejs
|
||||
if (settings.adminDomain()) reverseProxy.writeAdminConfig(settings.adminDomain(), NOOP_CALLBACK);
|
||||
backups.configureCollectd(backupConfig, callback);
|
||||
});
|
||||
},
|
||||
|
||||
// check activation state and start the platform
|
||||
users.isActivated(function (error, activated) {
|
||||
if (error) return debug(error);
|
||||
if (!activated) return debug('initialize: not activated yet'); // not activated
|
||||
// always generate webadmin config since we have no versioning mechanism for the ejs
|
||||
function (callback) {
|
||||
if (!settings.adminDomain()) return callback();
|
||||
|
||||
onActivated(NOOP_CALLBACK);
|
||||
reverseProxy.writeDashboardConfig(settings.adminDomain(), callback);
|
||||
},
|
||||
|
||||
// check activation state and start the platform
|
||||
function (callback) {
|
||||
users.isActivated(function (error, activated) {
|
||||
if (error) return callback(error);
|
||||
|
||||
// configure nginx to be reachable by IP when not activated. for the moment, the IP based redirect exists even after domain is setup
|
||||
// just in case user forgot or some network error happenned in the middle (then browser refresh takes you to activation page)
|
||||
// we remove the config as a simple security measure to not expose IP <-> domain
|
||||
if (!activated) {
|
||||
debug('runStartupTasks: not activated. generating IP based redirection config');
|
||||
return reverseProxy.writeDefaultConfig({ activated: false }, callback);
|
||||
}
|
||||
|
||||
onActivated(callback);
|
||||
});
|
||||
}
|
||||
];
|
||||
|
||||
// we used to run tasks in parallel but simultaneous nginx reloads was causing issues
|
||||
async.series(async.reflectAll(tasks), function (error, results) {
|
||||
results.forEach((result, idx) => {
|
||||
if (result.error) debug(`Startup task at index ${idx} failed: ${result.error.message}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -140,15 +176,17 @@ function getConfig(callback) {
|
||||
version: constants.VERSION,
|
||||
isDemo: settings.isDemo(),
|
||||
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
|
||||
footer: allSettings[settings.FOOTER_KEY] || constants.FOOTER,
|
||||
features: appstore.getFeatures()
|
||||
footer: branding.renderFooter(allSettings[settings.FOOTER_KEY] || constants.FOOTER),
|
||||
features: appstore.getFeatures(),
|
||||
profileLocked: allSettings[settings.DIRECTORY_CONFIG_KEY].lockUserProfiles,
|
||||
mandatory2FA: allSettings[settings.DIRECTORY_CONFIG_KEY].mandatory2FA
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function reboot(callback) {
|
||||
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);
|
||||
});
|
||||
@@ -166,24 +204,11 @@ function runSystemChecks(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
async.parallel([
|
||||
checkBackupConfiguration,
|
||||
checkMailStatus,
|
||||
checkRebootRequired
|
||||
], 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) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -274,7 +299,7 @@ function prepareDashboardDomain(domain, auditSource, callback) {
|
||||
const conflict = result.filter(app => app.fqdn === fqdn);
|
||||
if (conflict.length) return callback(new BoxError(BoxError.BAD_STATE, 'Dashboard location conflicts with an existing app'));
|
||||
|
||||
tasks.add(tasks.TASK_PREPARE_DASHBOARD_DOMAIN, [ domain, auditSource ], function (error, taskId) {
|
||||
tasks.add(tasks.TASK_SETUP_DNS_AND_CERT, [ constants.ADMIN_LOCATION, domain, auditSource ], function (error, taskId) {
|
||||
if (error) return callback(error);
|
||||
|
||||
tasks.startTask(taskId, {}, NOOP_CALLBACK);
|
||||
@@ -296,12 +321,12 @@ function setDashboardDomain(domain, auditSource, callback) {
|
||||
domains.get(domain, function (error, domainObject) {
|
||||
if (error) return callback(error);
|
||||
|
||||
reverseProxy.writeAdminConfig(domain, function (error) {
|
||||
reverseProxy.writeDashboardConfig(domain, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const fqdn = domains.fqdn(constants.ADMIN_LOCATION, domainObject);
|
||||
|
||||
settings.setAdmin(domain, fqdn, function (error) {
|
||||
settings.setAdminLocation(domain, fqdn, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
eventlog.add(eventlog.ACTION_DASHBOARD_DOMAIN_UPDATE, auditSource, { domain: domain, fqdn: fqdn });
|
||||
@@ -313,36 +338,24 @@ function setDashboardDomain(domain, auditSource, callback) {
|
||||
}
|
||||
|
||||
// call this only post activation because it will restart mail server
|
||||
function setDashboardAndMailDomain(domain, auditSource, callback) {
|
||||
function updateDashboardDomain(domain, auditSource, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`setDashboardAndMailDomain: ${domain}`);
|
||||
debug(`updateDashboardDomain: ${domain}`);
|
||||
|
||||
if (settings.isDemo()) return callback(new BoxError(BoxError.CONFLICT, 'Not allowed in demo mode'));
|
||||
|
||||
setDashboardDomain(domain, auditSource, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
mail.onMailFqdnChanged(NOOP_CALLBACK); // this will update dns and re-configure mail server
|
||||
addons.restartService('turn', NOOP_CALLBACK); // to update the realm variable
|
||||
addons.rebuildService('turn', NOOP_CALLBACK); // to update the realm variable
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function setupDashboard(auditSource, progressCallback, callback) {
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
async.series([
|
||||
domains.prepareDashboardDomain.bind(null, settings.adminDomain(), auditSource, progressCallback),
|
||||
setDashboardDomain.bind(null, settings.adminDomain(), auditSource)
|
||||
], callback);
|
||||
}
|
||||
|
||||
function renewCerts(options, auditSource, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
@@ -356,3 +369,34 @@ function renewCerts(options, auditSource, callback) {
|
||||
callback(null, taskId);
|
||||
});
|
||||
}
|
||||
|
||||
function setupDnsAndCert(subdomain, domain, auditSource, progressCallback, callback) {
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
domains.get(domain, function (error, domainObject) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const adminFqdn = domains.fqdn(subdomain, domainObject);
|
||||
|
||||
sysinfo.getServerIp(function (error, ip) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.series([
|
||||
(done) => { progressCallback({ message: `Updating DNS of ${adminFqdn}` }); done(); },
|
||||
domains.upsertDnsRecords.bind(null, subdomain, domain, 'A', [ ip ]),
|
||||
(done) => { progressCallback({ message: `Waiting for DNS of ${adminFqdn}` }); done(); },
|
||||
domains.waitForDnsRecord.bind(null, subdomain, domain, 'A', ip, { interval: 30000, times: 50000 }),
|
||||
(done) => { progressCallback({ message: `Getting certificate of ${adminFqdn}` }); done(); },
|
||||
reverseProxy.ensureCertificate.bind(null, domains.fqdn(subdomain, domainObject), domain, auditSource)
|
||||
], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+4
-4
@@ -26,7 +26,7 @@ exports = module.exports = {
|
||||
|
||||
PORT: CLOUDRON ? 3000 : 5454,
|
||||
INTERNAL_SMTP_PORT: 2525, // this value comes from the mail container
|
||||
SYSADMIN_PORT: 3001, // unused
|
||||
AUTHWALL_PORT: 3001,
|
||||
LDAP_PORT: 3002,
|
||||
DOCKER_PROXY_PORT: 3003,
|
||||
|
||||
@@ -37,7 +37,7 @@ exports = module.exports = {
|
||||
DEFAULT_MEMORY_LIMIT: (256 * 1024 * 1024), // see also client.js
|
||||
|
||||
DEMO_USERNAME: 'cloudron',
|
||||
DEMO_BLACKLISTED_APPS: [ 'com.github.cloudtorrent' ],
|
||||
DEMO_BLACKLISTED_APPS: [ 'com.github.cloudtorrent', 'net.alltubedownload.cloudronapp', 'com.adguard.home.cloudronapp' ],
|
||||
|
||||
AUTOUPDATE_PATTERN_NEVER: 'never',
|
||||
|
||||
@@ -48,8 +48,8 @@ exports = module.exports = {
|
||||
|
||||
SUPPORT_EMAIL: 'support@cloudron.io',
|
||||
|
||||
FOOTER: '© 2020 [Cloudron](https://cloudron.io) [Forum <i class="fa fa-comments"></i>](https://forum.cloudron.io)',
|
||||
FOOTER: '© %YEAR% [Cloudron](https://cloudron.io) [Forum <i class="fa fa-comments"></i>](https://forum.cloudron.io)',
|
||||
|
||||
VERSION: process.env.BOX_ENV === 'cloudron' ? fs.readFileSync(path.join(__dirname, '../VERSION'), 'utf8').trim() : '5.1.1-test'
|
||||
VERSION: process.env.BOX_ENV === 'cloudron' ? fs.readFileSync(path.join(__dirname, '../VERSION'), 'utf8').trim() : '6.0.1-test'
|
||||
};
|
||||
|
||||
|
||||
+27
-70
@@ -4,10 +4,7 @@
|
||||
// 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_ENSURE_BACKUP_PATTERN_LT_6HOURS = '00 45 1,7,13,19 * * *',
|
||||
DEFAULT_BOX_ENSURE_BACKUP_PATTERN_GT_6HOURS = '00 45 1,3,5,23 * * *',
|
||||
DEFAULT_BOX_AUTOUPDATE_PATTERN = '00 00 1,3,5,23 * * *',
|
||||
DEFAULT_APP_AUTOUPDATE_PATTERN = '00 15 1,3,5,23 * * *';
|
||||
DEFAULT_AUTOUPDATE_PATTERN = '00 00 1,3,5,23 * * *';
|
||||
|
||||
exports = module.exports = {
|
||||
startJobs,
|
||||
@@ -16,13 +13,11 @@ exports = module.exports = {
|
||||
|
||||
handleSettingsChanged,
|
||||
|
||||
DEFAULT_BOX_AUTOUPDATE_PATTERN,
|
||||
DEFAULT_APP_AUTOUPDATE_PATTERN
|
||||
DEFAULT_AUTOUPDATE_PATTERN,
|
||||
};
|
||||
|
||||
var appHealthMonitor = require('./apphealthmonitor.js'),
|
||||
apps = require('./apps.js'),
|
||||
appstore = require('./appstore.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
auditSource = require('./auditsource.js'),
|
||||
@@ -41,12 +36,9 @@ var appHealthMonitor = require('./apphealthmonitor.js'),
|
||||
updateChecker = require('./updatechecker.js');
|
||||
|
||||
var gJobs = {
|
||||
alive: null, // send periodic stats
|
||||
appAutoUpdater: null,
|
||||
boxAutoUpdater: null,
|
||||
appUpdateChecker: null,
|
||||
autoUpdater: null,
|
||||
backup: null,
|
||||
boxUpdateChecker: null,
|
||||
updateChecker: null,
|
||||
systemChecks: null,
|
||||
diskSpaceChecker: null,
|
||||
certificateRenew: null,
|
||||
@@ -72,15 +64,11 @@ var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
function startJobs(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
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
|
||||
});
|
||||
debug('startJobs: starting cron jobs');
|
||||
|
||||
const randomTick = Math.floor(60*Math.random());
|
||||
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),
|
||||
start: true
|
||||
});
|
||||
@@ -91,15 +79,10 @@ function startJobs(callback) {
|
||||
start: true
|
||||
});
|
||||
|
||||
gJobs.boxUpdateCheckerJob = new CronJob({
|
||||
cronTime: '00 ' + randomMinute + ' 1,3,5,21,23 * * *', // 5 times
|
||||
onTick: () => updateChecker.checkBoxUpdates({ automatic: true }, NOOP_CALLBACK),
|
||||
start: true
|
||||
});
|
||||
|
||||
gJobs.appUpdateChecker = new CronJob({
|
||||
cronTime: '00 ' + randomMinute + ' 2,4,6,20,22 * * *', // 5 times
|
||||
onTick: () => updateChecker.checkAppUpdates({ automatic: true }, NOOP_CALLBACK),
|
||||
// this is run separately from the update itself so that the user can disable automatic updates but can still get a notification
|
||||
gJobs.updateCheckerJob = new CronJob({
|
||||
cronTime: `${randomTick} ${randomTick} 1,5,9,13,17,21,23 * * *`,
|
||||
onTick: () => updateChecker.checkForUpdates({ automatic: true }, NOOP_CALLBACK),
|
||||
start: true
|
||||
});
|
||||
|
||||
@@ -150,8 +133,7 @@ function startJobs(callback) {
|
||||
|
||||
const tz = allSettings[settings.TIME_ZONE_KEY];
|
||||
backupConfigChanged(allSettings[settings.BACKUP_CONFIG_KEY], tz);
|
||||
appAutoupdatePatternChanged(allSettings[settings.APP_AUTOUPDATE_PATTERN_KEY], tz);
|
||||
boxAutoupdatePatternChanged(allSettings[settings.BOX_AUTOUPDATE_PATTERN_KEY], tz);
|
||||
autoupdatePatternChanged(allSettings[settings.AUTOUPDATE_PATTERN_KEY], tz);
|
||||
dynamicDnsChanged(allSettings[settings.DYNAMIC_DNS_KEY]);
|
||||
|
||||
callback();
|
||||
@@ -166,8 +148,7 @@ function handleSettingsChanged(key, value) {
|
||||
switch (key) {
|
||||
case settings.TIME_ZONE_KEY:
|
||||
case settings.BACKUP_CONFIG_KEY:
|
||||
case settings.APP_AUTOUPDATE_PATTERN_KEY:
|
||||
case settings.BOX_AUTOUPDATE_PATTERN_KEY:
|
||||
case settings.AUTOUPDATE_PATTERN_KEY:
|
||||
case settings.DYNAMIC_DNS_KEY:
|
||||
debug('handleSettingsChanged: recreating all jobs');
|
||||
async.series([
|
||||
@@ -184,71 +165,47 @@ function backupConfigChanged(value, tz) {
|
||||
assert.strictEqual(typeof value, 'object');
|
||||
assert.strictEqual(typeof tz, 'string');
|
||||
|
||||
debug(`backupConfigChanged: interval ${value.intervalSecs} (${tz})`);
|
||||
debug(`backupConfigChanged: schedule ${value.schedulePattern} (${tz})`);
|
||||
|
||||
if (gJobs.backup) gJobs.backup.stop();
|
||||
let pattern;
|
||||
if (value.intervalSecs <= 6 * 60 * 60) {
|
||||
pattern = DEFAULT_BOX_ENSURE_BACKUP_PATTERN_LT_6HOURS; // no option but to backup in the middle of the day
|
||||
} else {
|
||||
pattern = DEFAULT_BOX_ENSURE_BACKUP_PATTERN_GT_6HOURS; // avoid middle of the day backups. it's 45 to not overlap auto-updates
|
||||
}
|
||||
|
||||
gJobs.backup = new CronJob({
|
||||
cronTime: pattern,
|
||||
onTick: backups.ensureBackup.bind(null, auditSource.CRON, NOOP_CALLBACK),
|
||||
cronTime: value.schedulePattern,
|
||||
onTick: backups.startBackupTask.bind(null, auditSource.CRON, NOOP_CALLBACK),
|
||||
start: true,
|
||||
timeZone: tz
|
||||
});
|
||||
}
|
||||
|
||||
function boxAutoupdatePatternChanged(pattern, tz) {
|
||||
function autoupdatePatternChanged(pattern, tz) {
|
||||
assert.strictEqual(typeof pattern, 'string');
|
||||
assert.strictEqual(typeof tz, 'string');
|
||||
|
||||
debug(`boxAutoupdatePatternChanged: pattern - ${pattern} (${tz})`);
|
||||
debug(`autoupdatePatternChanged: pattern - ${pattern} (${tz})`);
|
||||
|
||||
if (gJobs.boxAutoUpdater) gJobs.boxAutoUpdater.stop();
|
||||
if (gJobs.autoUpdater) gJobs.autoUpdater.stop();
|
||||
|
||||
if (pattern === constants.AUTOUPDATE_PATTERN_NEVER) return;
|
||||
|
||||
gJobs.boxAutoUpdater = new CronJob({
|
||||
gJobs.autoUpdater = new CronJob({
|
||||
cronTime: pattern,
|
||||
onTick: function() {
|
||||
var updateInfo = updateChecker.getUpdateInfo();
|
||||
if (updateInfo.box) {
|
||||
debug('Starting autoupdate to %j', updateInfo.box);
|
||||
const updateInfo = updateChecker.getUpdateInfo();
|
||||
// do box before app updates. for the off chance that the box logic fixes some app update logic issue
|
||||
if (updateInfo.box && !updateInfo.box.unstable) {
|
||||
debug('Starting box autoupdate to %j', updateInfo.box);
|
||||
updater.updateToLatest({ skipBackup: false }, auditSource.CRON, NOOP_CALLBACK);
|
||||
} else {
|
||||
debug('No box auto updates available');
|
||||
return;
|
||||
}
|
||||
},
|
||||
start: true,
|
||||
timeZone: tz
|
||||
});
|
||||
}
|
||||
|
||||
function appAutoupdatePatternChanged(pattern, tz) {
|
||||
assert.strictEqual(typeof pattern, 'string');
|
||||
assert.strictEqual(typeof tz, 'string');
|
||||
|
||||
debug(`appAutoupdatePatternChanged: pattern ${pattern} (${tz})`);
|
||||
|
||||
if (gJobs.appAutoUpdater) gJobs.appAutoUpdater.stop();
|
||||
|
||||
if (pattern === constants.AUTOUPDATE_PATTERN_NEVER) return;
|
||||
|
||||
gJobs.appAutoUpdater = new CronJob({
|
||||
cronTime: pattern,
|
||||
onTick: function() {
|
||||
var updateInfo = updateChecker.getUpdateInfo();
|
||||
if (updateInfo.apps) {
|
||||
if (updateInfo.apps && Object.keys(updateInfo.apps).length > 0) {
|
||||
debug('Starting app update to %j', updateInfo.apps);
|
||||
apps.autoupdateApps(updateInfo.apps, auditSource.CRON, NOOP_CALLBACK);
|
||||
} else {
|
||||
debug('No app auto updates available');
|
||||
}
|
||||
},
|
||||
|
||||
start: true,
|
||||
timeZone: tz
|
||||
});
|
||||
|
||||
+32
-82
@@ -22,8 +22,7 @@ var assert = require('assert'),
|
||||
once = require('once'),
|
||||
util = require('util');
|
||||
|
||||
var gConnectionPool = null,
|
||||
gDefaultConnection = null;
|
||||
var gConnectionPool = null;
|
||||
|
||||
const gDatabase = {
|
||||
hostname: '127.0.0.1',
|
||||
@@ -43,66 +42,37 @@ function initialize(callback) {
|
||||
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({
|
||||
connectionLimit: 5, // this has to be > 1 since we store one connection as 'default'. the rest for transactions
|
||||
connectionLimit: 5,
|
||||
host: gDatabase.hostname,
|
||||
user: gDatabase.username,
|
||||
password: gDatabase.password,
|
||||
port: gDatabase.port,
|
||||
database: gDatabase.name,
|
||||
multipleStatements: false,
|
||||
waitForConnections: true, // getConnection() will wait until a connection is avaiable
|
||||
ssl: false,
|
||||
timezone: 'Z' // mysql follows the SYSTEM timezone. on Cloudron, this is UTC
|
||||
});
|
||||
|
||||
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('SET SESSION sql_mode = \'strict_all_tables\'');
|
||||
});
|
||||
|
||||
reconnect(callback);
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function uninitialize(callback) {
|
||||
if (gConnectionPool) {
|
||||
gConnectionPool.end(callback);
|
||||
gDefaultConnection = null;
|
||||
gConnectionPool = null;
|
||||
} else {
|
||||
callback(null);
|
||||
}
|
||||
}
|
||||
if (!gConnectionPool) return callback(null);
|
||||
|
||||
function reconnect(callback) {
|
||||
callback = callback ? once(callback) : function () {};
|
||||
|
||||
debug('reconnect: connecting to database');
|
||||
|
||||
gConnectionPool.getConnection(function (error, connection) {
|
||||
if (error) {
|
||||
debug(`reconnect: db connection error. ${error.message} fatal:${error.fatal} code:${error.code}. Will retry in 10 seconds`);
|
||||
return setTimeout(reconnect.bind(null, callback), 10000);
|
||||
}
|
||||
|
||||
debug('reconnect: connected to database');
|
||||
|
||||
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');
|
||||
|
||||
debug(`reconnect: db connection error. ${error.message} fatal:${error.fatal} code:${error.code}. Will retry in 10 seconds`);
|
||||
|
||||
gDefaultConnection = null;
|
||||
|
||||
// This is most likely an issue an can cause double callbacks from reconnect()
|
||||
setTimeout(reconnect.bind(null, callback), 10000);
|
||||
});
|
||||
|
||||
gDefaultConnection = connection;
|
||||
|
||||
callback(null);
|
||||
});
|
||||
gConnectionPool.end(callback);
|
||||
gConnectionPool = null;
|
||||
}
|
||||
|
||||
function clear(callback) {
|
||||
@@ -115,39 +85,14 @@ function clear(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) return callback(error);
|
||||
|
||||
connection.beginTransaction(function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
return callback(null, connection);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function query() {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
var callback = args[args.length - 1];
|
||||
const args = Array.prototype.slice.call(arguments);
|
||||
const callback = args[args.length - 1];
|
||||
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'));
|
||||
|
||||
gDefaultConnection.query.apply(gDefaultConnection, args);
|
||||
}
|
||||
|
||||
function rollback(connection, transactionError, callback) {
|
||||
connection.rollback(function (error) {
|
||||
if (error) debug('rollback: error when rolling back', error);
|
||||
|
||||
connection.release();
|
||||
callback(transactionError);
|
||||
});
|
||||
gConnectionPool.query.apply(gConnectionPool, args); // this is same as getConnection/query/release
|
||||
}
|
||||
|
||||
function transaction(queries, callback) {
|
||||
@@ -156,21 +101,26 @@ function transaction(queries, callback) {
|
||||
|
||||
callback = once(callback);
|
||||
|
||||
beginTransaction(function (error, connection) {
|
||||
gConnectionPool.getConnection(function (error, connection) {
|
||||
if (error) return callback(error);
|
||||
|
||||
connection.on('error', callback);
|
||||
const releaseConnection = (error) => { connection.release(); callback(error); };
|
||||
|
||||
async.mapSeries(queries, function iterator(query, done) {
|
||||
connection.query(query.query, query.args, done);
|
||||
}, function seriesDone(error, results) {
|
||||
if (error) return rollback(connection, error, callback);
|
||||
connection.beginTransaction(function (error) {
|
||||
if (error) return releaseConnection(error);
|
||||
|
||||
connection.commit(function (error) {
|
||||
if (error) return rollback(connection, error, callback);
|
||||
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.release();
|
||||
callback(null, results);
|
||||
connection.commit(function (error) {
|
||||
if (error) return connection.rollback(() => releaseConnection(error));
|
||||
|
||||
connection.release();
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -194,7 +144,7 @@ function exportToFile(file, callback) {
|
||||
|
||||
// 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
|
||||
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}"`;
|
||||
|
||||
|
||||
-177
@@ -1,177 +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'),
|
||||
constants = require('../constants.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 = constants.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 === constants.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);
|
||||
});
|
||||
});
|
||||
}
|
||||
+7
-4
@@ -119,9 +119,10 @@ function get(domainObject, location, type, callback) {
|
||||
zoneName = domainObject.zoneName,
|
||||
name = domains.getName(domainObject, location, type) || '';
|
||||
|
||||
getZoneRecords(dnsConfig, zoneName, name, type, function (error, { records }) {
|
||||
getZoneRecords(dnsConfig, zoneName, name, type, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const { records } = result;
|
||||
var tmp = records.map(function (record) { return record.target; });
|
||||
|
||||
debug('get: %j', tmp);
|
||||
@@ -143,9 +144,10 @@ function upsert(domainObject, location, type, values, callback) {
|
||||
|
||||
debug('upsert: %s for zone %s of type %s with values %j', name, zoneName, type, values);
|
||||
|
||||
getZoneRecords(dnsConfig, zoneName, name, type, function (error, { zoneId, records }) {
|
||||
getZoneRecords(dnsConfig, zoneName, name, type, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const { zoneId, records } = result;
|
||||
let i = 0, recordIds = []; // used to track available records to update instead of create
|
||||
|
||||
async.eachSeries(values, function (value, iteratorCallback) {
|
||||
@@ -222,9 +224,10 @@ function del(domainObject, location, type, values, callback) {
|
||||
zoneName = domainObject.zoneName,
|
||||
name = domains.getName(domainObject, location, type) || '';
|
||||
|
||||
getZoneRecords(dnsConfig, zoneName, name, type, function (error, { zoneId, records }) {
|
||||
getZoneRecords(dnsConfig, zoneName, name, type, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const { zoneId, records } = result;
|
||||
if (records.length === 0) return callback(null);
|
||||
|
||||
var tmp = records.filter(function (record) { return values.some(function (value) { return value === record.target; }); });
|
||||
@@ -287,7 +290,7 @@ function verifyDnsConfig(domainObject, callback) {
|
||||
if (error || !nameservers) return callback(new BoxError(BoxError.BAD_FIELD, error ? error.message : 'Unable to get nameservers', { field: 'nameservers' }));
|
||||
|
||||
if (nameservers.map(function (n) { return n.toLowerCase(); }).indexOf('ns1.linode.com') === -1) {
|
||||
debug('verifyDnsConfig: %j does not contains DO NS', nameservers);
|
||||
debug('verifyDnsConfig: %j does not contains linode NS', nameservers);
|
||||
return callback(new BoxError(BoxError.BAD_FIELD, 'Domain nameservers are not set to Linode', { field: 'nameservers' }));
|
||||
}
|
||||
|
||||
|
||||
@@ -78,13 +78,11 @@ function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
|
||||
return callback(new BoxError(BoxError.EXTERNAL_ERROR, errorMessage));
|
||||
}
|
||||
if (!tmp.CommandResponse[0]) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response'));
|
||||
if (!tmp.CommandResponse[0].DomainDNSGetHostsResult[0]) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response'));
|
||||
|
||||
var hosts = result.ApiResponse.CommandResponse[0].DomainDNSGetHostsResult[0].host.map(function (h) {
|
||||
return h['$'];
|
||||
});
|
||||
const host = safe.query(tmp, 'CommandResponse[0].DomainDNSGetHostsResult[0].host');
|
||||
if (!host) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Invalid response: ${JSON.stringify(tmp)}`));
|
||||
if (!Array.isArray(host)) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `host is not an array: ${JSON.stringify(tmp)}`));
|
||||
|
||||
const hosts = host.map(h => h['$']);
|
||||
callback(null, hosts);
|
||||
});
|
||||
});
|
||||
|
||||
+3
-2
@@ -281,13 +281,14 @@ function verifyDnsConfig(domainObject, callback) {
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
debug('verifyDnsConfig: Test A record removed again');
|
||||
|
||||
+173
-108
@@ -1,35 +1,35 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
testRegistryConfig: testRegistryConfig,
|
||||
setRegistryConfig: setRegistryConfig,
|
||||
injectPrivateFields: injectPrivateFields,
|
||||
removePrivateFields: removePrivateFields,
|
||||
testRegistryConfig,
|
||||
setRegistryConfig,
|
||||
injectPrivateFields,
|
||||
removePrivateFields,
|
||||
|
||||
ping: ping,
|
||||
ping,
|
||||
|
||||
info: info,
|
||||
downloadImage: downloadImage,
|
||||
createContainer: createContainer,
|
||||
startContainer: startContainer,
|
||||
restartContainer: restartContainer,
|
||||
stopContainer: stopContainer,
|
||||
info,
|
||||
downloadImage,
|
||||
createContainer,
|
||||
startContainer,
|
||||
restartContainer,
|
||||
stopContainer,
|
||||
stopContainerByName: stopContainer,
|
||||
stopContainers: stopContainers,
|
||||
deleteContainer: deleteContainer,
|
||||
deleteContainerByName: deleteContainer,
|
||||
deleteImage: deleteImage,
|
||||
deleteContainers: deleteContainers,
|
||||
createSubcontainer: createSubcontainer,
|
||||
getContainerIdByIp: getContainerIdByIp,
|
||||
inspect: inspect,
|
||||
stopContainers,
|
||||
deleteContainer,
|
||||
deleteImage,
|
||||
deleteContainers,
|
||||
createSubcontainer,
|
||||
getContainerIdByIp,
|
||||
inspect,
|
||||
getContainerIp,
|
||||
inspectByName: inspect,
|
||||
execContainer: execContainer,
|
||||
getEvents: getEvents,
|
||||
memoryUsage: memoryUsage,
|
||||
createVolume: createVolume,
|
||||
removeVolume: removeVolume,
|
||||
clearVolume: clearVolume
|
||||
execContainer,
|
||||
getEvents,
|
||||
memoryUsage,
|
||||
createVolume,
|
||||
removeVolume,
|
||||
clearVolume
|
||||
};
|
||||
|
||||
var addons = require('./addons.js'),
|
||||
@@ -40,11 +40,13 @@ var addons = require('./addons.js'),
|
||||
constants = require('./constants.js'),
|
||||
debug = require('debug')('box:docker'),
|
||||
Docker = require('dockerode'),
|
||||
os = require('os'),
|
||||
path = require('path'),
|
||||
settings = require('./settings.js'),
|
||||
shell = require('./shell.js'),
|
||||
safe = require('safetydance'),
|
||||
util = require('util'),
|
||||
volumes = require('./volumes.js'),
|
||||
_ = require('underscore');
|
||||
|
||||
const CLEARVOLUME_CMD = path.join(__dirname, 'scripts/clearvolume.sh'),
|
||||
@@ -171,26 +173,53 @@ function downloadImage(manifest, callback) {
|
||||
|
||||
debug('downloadImage %s', manifest.dockerImage);
|
||||
|
||||
var attempt = 1;
|
||||
const image = gConnection.getImage(manifest.dockerImage);
|
||||
|
||||
async.retry({ times: 10, interval: 5000, errorFilter: e => e.reason !== BoxError.NOT_FOUND }, function (retryCallback) {
|
||||
debug('Downloading image %s. attempt: %s', manifest.dockerImage, attempt++);
|
||||
image.inspect(function (error, result) {
|
||||
if (!error && result) return callback(null); // image is already present locally
|
||||
|
||||
pullImage(manifest, retryCallback);
|
||||
}, callback);
|
||||
let attempt = 1;
|
||||
|
||||
async.retry({ times: 10, interval: 5000, errorFilter: e => e.reason !== BoxError.NOT_FOUND }, function (retryCallback) {
|
||||
debug('Downloading image %s. attempt: %s', manifest.dockerImage, attempt++);
|
||||
|
||||
pullImage(manifest, retryCallback);
|
||||
}, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function getBindsSync(app) {
|
||||
function getBinds(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (app.mounts.length === 0) return callback(null);
|
||||
|
||||
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'}`);
|
||||
volumes.list(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
let volumesById = {};
|
||||
result.forEach(r => volumesById[r.id] = r);
|
||||
|
||||
for (const mount of app.mounts) {
|
||||
const volume = volumesById[mount.volumeId];
|
||||
binds.push(`${volume.hostPath}:/media/${volume.name}:${mount.readOnly ? 'ro' : 'rw'}`);
|
||||
}
|
||||
|
||||
callback(null, binds);
|
||||
});
|
||||
}
|
||||
|
||||
function getLowerUpIp() { // see getifaddrs and IFF_LOWER_UP and netdevice
|
||||
const ni = os.networkInterfaces(); // { lo: [], eth0: [] }
|
||||
for (const iname of Object.keys(ni)) {
|
||||
if (iname === 'lo') continue;
|
||||
for (const address of ni[iname]) {
|
||||
if (!address.internal && address.family === 'IPv4') return address.address;
|
||||
}
|
||||
}
|
||||
|
||||
return binds;
|
||||
return null;
|
||||
}
|
||||
|
||||
function createSubcontainer(app, name, cmd, options, callback) {
|
||||
@@ -200,12 +229,11 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
let isAppContainer = !cmd; // non app-containers are like scheduler and exec (terminal) containers
|
||||
let isAppContainer = !cmd; // non app-containers are like scheduler
|
||||
|
||||
var manifest = app.manifest;
|
||||
var exposedPorts = {}, dockerPortBindings = { };
|
||||
var domain = app.fqdn;
|
||||
const hostname = isAppContainer ? app.id : name;
|
||||
|
||||
const envPrefix = manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
|
||||
|
||||
@@ -219,11 +247,6 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
`${envPrefix}APP_DOMAIN=${domain}`
|
||||
];
|
||||
|
||||
// docker portBindings requires ports to be exposed
|
||||
exposedPorts[manifest.httpPort + '/tcp'] = {};
|
||||
|
||||
dockerPortBindings[manifest.httpPort + '/tcp'] = [ { HostIp: '127.0.0.1', HostPort: app.httpPort + '' } ];
|
||||
|
||||
var portEnv = [];
|
||||
for (let portName in app.portBindings) {
|
||||
const hostPort = app.portBindings[portName];
|
||||
@@ -232,10 +255,12 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
|
||||
var containerPort = ports[portName].containerPort || hostPort;
|
||||
|
||||
// docker portBindings requires ports to be exposed
|
||||
exposedPorts[`${containerPort}/${portType}`] = {};
|
||||
portEnv.push(`${portName}=${hostPort}`);
|
||||
|
||||
dockerPortBindings[`${containerPort}/${portType}`] = [ { HostIp: '0.0.0.0', HostPort: hostPort + '' } ];
|
||||
const hostIp = hostPort === 53 ? getLowerUpIp() : '0.0.0.0'; // port 53 is special because it is possibly taken by systemd-resolved
|
||||
dockerPortBindings[`${containerPort}/${portType}`] = [ { HostIp: hostIp, HostPort: hostPort + '' } ];
|
||||
}
|
||||
|
||||
let appEnv = [];
|
||||
@@ -257,77 +282,101 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
addons.getEnvironment(app, function (error, addonEnv) {
|
||||
if (error) return callback(error);
|
||||
|
||||
// do no set hostname of containers to location as it might conflict with addons names. for example, an app installed in mail
|
||||
// location may not reach mail container anymore by DNS. We cannot set hostname to fqdn either as that sets up the dns
|
||||
// name to look up the internal docker ip. this makes curl from within container fail
|
||||
// Note that Hostname has no effect on DNS. We have to use the --net-alias for dns.
|
||||
// Hostname cannot be set with container NetworkMode
|
||||
var containerOptions = {
|
||||
name: name, // for referencing containers
|
||||
Tty: isAppContainer,
|
||||
Hostname: hostname,
|
||||
Image: app.manifest.dockerImage,
|
||||
Cmd: (isAppContainer && app.debugMode && app.debugMode.cmd) ? app.debugMode.cmd : cmd,
|
||||
Env: stdEnv.concat(addonEnv).concat(portEnv).concat(appEnv),
|
||||
ExposedPorts: isAppContainer ? exposedPorts : { },
|
||||
Volumes: { // see also ReadonlyRootfs
|
||||
'/tmp': {},
|
||||
'/run': {}
|
||||
},
|
||||
Labels: {
|
||||
'fqdn': app.fqdn,
|
||||
'appId': app.id,
|
||||
'isSubcontainer': String(!isAppContainer),
|
||||
'isCloudronManaged': String(true)
|
||||
},
|
||||
HostConfig: {
|
||||
Mounts: addons.getMountsSync(app, app.manifest.addons),
|
||||
Binds: getBindsSync(app), // ideally, we have to use 'Mounts' but we have to create volumes then
|
||||
LogConfig: {
|
||||
Type: 'syslog',
|
||||
Config: {
|
||||
'tag': app.id,
|
||||
'syslog-address': 'udp://127.0.0.1:2514', // see apps.js:validatePortBindings()
|
||||
'syslog-format': 'rfc5424'
|
||||
}
|
||||
getBinds(app, function (error, binds) {
|
||||
if (error) return callback(error);
|
||||
|
||||
let containerOptions = {
|
||||
name: name, // for referencing containers
|
||||
Tty: isAppContainer,
|
||||
Image: app.manifest.dockerImage,
|
||||
Cmd: (isAppContainer && app.debugMode && app.debugMode.cmd) ? app.debugMode.cmd : cmd,
|
||||
Env: stdEnv.concat(addonEnv).concat(portEnv).concat(appEnv),
|
||||
ExposedPorts: isAppContainer ? exposedPorts : { },
|
||||
Volumes: { // see also ReadonlyRootfs
|
||||
'/tmp': {},
|
||||
'/run': {}
|
||||
},
|
||||
Memory: memoryLimit / 2,
|
||||
MemorySwap: memoryLimit, // Memory + Swap
|
||||
PortBindings: isAppContainer ? dockerPortBindings : { },
|
||||
PublishAllPorts: false,
|
||||
ReadonlyRootfs: app.debugMode ? !!app.debugMode.readonlyRootfs : true,
|
||||
RestartPolicy: {
|
||||
'Name': isAppContainer ? 'unless-stopped' : 'no',
|
||||
'MaximumRetryCount': 0
|
||||
Labels: {
|
||||
'fqdn': app.fqdn,
|
||||
'appId': app.id,
|
||||
'isSubcontainer': String(!isAppContainer),
|
||||
'isCloudronManaged': String(true)
|
||||
},
|
||||
CpuShares: app.cpuShares,
|
||||
VolumesFrom: isAppContainer ? null : [ app.containerId + ':rw' ],
|
||||
NetworkMode: 'cloudron', // user defined bridge network
|
||||
Dns: ['172.18.0.1'], // use internal dns
|
||||
DnsSearch: ['.'], // use internal dns
|
||||
SecurityOpt: [ 'apparmor=docker-cloudron-app' ],
|
||||
CapAdd: [],
|
||||
CapDrop: [ 'NET_RAW' ] // https://docs-stage.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
|
||||
},
|
||||
NetworkingConfig: {
|
||||
EndpointsConfig: {
|
||||
cloudron: {
|
||||
Aliases: [ name ] // this allows sub-containers reach app containers by name
|
||||
}
|
||||
HostConfig: {
|
||||
Mounts: addons.getMountsSync(app, app.manifest.addons),
|
||||
Binds: binds, // ideally, we have to use 'Mounts' but we have to create volumes then
|
||||
LogConfig: {
|
||||
Type: 'syslog',
|
||||
Config: {
|
||||
'tag': app.id,
|
||||
'syslog-address': 'udp://127.0.0.1:2514', // see apps.js:validatePortBindings()
|
||||
'syslog-format': 'rfc5424'
|
||||
}
|
||||
},
|
||||
Memory: memoryLimit / 2,
|
||||
MemorySwap: memoryLimit, // Memory + Swap
|
||||
PortBindings: isAppContainer ? dockerPortBindings : { },
|
||||
PublishAllPorts: false,
|
||||
ReadonlyRootfs: app.debugMode ? !!app.debugMode.readonlyRootfs : true,
|
||||
RestartPolicy: {
|
||||
'Name': isAppContainer ? 'unless-stopped' : 'no',
|
||||
'MaximumRetryCount': 0
|
||||
},
|
||||
CpuShares: app.cpuShares,
|
||||
VolumesFrom: isAppContainer ? null : [ app.containerId + ':rw' ],
|
||||
SecurityOpt: [ 'apparmor=docker-cloudron-app' ],
|
||||
CapAdd: [],
|
||||
CapDrop: []
|
||||
}
|
||||
};
|
||||
|
||||
// do no set hostname of containers to location as it might conflict with addons names. for example, an app installed in mail
|
||||
// location may not reach mail container anymore by DNS. We cannot set hostname to fqdn either as that sets up the dns
|
||||
// name to look up the internal docker ip. this makes curl from within container fail
|
||||
// Note that Hostname has no effect on DNS. We have to use the --net-alias for dns.
|
||||
// Hostname cannot be set with container NetworkMode. Subcontainers run is the network space of the app container
|
||||
// This is done to prevent lots of up/down events and iptables locking
|
||||
if (isAppContainer) {
|
||||
containerOptions.Hostname = app.id;
|
||||
containerOptions.HostConfig.NetworkMode = 'cloudron'; // user defined bridge network
|
||||
containerOptions.HostConfig.Dns = ['172.18.0.1']; // use internal dns
|
||||
containerOptions.HostConfig.DnsSearch = ['.']; // use internal dns
|
||||
|
||||
containerOptions.NetworkingConfig = {
|
||||
EndpointsConfig: {
|
||||
cloudron: {
|
||||
IPAMConfig: {
|
||||
IPv4Address: app.containerIp
|
||||
},
|
||||
Aliases: [ name ] // adds hostname entry with container name
|
||||
}
|
||||
}
|
||||
};
|
||||
} else {
|
||||
containerOptions.HostConfig.NetworkMode = `container:${app.containerId}`; // scheduler containers must have same IP as app for various addon auth
|
||||
}
|
||||
};
|
||||
|
||||
var capabilities = manifest.capabilities || [];
|
||||
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
|
||||
var capabilities = manifest.capabilities || [];
|
||||
|
||||
containerOptions = _.extend(containerOptions, options);
|
||||
// https://docs-stage.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
|
||||
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
|
||||
|
||||
gConnection.createContainer(containerOptions, function (error, container) {
|
||||
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
|
||||
if (capabilities.includes('vaapi') && safe.fs.existsSync('/dev/dri')) {
|
||||
containerOptions.HostConfig.Devices = [
|
||||
{ PathOnHost: '/dev/dri', PathInContainer: '/dev/dri', CgroupPermissions: 'rwm' }
|
||||
];
|
||||
}
|
||||
|
||||
callback(null, container);
|
||||
containerOptions = _.extend(containerOptions, options);
|
||||
|
||||
gConnection.createContainer(containerOptions, function (error, container) {
|
||||
if (error && error.statusCode === 409) return callback(new BoxError(BoxError.ALREADY_EXISTS, error));
|
||||
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
|
||||
|
||||
callback(null, container);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -392,7 +441,7 @@ function stopContainer(containerId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function deleteContainer(containerId, callback) {
|
||||
function deleteContainer(containerId, callback) { // id can also be name
|
||||
assert(!containerId || typeof containerId === 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -504,13 +553,29 @@ function inspect(containerId, callback) {
|
||||
var container = gConnection.getContainer(containerId);
|
||||
|
||||
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));
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function getContainerIp(containerId, callback) {
|
||||
assert.strictEqual(typeof containerId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (constants.TEST) return callback(null, '127.0.5.5');
|
||||
|
||||
inspect(containerId, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const ip = safe.query(result, 'NetworkSettings.Networks.cloudron.IPAddress', null);
|
||||
if (!ip) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Error getting container IP'));
|
||||
|
||||
callback(null, ip);
|
||||
});
|
||||
}
|
||||
|
||||
function execContainer(containerId, options, callback) {
|
||||
assert.strictEqual(typeof containerId, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
|
||||
+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
|
||||
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 });
|
||||
});
|
||||
|
||||
|
||||
+6
-59
@@ -26,13 +26,10 @@ module.exports = exports = {
|
||||
|
||||
parentDomain: parentDomain,
|
||||
|
||||
checkDnsRecords: checkDnsRecords,
|
||||
|
||||
prepareDashboardDomain: prepareDashboardDomain
|
||||
checkDnsRecords: checkDnsRecords
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
constants = require('./constants.js'),
|
||||
debug = require('debug')('box:domains'),
|
||||
@@ -54,7 +51,6 @@ function api(provider) {
|
||||
assert.strictEqual(typeof provider, 'string');
|
||||
|
||||
switch (provider) {
|
||||
case 'caas': return require('./dns/caas.js');
|
||||
case 'cloudflare': return require('./dns/cloudflare.js');
|
||||
case 'route53': return require('./dns/route53.js');
|
||||
case 'gcdns': return require('./dns/gcdns.js');
|
||||
@@ -93,14 +89,12 @@ function verifyDnsConfig(dnsConfig, domain, zoneName, provider, callback) {
|
||||
if (error && error.reason === BoxError.EXTERNAL_ERROR) return callback(new BoxError(BoxError.BAD_FIELD, `Configuration error: ${error.message}`));
|
||||
if (error) return callback(error);
|
||||
|
||||
result.hyphenatedSubdomains = !!dnsConfig.hyphenatedSubdomains;
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function fqdn(location, domainObject) {
|
||||
return location + (location ? (domainObject.config.hyphenatedSubdomains ? '-' : '.') : '') + domainObject.domain;
|
||||
return location + (location ? '.' : '') + domainObject.domain;
|
||||
}
|
||||
|
||||
// Hostname validation comes from RFC 1123 (section 2.1)
|
||||
@@ -134,10 +128,6 @@ function validateHostname(location, domainObject) {
|
||||
if (/^[-.]/.test(location)) return new BoxError(BoxError.BAD_FIELD, 'Subdomain cannot start or end with hyphen or dot', { field: 'location' });
|
||||
}
|
||||
|
||||
if (domainObject.config.hyphenatedSubdomains) {
|
||||
if (location.indexOf('.') !== -1) return new BoxError(BoxError.BAD_FIELD, 'Subdomain cannot contain a dot', { field: 'location' });
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -149,10 +139,9 @@ function validateTlsConfig(tlsConfig, dnsProvider) {
|
||||
case 'letsencrypt-prod':
|
||||
case 'letsencrypt-staging':
|
||||
case 'fallback':
|
||||
case 'caas':
|
||||
break;
|
||||
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) {
|
||||
@@ -313,6 +302,7 @@ function del(domain, auditSource, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (domain === settings.adminDomain()) return callback(new BoxError(BoxError.CONFLICT, 'Cannot remove admin domain'));
|
||||
if (domain === settings.mailDomain()) return callback(new BoxError(BoxError.CONFLICT, 'Cannot remove mail domain'));
|
||||
|
||||
domaindb.del(domain, function (error) {
|
||||
if (error) return callback(error);
|
||||
@@ -341,19 +331,7 @@ function getName(domain, location, type) {
|
||||
|
||||
if (location === '') return part;
|
||||
|
||||
if (!domain.config.hyphenatedSubdomains) return part ? `${location}.${part}` : location;
|
||||
|
||||
// hyphenatedSubdomains
|
||||
if (type !== 'TXT') return `${location}-${part}`;
|
||||
|
||||
if (location.startsWith('_acme-challenge.')) {
|
||||
return `${location}-${part}`;
|
||||
} else if (location === '_acme-challenge') {
|
||||
const up = part.replace(/^[^.]*\.?/, ''); // this gets the domain one level up
|
||||
return up ? `${location}.${up}` : location;
|
||||
} else {
|
||||
return `${location}.${part}`;
|
||||
}
|
||||
return part ? `${location}.${part}` : location;
|
||||
}
|
||||
|
||||
function getDnsRecords(location, domain, type, callback) {
|
||||
@@ -461,8 +439,7 @@ function removePrivateFields(domain) {
|
||||
function removeRestrictedFields(domain) {
|
||||
var result = _.pick(domain, 'domain', 'zoneName', 'provider');
|
||||
|
||||
// always ensure config object
|
||||
result.config = { hyphenatedSubdomains: !!domain.config.hyphenatedSubdomains };
|
||||
result.config = {}; // always ensure config object
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -474,33 +451,3 @@ function makeWildcard(hostname) {
|
||||
parts[0] = '*';
|
||||
return parts.join('.');
|
||||
}
|
||||
|
||||
function prepareDashboardDomain(domain, auditSource, progressCallback, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
get(domain, function (error, domainObject) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const adminFqdn = fqdn(constants.ADMIN_LOCATION, domainObject);
|
||||
|
||||
sysinfo.getServerIp(function (error, ip) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.series([
|
||||
(done) => { progressCallback({ percent: 10, message: `Updating DNS of ${adminFqdn}` }); done(); },
|
||||
upsertDnsRecords.bind(null, constants.ADMIN_LOCATION, domain, 'A', [ ip ]),
|
||||
(done) => { progressCallback({ percent: 40, message: `Waiting for DNS of ${adminFqdn}` }); done(); },
|
||||
waitForDnsRecord.bind(null, constants.ADMIN_LOCATION, domain, 'A', ip, { interval: 30000, times: 50000 }),
|
||||
(done) => { progressCallback({ percent: 70, message: `Getting certificate of ${adminFqdn}` }); done(); },
|
||||
reverseProxy.ensureCertificate.bind(null, fqdn(constants.ADMIN_LOCATION, domainObject), domain, auditSource)
|
||||
], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ exports = module.exports = {
|
||||
ACTION_DOMAIN_UPDATE: 'domain.update',
|
||||
ACTION_DOMAIN_REMOVE: 'domain.remove',
|
||||
|
||||
ACTION_MAIL_LOCATION: 'mail.location',
|
||||
ACTION_MAIL_ENABLED: 'mail.enabled',
|
||||
ACTION_MAIL_DISABLED: 'mail.disabled',
|
||||
ACTION_MAIL_MAILBOX_ADD: 'mail.box.add',
|
||||
@@ -60,6 +61,10 @@ exports = module.exports = {
|
||||
ACTION_USER_UPDATE: 'user.update',
|
||||
ACTION_USER_TRANSFER: 'user.transfer',
|
||||
|
||||
ACTION_VOLUME_ADD: 'volume.add',
|
||||
ACTION_VOLUME_UPDATE: 'volume.update',
|
||||
ACTION_VOLUME_REMOVE: 'volume.remove',
|
||||
|
||||
ACTION_DYNDNS_UPDATE: 'dyndns.update',
|
||||
|
||||
ACTION_SUPPORT_TICKET: 'support.ticket',
|
||||
|
||||
+35
-24
@@ -42,14 +42,14 @@ function translateUser(ldapConfig, ldapUser) {
|
||||
|
||||
return {
|
||||
username: ldapUser[ldapConfig.usernameField],
|
||||
email: ldapUser.mail,
|
||||
email: ldapUser.mail || ldapUser.mailPrimaryAddress,
|
||||
displayName: ldapUser.cn // user.giveName + ' ' + user.sn
|
||||
};
|
||||
}
|
||||
|
||||
function validUserRequirements(user) {
|
||||
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;
|
||||
} else {
|
||||
return true;
|
||||
@@ -57,8 +57,9 @@ function validUserRequirements(user) {
|
||||
}
|
||||
|
||||
// performs service bind if required
|
||||
function getClient(externalLdapConfig, callback) {
|
||||
function getClient(externalLdapConfig, doBindAuth, callback) {
|
||||
assert.strictEqual(typeof externalLdapConfig, 'object');
|
||||
assert.strictEqual(typeof doBindAuth, 'boolean');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// ensure we only callback once since we also have to listen to client.error events
|
||||
@@ -88,13 +89,14 @@ function getClient(externalLdapConfig, callback) {
|
||||
callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
|
||||
});
|
||||
|
||||
if (!externalLdapConfig.bindDn) return callback(null, client);
|
||||
// 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) {
|
||||
if (error instanceof ldap.InvalidCredentialsError) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
|
||||
|
||||
callback(null, client, externalLdapConfig);
|
||||
callback(null, client);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -103,7 +105,7 @@ function ldapGetByDN(externalLdapConfig, dn, callback) {
|
||||
assert.strictEqual(typeof dn, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getClient(externalLdapConfig, function (error, client) {
|
||||
getClient(externalLdapConfig, true, function (error, client) {
|
||||
if (error) return callback(error);
|
||||
|
||||
let searchOptions = {
|
||||
@@ -113,6 +115,9 @@ function ldapGetByDN(externalLdapConfig, dn, callback) {
|
||||
|
||||
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));
|
||||
@@ -140,7 +145,7 @@ function ldapUserSearch(externalLdapConfig, options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getClient(externalLdapConfig, function (error, client) {
|
||||
getClient(externalLdapConfig, true, function (error, client) {
|
||||
if (error) return callback(error);
|
||||
|
||||
let searchOptions = {
|
||||
@@ -181,7 +186,7 @@ function ldapGroupSearch(externalLdapConfig, options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getClient(externalLdapConfig, function (error, client) {
|
||||
getClient(externalLdapConfig, true, function (error, client) {
|
||||
if (error) return callback(error);
|
||||
|
||||
let searchOptions = {
|
||||
@@ -248,7 +253,7 @@ function testConfig(config, callback) {
|
||||
if (!config.groupnameField || typeof config.groupnameField !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'groupFilter must not be empty'));
|
||||
}
|
||||
|
||||
getClient(config, function (error, client) {
|
||||
getClient(config, true, function (error, client) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var opts = {
|
||||
@@ -305,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) {
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -332,12 +337,15 @@ function verifyPassword(user, password, callback) {
|
||||
if (ldapUsers.length === 0) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||
if (ldapUsers.length > 1) return callback(new BoxError(BoxError.CONFLICT));
|
||||
|
||||
let client = ldap.createClient({ url: externalLdapConfig.url });
|
||||
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));
|
||||
getClient(externalLdapConfig, false, function (error, client) {
|
||||
if (error) return callback(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]));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -391,7 +399,7 @@ function syncUsers(externalLdapConfig, progressCallback, callback) {
|
||||
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.message);
|
||||
if (error) debug('syncUsers: Failed to create user', user, error.message);
|
||||
iteratorCallback();
|
||||
});
|
||||
} else if (result.source !== 'ldap') {
|
||||
@@ -456,7 +464,7 @@ function syncGroups(externalLdapConfig, progressCallback, callback) {
|
||||
debug(`[adding group] groupname=${groupName}`);
|
||||
|
||||
groups.create(groupName, 'ldap', function (error) {
|
||||
if (error) console.error('Failed to create group', groupName, error);
|
||||
if (error) debug('syncGroups: Failed to create group', groupName, error);
|
||||
iteratorCallback();
|
||||
});
|
||||
} else {
|
||||
@@ -498,7 +506,7 @@ function syncGroupUsers(externalLdapConfig, progressCallback, callback) {
|
||||
ldapGroupSearch(externalLdapConfig, {}, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
if (!result || result.length === 0) {
|
||||
console.error(`Unable to find group ${group.name} ignoring for now.`);
|
||||
debug(`syncGroupUsers: Unable to find group ${group.name} ignoring for now.`);
|
||||
return callback();
|
||||
}
|
||||
|
||||
@@ -509,18 +517,21 @@ function syncGroupUsers(externalLdapConfig, progressCallback, callback) {
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
console.error(`Unable to find group ${group.name} ignoring for now.`);
|
||||
debug(`syncGroupUsers: Unable to find group ${group.name} ignoring for now.`);
|
||||
return callback();
|
||||
}
|
||||
|
||||
var ldapGroupMembers = found.member || [];
|
||||
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) {
|
||||
console.error(`Failed to get ${memberDn}:`, error);
|
||||
debug(`Failed to get ${memberDn}:`, error);
|
||||
return iteratorCallback();
|
||||
}
|
||||
|
||||
@@ -531,18 +542,18 @@ function syncGroupUsers(externalLdapConfig, progressCallback, callback) {
|
||||
|
||||
users.getByUsername(username, function (error, result) {
|
||||
if (error) {
|
||||
console.error(`Failed to get user by username ${username}`, 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) console.error('Failed to add member', error);
|
||||
if (error && error.reason !== BoxError.ALREADY_EXISTS) debug('syncGroupUsers: Failed to add member', error);
|
||||
iteratorCallback();
|
||||
});
|
||||
});
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) console.error(error);
|
||||
if (error) debug('syncGroupUsers: ', error);
|
||||
iteratorCallback();
|
||||
});
|
||||
});
|
||||
|
||||
+9
-3
@@ -5,6 +5,7 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
infra = require('./infra_version.js'),
|
||||
paths = require('./paths.js'),
|
||||
shell = require('./shell.js');
|
||||
@@ -15,6 +16,7 @@ function startGraphite(existingInfra, callback) {
|
||||
|
||||
const tag = infra.images.graphite.tag;
|
||||
const dataDir = paths.PLATFORM_DATA_DIR;
|
||||
const memoryLimit = 256;
|
||||
|
||||
if (existingInfra.version === infra.version && infra.images.graphite.tag === existingInfra.images.graphite.tag) return callback();
|
||||
|
||||
@@ -26,8 +28,8 @@ function startGraphite(existingInfra, callback) {
|
||||
--log-opt syslog-address=udp://127.0.0.1:2514 \
|
||||
--log-opt syslog-format=rfc5424 \
|
||||
--log-opt tag=graphite \
|
||||
-m 150m \
|
||||
--memory-swap 150m \
|
||||
-m ${memoryLimit}m \
|
||||
--memory-swap ${memoryLimit * 2}m \
|
||||
--dns 172.18.0.1 \
|
||||
--dns-search=. \
|
||||
-p 127.0.0.1:2003:2003 \
|
||||
@@ -37,5 +39,9 @@ function startGraphite(existingInfra, callback) {
|
||||
--label isCloudronManaged=true \
|
||||
--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);
|
||||
}
|
||||
|
||||
+1
-2
@@ -84,9 +84,8 @@ function getAll(callback) {
|
||||
function getAllWithMembers(callback) {
|
||||
database.query('SELECT ' + GROUPS_FIELDS + ',GROUP_CONCAT(groupMembers.userId) AS userIds ' +
|
||||
' 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 (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Group not found'));
|
||||
|
||||
results.forEach(function (result) { result.userIds = result.userIds ? result.userIds.split(',') : [ ]; });
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
exports = module.exports = {
|
||||
// a version change recreates all containers with latest docker config
|
||||
'version': '48.17.0',
|
||||
'version': '48.18.0',
|
||||
|
||||
'baseImages': [
|
||||
{ repo: 'cloudron/base', tag: 'cloudron/base:2.0.0@sha256:f9fea80513aa7c92fe2e7bf3978b54c8ac5222f47a9a32a7f8833edf0eb5a4f4' }
|
||||
@@ -16,12 +16,12 @@ exports = module.exports = {
|
||||
// docker inspect --format='{{index .RepoDigests 0}}' $IMAGE to get the sha256
|
||||
'images': {
|
||||
'turn': { repo: 'cloudron/turn', tag: 'cloudron/turn:1.1.0@sha256:e1dd22aa6eef5beb7339834b200a8bb787ffc2264ce11139857a054108fefb4f' },
|
||||
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:2.3.1@sha256:c1145d43c8a912fe6f5a5629a4052454a4aa6f23391c1efbffeec9d12d72a256' },
|
||||
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:2.2.1@sha256:430f3e8b700327d4afa03a7b4e10a8b5544f171e0946ead8cdc5b67ee32db8e4' },
|
||||
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:2.2.0@sha256:205486ff0f6bf6854610572df401cf3651bc62baf28fd26e9c5632497f10c2cb' },
|
||||
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:2.3.2@sha256:dd624870c7f8ba9b2759f93ce740d1e092a1ac4b2d6af5007a01b30ad6b316d0' },
|
||||
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:3.3.0@sha256:0daf1be5320c095077392bf21d247b93ceaddca46c866c17259a335c80d2f357' },
|
||||
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:3.0.0@sha256:59e50b1f55e433ffdf6d678f8c658812b4119f631db8325572a52ee40d3bc562' },
|
||||
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.3.0@sha256:0e31ec817e235b1814c04af97b1e7cf0053384aca2569570ce92bef0d95e94d2' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.9.4@sha256:0e169b97a0584a76197d2bbc039d8698bf93f815588b3b43c251bd83dd545465' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:3.0.1@sha256:ff24c70966937e8c3477d534bbb192e0364d3e9d6924ee0911278009d802b2b0' },
|
||||
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.3.0@sha256:b7bc1ca4f4d0603a01369a689129aa273a938ce195fe43d00d42f4f2d5212f50' },
|
||||
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:1.1.0@sha256:0c1fe4dd6121900624dcb383251ecb0084c3810e095064933de671409d8d6d7b' }
|
||||
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:3.0.0@sha256:7e0165f17789192fd4f92efb34aa373450fa859e3b502684b2b121a5582965bf' }
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
ipFromInt,
|
||||
intFromIp
|
||||
};
|
||||
|
||||
const assert = require('assert');
|
||||
|
||||
function intFromIp(address) {
|
||||
assert.strictEqual(typeof address, 'string');
|
||||
|
||||
const parts = address.split('.');
|
||||
|
||||
if (parts.length !== 4) return null;
|
||||
|
||||
return (parseInt(parts[0], 10) << (8*3)) & 0xFF000000 |
|
||||
(parseInt(parts[1], 10) << (8*2)) & 0x00FF0000 |
|
||||
(parseInt(parts[2], 10) << (8*1)) & 0x0000FF00 |
|
||||
(parseInt(parts[3], 10) << (8*0)) & 0x000000FF;
|
||||
}
|
||||
|
||||
function ipFromInt(input) {
|
||||
assert.strictEqual(typeof input, 'number');
|
||||
|
||||
let output = [];
|
||||
|
||||
for (let i = 3; i >= 0; --i) {
|
||||
const octet = (input >> (i*8)) & 0x000000FF;
|
||||
output.push(octet);
|
||||
}
|
||||
|
||||
return output.join('.');
|
||||
}
|
||||
|
||||
+5
-21
@@ -16,21 +16,15 @@ const NOOP_CALLBACK = function () { };
|
||||
|
||||
const gConnection = new Docker({ socketPath: '/var/run/docker.sock' });
|
||||
|
||||
function ignoreError(func) {
|
||||
return function (callback) {
|
||||
func(function (error) {
|
||||
if (error) console.error('Ignored error:', error);
|
||||
function cleanupTokens(callback) {
|
||||
assert(!callback || typeof callback === 'function'); // callback is null when called from cronjob
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
}
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
function cleanupExpiredTokens(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
debug('Cleaning up expired tokens');
|
||||
|
||||
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);
|
||||
|
||||
@@ -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) {
|
||||
assert.strictEqual(typeof containerInfo, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
+85
-28
@@ -5,7 +5,8 @@ exports = module.exports = {
|
||||
stop: stop
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
var addons = require('./addons.js'),
|
||||
assert = require('assert'),
|
||||
appdb = require('./appdb.js'),
|
||||
apps = require('./apps.js'),
|
||||
async = require('async'),
|
||||
@@ -13,6 +14,7 @@ var assert = require('assert'),
|
||||
constants = require('./constants.js'),
|
||||
debug = require('debug')('box:ldap'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
groups = require('./groups.js'),
|
||||
ldap = require('ldapjs'),
|
||||
mail = require('./mail.js'),
|
||||
mailboxdb = require('./mailboxdb.js'),
|
||||
@@ -132,8 +134,8 @@ function userSearch(req, res, next) {
|
||||
|
||||
var dn = ldap.parseDN('cn=' + user.id + ',ou=users,dc=cloudron');
|
||||
|
||||
var groups = [ GROUP_USERS_DN ];
|
||||
if (users.compareRoles(user.role, users.ROLE_ADMIN) >= 0) groups.push(GROUP_ADMINS_DN);
|
||||
var memberof = [ GROUP_USERS_DN ];
|
||||
if (users.compareRoles(user.role, users.ROLE_ADMIN) >= 0) memberof.push(GROUP_ADMINS_DN);
|
||||
|
||||
var displayName = user.displayName || user.username || ''; // displayName can be empty and username can be null
|
||||
var nameParts = displayName.split(' ');
|
||||
@@ -154,7 +156,7 @@ function userSearch(req, res, next) {
|
||||
givenName: firstName,
|
||||
username: user.username,
|
||||
samaccountname: user.username, // to support ActiveDirectory clients
|
||||
memberof: groups
|
||||
memberof: memberof
|
||||
}
|
||||
};
|
||||
|
||||
@@ -327,7 +329,9 @@ function mailboxSearch(req, res, next) {
|
||||
async.eachSeries(mailboxes, function (mailbox, callback) {
|
||||
var dn = ldap.parseDN(`cn=${mailbox.name}@${mailbox.domain},ou=mailboxes,dc=cloudron`);
|
||||
|
||||
users.get(mailbox.ownerId, function (error, userObject) {
|
||||
let getFunc = mailbox.ownerType === mail.OWNERTYPE_USER ? users.get : groups.get;
|
||||
|
||||
getFunc(mailbox.ownerId, function (error, ownerObject) {
|
||||
if (error) return callback(); // skip mailboxes with unknown owner
|
||||
|
||||
var obj = {
|
||||
@@ -335,7 +339,7 @@ function mailboxSearch(req, res, next) {
|
||||
attributes: {
|
||||
objectclass: ['mailbox'],
|
||||
objectcategory: 'mailbox',
|
||||
displayname: userObject.displayName,
|
||||
displayname: mailbox.ownerType === mail.OWNERTYPE_USER ? ownerObject.displayName : ownerObject.name,
|
||||
cn: `${mailbox.name}@${mailbox.domain}`,
|
||||
uid: `${mailbox.name}@${mailbox.domain}`,
|
||||
mail: `${mailbox.name}@${mailbox.domain}`
|
||||
@@ -346,7 +350,7 @@ function mailboxSearch(req, res, next) {
|
||||
if (error) return callback(error);
|
||||
|
||||
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
|
||||
@@ -482,11 +486,11 @@ function authorizeUserForApp(req, res, next) {
|
||||
assert.strictEqual(typeof req.user, 'object');
|
||||
assert.strictEqual(typeof req.app, 'object');
|
||||
|
||||
apps.hasAccessTo(req.app, req.user, function (error, result) {
|
||||
apps.hasAccessTo(req.app, req.user, function (error, hasAccess) {
|
||||
if (error) return next(new ldap.OperationsError(error.toString()));
|
||||
|
||||
// we return no such object, to avoid leakage of a users existence
|
||||
if (!result) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
if (!hasAccess) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: req.app.id }, { userId: req.user.id, user: users.removePrivateFields(req.user) });
|
||||
|
||||
@@ -494,6 +498,30 @@ function authorizeUserForApp(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function verifyMailboxPassword(mailbox, password, callback) {
|
||||
assert.strictEqual(typeof mailbox, 'object');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (mailbox.ownerType === mail.OWNERTYPE_USER) return users.verify(mailbox.ownerId, password, users.AP_MAIL /* identifier */, callback);
|
||||
|
||||
groups.getMembers(mailbox.ownerId, function (error, userIds) {
|
||||
if (error) return callback(error);
|
||||
|
||||
let verifiedUser = null;
|
||||
async.someSeries(userIds, function iterator(userId, iteratorDone) {
|
||||
users.verify(userId, password, users.AP_MAIL /* identifier */, function (error, result) {
|
||||
if (error) return iteratorDone(null, false);
|
||||
verifiedUser = result;
|
||||
iteratorDone(null, true);
|
||||
});
|
||||
}, function (error, result) {
|
||||
if (!result) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||
callback(null, verifiedUser);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function authenticateUserMailbox(req, res, next) {
|
||||
debug('user mailbox auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
|
||||
|
||||
@@ -513,7 +541,7 @@ function authenticateUserMailbox(req, res, next) {
|
||||
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
if (error) return next(new ldap.OperationsError(error.message));
|
||||
|
||||
users.verify(mailbox.ownerId, req.credentials || '', users.AP_MAIL, function (error, result) {
|
||||
verifyMailboxPassword(mailbox, req.credentials || '', function (error, result) {
|
||||
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
|
||||
if (error) return next(new ldap.OperationsError(error.message));
|
||||
@@ -547,6 +575,16 @@ function authenticateSftp(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function loadSftpConfig(req, res, next) {
|
||||
addons.getServicesConfig('sftp', function (error, service, servicesConfig) {
|
||||
if (error) return next(new ldap.OperationsError(error.toString()));
|
||||
|
||||
req.requireAdmin = servicesConfig['sftp'].requireAdmin;
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
function userSearchSftp(req, res, next) {
|
||||
debug('sftp user search: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id);
|
||||
|
||||
@@ -570,6 +608,8 @@ function userSearchSftp(req, res, next) {
|
||||
users.getByUsername(username, function (error, user) {
|
||||
if (error) return next(new ldap.OperationsError(error.toString()));
|
||||
|
||||
if (req.requireAdmin && users.compareRoles(user.role, users.ROLE_ADMIN) < 0) return next(new ldap.InsufficientAccessRightsError('Insufficient previleges'));
|
||||
|
||||
apps.hasAccessTo(app, user, function (error, hasAccess) {
|
||||
if (error) return next(new ldap.OperationsError(error.toString()));
|
||||
if (!hasAccess) return next(new ldap.InsufficientAccessRightsError('Not authorized'));
|
||||
@@ -577,7 +617,7 @@ function userSearchSftp(req, res, next) {
|
||||
var obj = {
|
||||
dn: ldap.parseDN(`cn=${username}@${appFqdn},ou=sftp,dc=cloudron`).toString(),
|
||||
attributes: {
|
||||
homeDirectory: path.join('/app/data', app.id, 'data'),
|
||||
homeDirectory: path.join('/app/data', app.id),
|
||||
objectclass: ['user'],
|
||||
objectcategory: 'person',
|
||||
cn: user.id,
|
||||
@@ -593,16 +633,37 @@ function userSearchSftp(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function verifyAppMailboxPassword(addonId, username, password, callback) {
|
||||
assert.strictEqual(typeof addonId, 'string');
|
||||
assert.strictEqual(typeof username, 'string');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const pattern = addonId === 'sendmail' ? 'MAIL_SMTP' : 'MAIL_IMAP';
|
||||
appdb.getAppIdByAddonConfigValue(addonId, `%${pattern}_PASSWORD`, password, function (error, appId) { // search by password because this is unique for each app
|
||||
if (error) return callback(error);
|
||||
|
||||
appdb.getAddonConfig(appId, addonId, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (!result.some(r => r.name.endsWith(`${pattern}_USERNAME`) && r.value === username)) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function authenticateMailAddon(req, res, next) {
|
||||
debug('mail addon auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
|
||||
|
||||
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
var email = req.dn.rdns[0].attrs.cn.value.toLowerCase();
|
||||
var parts = email.split('@');
|
||||
const email = req.dn.rdns[0].attrs.cn.value.toLowerCase();
|
||||
const parts = email.split('@');
|
||||
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
const addonId = req.dn.rdns[1].attrs.ou.value.toLowerCase(); // 'sendmail' or 'recvmail'
|
||||
if (addonId !== 'sendmail' && addonId !== 'recvmail') return next(new ldap.OperationsError('Invalid DN'));
|
||||
|
||||
mail.getDomain(parts[1], function (error, domain) {
|
||||
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
@@ -610,21 +671,17 @@ function authenticateMailAddon(req, res, next) {
|
||||
|
||||
if (addonId === 'recvmail' && !domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
let namePattern; // manifest v2 has a CLOUDRON_ prefix for names
|
||||
if (addonId === 'sendmail') namePattern = '%MAIL_SMTP_PASSWORD';
|
||||
else if (addonId === 'recvmail') namePattern = '%MAIL_IMAP_PASSWORD';
|
||||
else return next(new ldap.OperationsError('Invalid DN'));
|
||||
verifyAppMailboxPassword(addonId, email, req.credentials || '', function (error) {
|
||||
if (!error) return res.end(); // validated as app
|
||||
|
||||
// note: with sendmail addon, apps can send mail without a mailbox (unlike users)
|
||||
appdb.getAppIdByAddonConfigValue(addonId, namePattern, req.credentials || '', function (error, appId) {
|
||||
if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
|
||||
if (error && error.reason !== BoxError.NOT_FOUND) return next(new ldap.OperationsError(error.message));
|
||||
if (appId) return res.end();
|
||||
|
||||
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) return next(new ldap.OperationsError(error.message));
|
||||
|
||||
users.verify(mailbox.ownerId, req.credentials || '', users.AP_MAIL, function (error, result) {
|
||||
verifyMailboxPassword(mailbox, req.credentials || '', function (error, result) {
|
||||
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
|
||||
if (error) return next(new ldap.OperationsError(error.message));
|
||||
@@ -645,14 +702,14 @@ function start(callback) {
|
||||
debug: NOOP,
|
||||
info: debug,
|
||||
warn: debug,
|
||||
error: console.error,
|
||||
fatal: console.error
|
||||
error: debug,
|
||||
fatal: debug
|
||||
};
|
||||
|
||||
gServer = ldap.createServer({ log: logger });
|
||||
|
||||
gServer.on('error', function (error) {
|
||||
console.error('LDAP:', error);
|
||||
debug('start: server error ', error);
|
||||
});
|
||||
|
||||
gServer.search('ou=users,dc=cloudron', authenticateApp, userSearch);
|
||||
@@ -660,16 +717,16 @@ function start(callback) {
|
||||
gServer.bind('ou=users,dc=cloudron', authenticateApp, authenticateUser, authorizeUserForApp);
|
||||
|
||||
// http://www.ietf.org/proceedings/43/I-D/draft-srivastava-ldap-mail-00.txt
|
||||
gServer.search('ou=mailboxes,dc=cloudron', mailboxSearch); // haraka, dovecot
|
||||
gServer.search('ou=mailboxes,dc=cloudron', mailboxSearch); // haraka (address translation), dovecot (LMTP), sogo (mailbox search)
|
||||
gServer.bind('ou=mailboxes,dc=cloudron', authenticateUserMailbox); // apps like sogo can use domain=${domain} to authenticate a mailbox
|
||||
gServer.search('ou=mailaliases,dc=cloudron', mailAliasSearch); // haraka
|
||||
gServer.search('ou=mailinglists,dc=cloudron', mailingListSearch); // haraka
|
||||
|
||||
gServer.bind('ou=recvmail,dc=cloudron', authenticateMailAddon); // dovecot
|
||||
gServer.bind('ou=sendmail,dc=cloudron', authenticateMailAddon); // haraka
|
||||
gServer.bind('ou=recvmail,dc=cloudron', authenticateMailAddon); // dovecot (IMAP auth)
|
||||
gServer.bind('ou=sendmail,dc=cloudron', authenticateMailAddon); // haraka (MSA auth)
|
||||
|
||||
gServer.bind('ou=sftp,dc=cloudron', authenticateSftp); // sftp
|
||||
gServer.search('ou=sftp,dc=cloudron', userSearchSftp);
|
||||
gServer.search('ou=sftp,dc=cloudron', loadSftpConfig, userSearchSftp);
|
||||
|
||||
gServer.compare('cn=users,ou=groups,dc=cloudron', authenticateApp, groupUsersCompare);
|
||||
gServer.compare('cn=admins,ou=groups,dc=cloudron', authenticateApp, groupAdminsCompare);
|
||||
|
||||
+210
-73
@@ -1,59 +1,69 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
getStatus: getStatus,
|
||||
checkConfiguration: checkConfiguration,
|
||||
getStatus,
|
||||
checkConfiguration,
|
||||
|
||||
getDomains: getDomains,
|
||||
getLocation,
|
||||
setLocation, // triggers the change task
|
||||
changeLocation, // does the actual changing
|
||||
|
||||
getDomain: getDomain,
|
||||
clearDomains: clearDomains,
|
||||
getDomains,
|
||||
|
||||
onDomainAdded: onDomainAdded,
|
||||
onDomainRemoved: onDomainRemoved,
|
||||
getDomain,
|
||||
clearDomains,
|
||||
|
||||
removePrivateFields: removePrivateFields,
|
||||
onDomainAdded,
|
||||
onDomainRemoved,
|
||||
|
||||
setDnsRecords: setDnsRecords,
|
||||
onMailFqdnChanged: onMailFqdnChanged,
|
||||
removePrivateFields,
|
||||
|
||||
validateName: validateName,
|
||||
setDnsRecords,
|
||||
|
||||
setMailFromValidation: setMailFromValidation,
|
||||
setCatchAllAddress: setCatchAllAddress,
|
||||
setMailRelay: setMailRelay,
|
||||
setMailEnabled: setMailEnabled,
|
||||
validateName,
|
||||
|
||||
setMailFromValidation,
|
||||
setCatchAllAddress,
|
||||
setMailRelay,
|
||||
setMailEnabled,
|
||||
setBanner,
|
||||
|
||||
startMail: restartMail,
|
||||
restartMail: restartMail,
|
||||
handleCertChanged: handleCertChanged,
|
||||
getMailAuth: getMailAuth,
|
||||
restartMail,
|
||||
handleCertChanged,
|
||||
getMailAuth,
|
||||
|
||||
sendTestMail: sendTestMail,
|
||||
sendTestMail,
|
||||
|
||||
listMailboxes: listMailboxes,
|
||||
removeMailboxes: removeMailboxes,
|
||||
getMailbox: getMailbox,
|
||||
addMailbox: addMailbox,
|
||||
updateMailboxOwner: updateMailboxOwner,
|
||||
removeMailbox: removeMailbox,
|
||||
getMailboxCount,
|
||||
listMailboxes,
|
||||
getMailbox,
|
||||
addMailbox,
|
||||
updateMailboxOwner,
|
||||
removeMailbox,
|
||||
|
||||
getAliases: getAliases,
|
||||
setAliases: setAliases,
|
||||
getAliases,
|
||||
setAliases,
|
||||
|
||||
getLists: getLists,
|
||||
getList: getList,
|
||||
addList: addList,
|
||||
updateList: updateList,
|
||||
removeList: removeList,
|
||||
resolveList: resolveList,
|
||||
getLists,
|
||||
getList,
|
||||
addList,
|
||||
updateList,
|
||||
removeList,
|
||||
resolveList,
|
||||
|
||||
OWNERTYPE_USER: 'user',
|
||||
OWNERTYPE_GROUP: 'group',
|
||||
|
||||
_removeMailboxes: removeMailboxes,
|
||||
_readDkimPublicKeySync: readDkimPublicKeySync
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
var addons = require('./addons.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
cloudron = require('./cloudron.js'),
|
||||
constants = require('./constants.js'),
|
||||
debug = require('debug')('box:mail'),
|
||||
dns = require('./native-dns.js'),
|
||||
@@ -69,18 +79,21 @@ var assert = require('assert'),
|
||||
nodemailer = require('nodemailer'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
request = require('request'),
|
||||
reverseProxy = require('./reverseproxy.js'),
|
||||
safe = require('safetydance'),
|
||||
settings = require('./settings.js'),
|
||||
shell = require('./shell.js'),
|
||||
smtpTransport = require('nodemailer-smtp-transport'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
tasks = require('./tasks.js'),
|
||||
users = require('./users.js'),
|
||||
validator = require('validator'),
|
||||
_ = require('underscore');
|
||||
|
||||
const DNS_OPTIONS = { timeout: 5000 };
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
const REMOVE_MAILBOX = path.join(__dirname, 'scripts/rmmailbox.sh');
|
||||
|
||||
function validateName(name) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
@@ -100,7 +113,6 @@ function checkOutboundPort25(callback) {
|
||||
var smtpServer = _.sample([
|
||||
'smtp.gmail.com',
|
||||
'smtp.live.com',
|
||||
'smtp.mail.yahoo.com',
|
||||
'smtp.1und1.de',
|
||||
]);
|
||||
|
||||
@@ -237,14 +249,14 @@ function checkSpf(domain, mailFqdn, callback) {
|
||||
let txtRecord = txtRecords[i].join(''); // https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
|
||||
if (txtRecord.indexOf('v=spf1 ') !== 0) continue; // not SPF
|
||||
spf.value = txtRecord;
|
||||
spf.status = spf.value.indexOf(' a:' + settings.adminFqdn()) !== -1;
|
||||
spf.status = spf.value.indexOf(' a:' + settings.mailFqdn()) !== -1;
|
||||
break;
|
||||
}
|
||||
|
||||
if (spf.status) {
|
||||
spf.expected = spf.value;
|
||||
} else if (i !== txtRecords.length) {
|
||||
spf.expected = 'v=spf1 a:' + settings.adminFqdn() + ' ' + spf.value.slice('v=spf1 '.length);
|
||||
spf.expected = 'v=spf1 a:' + settings.mailFqdn() + ' ' + spf.value.slice('v=spf1 '.length);
|
||||
}
|
||||
|
||||
callback(null, spf);
|
||||
@@ -543,7 +555,7 @@ function checkConfiguration(callback) {
|
||||
markdownMessage += '\n\n';
|
||||
});
|
||||
|
||||
if (markdownMessage) markdownMessage += 'Email Status is checked every 30 minutes.\n See the [troubleshooting docs](https://cloudron.io/documentation/troubleshooting/#mail-dns) for more information.\n';
|
||||
if (markdownMessage) markdownMessage += 'Email Status is checked every 30 minutes.\n See the [troubleshooting docs](https://docs.cloudron.io/troubleshooting/#mail-dns) for more information.\n';
|
||||
|
||||
callback(null, markdownMessage); // empty message means all status checks succeeded
|
||||
});
|
||||
@@ -575,15 +587,18 @@ function createMailConfig(mailFqdn, mailDomain, callback) {
|
||||
}
|
||||
|
||||
// create sections for per-domain configuration
|
||||
mailDomains.forEach(function (domain) {
|
||||
async.eachSeries(mailDomains, function (domain, iteratorDone) {
|
||||
const catchAll = domain.catchAll.map(function (c) { return `${c}@${domain.domain}`; }).join(',');
|
||||
const mailFromValidation = domain.mailFromValidation;
|
||||
|
||||
if (!safe.fs.appendFileSync(path.join(paths.ADDON_CONFIG_DIR, 'mail/mail.ini'),
|
||||
`[${domain.domain}]\ncatch_all=${catchAll}\nmail_from_validation=${mailFromValidation}\n\n`, 'utf8')) {
|
||||
return callback(new BoxError(BoxError.FS_ERROR, 'Could not create mail var file:' + safe.error.message));
|
||||
return iteratorDone(new BoxError(BoxError.FS_ERROR, 'Could not create mail var file:' + safe.error.message));
|
||||
}
|
||||
|
||||
if (!safe.fs.writeFileSync(`${paths.ADDON_CONFIG_DIR}/mail/banner/${domain.domain}.text`, domain.banner.text || '')) return iteratorDone(new BoxError(BoxError.FS_ERROR, 'Could not create text banner file:' + safe.error.message));
|
||||
if (!safe.fs.writeFileSync(`${paths.ADDON_CONFIG_DIR}/mail/banner/${domain.domain}.html`, domain.banner.html || '')) return iteratorDone(new BoxError(BoxError.FS_ERROR, 'Could not create html banner file:' + safe.error.message));
|
||||
|
||||
const relay = domain.relay;
|
||||
|
||||
const enableRelay = relay.provider !== 'cloudron-smtp' && relay.provider !== 'noop',
|
||||
@@ -593,15 +608,19 @@ function createMailConfig(mailFqdn, mailDomain, callback) {
|
||||
username = relay.username || '',
|
||||
password = relay.password || '';
|
||||
|
||||
if (!enableRelay) return;
|
||||
if (!enableRelay) return iteratorDone();
|
||||
|
||||
if (!safe.fs.appendFileSync(paths.ADDON_CONFIG_DIR + '/mail/smtp_forward.ini',
|
||||
`[${domain.domain}]\nenable_outbound=true\nhost=${host}\nport=${port}\nenable_tls=true\nauth_type=${authType}\nauth_user=${username}\nauth_pass=${password}\n\n`, 'utf8')) {
|
||||
return callback(new BoxError(BoxError.FS_ERROR, 'Could not create mail var file:' + safe.error.message));
|
||||
return iteratorDone(new BoxError(BoxError.FS_ERROR, 'Could not create mail var file:' + safe.error.message));
|
||||
}
|
||||
});
|
||||
|
||||
callback(null, mailInDomains.length !== 0 /* allowInbound */);
|
||||
iteratorDone();
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, mailInDomains.length !== 0 /* allowInbound */);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -629,7 +648,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.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);
|
||||
|
||||
createMailConfig(mailFqdn, mailDomain, function (error, allowInbound) {
|
||||
@@ -746,7 +768,7 @@ function txtRecordsWithSpf(domain, mailFqdn, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
domains.getDnsRecords('', domain, 'TXT', function (error, txtRecords) {
|
||||
if (error) return error;
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('txtRecordsWithSpf: current txt records - %j', txtRecords);
|
||||
|
||||
@@ -887,21 +909,73 @@ function setDnsRecords(domain, callback) {
|
||||
upsertDnsRecords(domain, settings.mailFqdn(), callback);
|
||||
}
|
||||
|
||||
function onMailFqdnChanged(callback) {
|
||||
function getLocation(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const mailFqdn = settings.mailFqdn(),
|
||||
mailDomain = settings.adminDomain();
|
||||
const domain = settings.mailDomain(), fqdn = settings.mailFqdn();
|
||||
const subdomain = fqdn.substr(0, fqdn.length - domain.length - 1);
|
||||
|
||||
domains.getAll(function (error, allDomains) {
|
||||
callback(null, { domain, subdomain });
|
||||
}
|
||||
|
||||
function changeLocation(auditSource, progressCallback, callback) {
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const fqdn = settings.mailFqdn(), domain = settings.mailDomain();
|
||||
const subdomain = fqdn.substr(0, fqdn.length - domain.length - 1);
|
||||
|
||||
let progress = 20;
|
||||
progressCallback({ percent: progress, message: `Setting up DNS of certs of mail server ${fqdn}` });
|
||||
|
||||
cloudron.setupDnsAndCert(subdomain, domain, auditSource, progressCallback, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.eachOfSeries(allDomains, function (domainObject, idx, iteratorDone) {
|
||||
upsertDnsRecords(domainObject.domain, mailFqdn, iteratorDone);
|
||||
}, function (error) {
|
||||
domains.getAll(function (error, allDomains) {
|
||||
if (error) return callback(error);
|
||||
|
||||
configureMail(mailFqdn, mailDomain, callback);
|
||||
async.eachOfSeries(allDomains, function (domainObject, idx, iteratorDone) {
|
||||
progressCallback({ percent: progress, message: `Updating DNS of ${domainObject.domain}` });
|
||||
progress += Math.round(70/allDomains.length);
|
||||
|
||||
upsertDnsRecords(domainObject.domain, fqdn, function (error) { // ignore any errors. we anyway report dns errors in status tab
|
||||
progressCallback({ percent: progress, message: `Updated DNS of ${domainObject.domain}: ${error ? error.message : 'success'}` });
|
||||
iteratorDone();
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
progressCallback({ percent: 90, message: 'Restarting mail server' });
|
||||
|
||||
restartMailIfActivated(callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setLocation(subdomain, domain, auditSource, callback) {
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
domains.get(domain, function (error, domainObject) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const fqdn = domains.fqdn(subdomain, domainObject);
|
||||
|
||||
settings.setMailLocation(domain, fqdn, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
tasks.add(tasks.TASK_CHANGE_MAIL_LOCATION, [ auditSource ], function (error, taskId) {
|
||||
if (error) return callback(error);
|
||||
|
||||
tasks.startTask(taskId, {}, NOOP_CALLBACK);
|
||||
eventlog.add(eventlog.ACTION_MAIL_LOCATION, auditSource, { subdomain, domain, taskId });
|
||||
|
||||
callback(null, taskId);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -910,6 +984,8 @@ function onDomainAdded(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!settings.mailFqdn()) return callback(); // mail domain is not set yet (when provisioning)
|
||||
|
||||
async.series([
|
||||
upsertDnsRecords.bind(null, domain, settings.mailFqdn()), // do this first to ensure DKIM keys
|
||||
restartMailIfActivated
|
||||
@@ -935,7 +1011,7 @@ function clearDomains(callback) {
|
||||
|
||||
// remove all fields that should never be sent out via REST API
|
||||
function removePrivateFields(domain) {
|
||||
let result = _.pick(domain, 'domain', 'enabled', 'mailFromValidation', 'catchAll', 'relay');
|
||||
let result = _.pick(domain, 'domain', 'enabled', 'mailFromValidation', 'catchAll', 'relay', 'banner');
|
||||
if (result.relay.provider !== 'cloudron-smtp') {
|
||||
if (result.relay.username === result.relay.password) result.relay.username = constants.SECRET_PLACEHOLDER;
|
||||
result.relay.password = constants.SECRET_PLACEHOLDER;
|
||||
@@ -957,6 +1033,20 @@ function setMailFromValidation(domain, enabled, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function setBanner(domain, banner, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof banner, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
maildb.update(domain, { banner }, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
restartMail(NOOP_CALLBACK);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function setCatchAllAddress(domain, addresses, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert(Array.isArray(addresses));
|
||||
@@ -1032,13 +1122,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(typeof search === 'string' || search === null);
|
||||
assert.strictEqual(typeof page, 'number');
|
||||
assert.strictEqual(typeof perPage, 'number');
|
||||
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);
|
||||
|
||||
callback(null, result);
|
||||
@@ -1068,10 +1170,11 @@ function getMailbox(name, domain, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function addMailbox(name, domain, userId, auditSource, callback) {
|
||||
function addMailbox(name, domain, ownerId, ownerType, auditSource, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof ownerId, 'string');
|
||||
assert.strictEqual(typeof ownerType, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -1080,49 +1183,80 @@ function addMailbox(name, domain, userId, auditSource, callback) {
|
||||
var error = validateName(name);
|
||||
if (error) return callback(error);
|
||||
|
||||
mailboxdb.addMailbox(name, domain, userId, function (error) {
|
||||
if (ownerType !== exports.OWNERTYPE_USER && ownerType !== exports.OWNERTYPE_GROUP) return callback(new BoxError(BoxError.BAD_FIELD, 'bad owner type'));
|
||||
|
||||
mailboxdb.addMailbox(name, domain, ownerId, ownerType, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_ADD, auditSource, { name, domain, userId });
|
||||
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_ADD, auditSource, { name, domain, ownerId, ownerType });
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function updateMailboxOwner(name, domain, userId, auditSource, callback) {
|
||||
function updateMailboxOwner(name, domain, ownerId, ownerType, auditSource, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof ownerId, 'string');
|
||||
assert.strictEqual(typeof ownerType, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
name = name.toLowerCase();
|
||||
|
||||
if (ownerType !== exports.OWNERTYPE_USER && ownerType !== exports.OWNERTYPE_GROUP) return callback(new BoxError(BoxError.BAD_FIELD, 'bad owner type'));
|
||||
|
||||
getMailbox(name, domain, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
mailboxdb.updateMailboxOwner(name, domain, userId, function (error) {
|
||||
mailboxdb.updateMailboxOwner(name, domain, ownerId, ownerType, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_UPDATE, auditSource, { name, domain, oldUserId: result.userId, userId });
|
||||
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_UPDATE, auditSource, { name, domain, oldUserId: result.userId, ownerId, ownerType });
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function removeMailbox(name, domain, auditSource, callback) {
|
||||
function removeSolrIndex(mailbox, callback) {
|
||||
assert.strictEqual(typeof mailbox, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
addons.getContainerDetails('mail', 'CLOUDRON_MAIL_TOKEN', function (error, addonDetails) {
|
||||
if (error) return callback(error);
|
||||
|
||||
request.post(`https://${addonDetails.ip}:3000/solr_delete_index?access_token=${addonDetails.token}`, { timeout: 2000, rejectUnauthorized: false, json: { mailbox } }, function (error, response) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (response.statusCode !== 200) return callback(new Error(`Error removing solr index - ${response.statusCode} ${JSON.stringify(response.body)}`));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function removeMailbox(name, domain, options, auditSource, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
mailboxdb.del(name, domain, function (error) {
|
||||
if (error) return callback(error);
|
||||
const mailbox =`${name}@${domain}`;
|
||||
const deleteMailFunc = options.deleteMails ? shell.sudo.bind(null, 'removeMailbox', [ REMOVE_MAILBOX, mailbox ], {}) : (next) => next();
|
||||
|
||||
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);
|
||||
|
||||
removeSolrIndex(mailbox, NOOP_CALLBACK);
|
||||
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_REMOVE, auditSource, { name, domain });
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1165,11 +1299,14 @@ function setAliases(name, domain, aliases, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getLists(domain, callback) {
|
||||
function getLists(domain, search, page, perPage, callback) {
|
||||
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');
|
||||
|
||||
mailboxdb.getLists(domain, function (error, result) {
|
||||
mailboxdb.getLists(domain, search, page, perPage, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, result);
|
||||
@@ -1296,7 +1433,7 @@ function resolveList(listName, listDomain, callback) {
|
||||
if (entry.type === mailboxdb.TYPE_MAILBOX) { // concrete mailbox
|
||||
result.push(member);
|
||||
} else if (entry.type === mailboxdb.TYPE_ALIAS) { // resolve aliases
|
||||
toResolve = toResolve.concat(`${entry.aliasName}@${entry.aliasTarget}`);
|
||||
toResolve = toResolve.concat(`${entry.aliasName}@${entry.aliasDomain}`);
|
||||
} else { // resolve list members
|
||||
toResolve = toResolve.concat(entry.members);
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ The application '<%= title %>' installed at <%= appFqdn %> is not responding.
|
||||
This is most likely a problem in the application.
|
||||
|
||||
To resolve this, you can try the following:
|
||||
* Restart the app by opening the app's web terminal - https://cloudron.io/documentation/apps/#web-terminal
|
||||
* Restore the app to the latest backup - https://cloudron.io/documentation/backups/#restoring-an-app
|
||||
* Restart the app by opening the app's web terminal - https://docs.cloudron.io/apps/#web-terminal
|
||||
* Restore the app to the latest backup - https://docs.cloudron.io/backups/#restoring-an-app
|
||||
* Contact us via <%= supportEmail %> or https://forum.cloudron.io
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Dear <%= cloudronName %> Admin,
|
||||
|
||||
Cloudron failed to create a complete backup. Please see https://cloudron.io/documentation/troubleshooting/#backups
|
||||
Cloudron failed to create a complete backup. Please see https://docs.cloudron.io/troubleshooting/#backups
|
||||
for troubleshooting.
|
||||
|
||||
Logs for this failure are available at <%= logUrl %>
|
||||
|
||||
@@ -8,7 +8,7 @@ The Cloudron will attempt to renew the certificate every 12 hours
|
||||
until the certificate expires (at which point it will switch to
|
||||
using the fallback certificate).
|
||||
|
||||
See https://cloudron.io/documentation/troubleshooting/#certificates to
|
||||
See https://docs.cloudron.io/troubleshooting/#certificates to
|
||||
double check if your server is configured correctly to obtain certificates
|
||||
via Let's Encrypt.
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ Dear <%= cloudronName %> Admin,
|
||||
|
||||
If this message appears repeatedly, give the app more memory.
|
||||
|
||||
* 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 an app's memory limit - https://docs.cloudron.io/apps/#memory-limit
|
||||
* To increase a service's memory limit - https://docs.cloudron.io/troubleshooting/#services
|
||||
|
||||
Out of memory event:
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<center>
|
||||
|
||||
<img src="<%= cloudronAvatarUrl %>" width="128px" height="128px"/>
|
||||
|
||||
<h3>{{ passwordResetEmail.salutation }}</h3>
|
||||
|
||||
<p>{{ passwordResetEmail.description }}</p>
|
||||
|
||||
<p>
|
||||
<a href="<%= resetLink %>">{{ passwordResetEmail.resetAction }}</a>
|
||||
</p>
|
||||
|
||||
<br/>
|
||||
|
||||
{{ passwordResetEmail.expireNote }}
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<div style="font-size: 10px; color: #333333; background: #ffffff;">
|
||||
Powered by <a href="https://cloudron.io">Cloudron</a>
|
||||
</div>
|
||||
|
||||
</center>
|
||||
@@ -0,0 +1,9 @@
|
||||
{{ passwordResetEmail.salutation }}
|
||||
|
||||
{{ passwordResetEmail.description }}
|
||||
|
||||
{{ passwordResetEmail.resetActionText }}
|
||||
|
||||
{{ passwordResetEmail.expireNote }}
|
||||
|
||||
Powered by https://cloudron.io
|
||||
@@ -1,41 +0,0 @@
|
||||
<%if (format === 'text') { %>
|
||||
|
||||
Hi <%= user.displayName || user.username || user.email %>,
|
||||
|
||||
Someone, hopefully you, has requested your account's password
|
||||
be reset. If you did not request this reset, please ignore this message.
|
||||
|
||||
To reset your password, please visit the following page:
|
||||
<%- resetLink %>
|
||||
|
||||
|
||||
|
||||
Powered by https://cloudron.io
|
||||
|
||||
<% } else { %>
|
||||
|
||||
<center>
|
||||
|
||||
<img src="<%= cloudronAvatarUrl %>" width="128px" height="128px"/>
|
||||
|
||||
<h3>Hi <%= user.displayName || user.username || user.email %>,</h3>
|
||||
|
||||
<p>
|
||||
Someone, hopefully you, has requested your account's password be reset.<br/>
|
||||
If you did not request this reset, please ignore this message.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="<%= resetLink %>">Click to reset your password</a>
|
||||
</p>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<div style="font-size: 10px; color: #333333; background: #ffffff;">
|
||||
Powered by <a href="https://cloudron.io">Cloudron</a>
|
||||
</div>
|
||||
|
||||
</center>
|
||||
|
||||
<% } %>
|
||||
@@ -0,0 +1,28 @@
|
||||
<center>
|
||||
|
||||
<img src="<%= cloudronAvatarUrl %>" width="128px" height="128px"/>
|
||||
|
||||
<h3>{{ welcomeEmail.salutation }}</h3>
|
||||
<h2>{{ welcomeEmail.welcomeTo }}</h2>
|
||||
|
||||
<p>
|
||||
<a href="<%= inviteLink %>">{{ welcomeEmail.inviteLinkAction }}</a>
|
||||
</p>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<div style="font-size: 10px; color: #333333; background: #ffffff;">
|
||||
<% if (invitor) { -%>
|
||||
{{ welcomeEmail.invitor }}
|
||||
<% } -%>
|
||||
|
||||
<br/>
|
||||
|
||||
{{ welcomeEmail.expireNote }}
|
||||
<br/>
|
||||
|
||||
Powered by <a href="https://cloudron.io">Cloudron</a>
|
||||
</div>
|
||||
|
||||
</center>
|
||||
@@ -0,0 +1,13 @@
|
||||
{{ welcomeEmail.salutation }}
|
||||
|
||||
{{ welcomeEmail.welcomeTo }}
|
||||
|
||||
{{ welcomeEmail.inviteLinkActionText }}
|
||||
|
||||
<% if (invitor) { %>
|
||||
{{ welcomeEmail.invitor }}
|
||||
<% } %>
|
||||
|
||||
{{ welcomeEmail.expireNote }}
|
||||
|
||||
Powered by https://cloudron.io
|
||||
@@ -1,46 +0,0 @@
|
||||
<%if (format === 'text') { %>
|
||||
|
||||
Dear <%= user.displayName || user.username || user.email %>,
|
||||
|
||||
Welcome to <%= cloudronName %>!
|
||||
|
||||
Follow the link to get started.
|
||||
<%- inviteLink %>
|
||||
|
||||
<% if (invitor && invitor.email) { %>
|
||||
You are receiving this email because you were invited by <%= invitor.email %>.
|
||||
<% } %>
|
||||
|
||||
|
||||
Powered by https://cloudron.io
|
||||
|
||||
<% } else { %>
|
||||
|
||||
<center>
|
||||
|
||||
<img src="<%= cloudronAvatarUrl %>" width="128px" height="128px"/>
|
||||
|
||||
<h3>Hi <%= user.displayName || user.username || user.email %>,</h3>
|
||||
|
||||
<h2>Welcome to <%= cloudronName %>!</h2>
|
||||
|
||||
<p>
|
||||
<a href="<%= inviteLink %>">Get started</a>.
|
||||
</p>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<div style="font-size: 10px; color: #333333; background: #ffffff;">
|
||||
<% if (invitor && invitor.email) { %>
|
||||
You are receiving this email because you were invited by <%= invitor.email %>.
|
||||
<% } %>
|
||||
|
||||
<br/>
|
||||
|
||||
Powered by <a href="https://cloudron.io">Cloudron</a>
|
||||
</div>
|
||||
|
||||
</center>
|
||||
|
||||
<% } %>
|
||||
+69
-43
@@ -1,31 +1,32 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
addMailbox: addMailbox,
|
||||
addList: addList,
|
||||
addMailbox,
|
||||
addList,
|
||||
|
||||
updateMailboxOwner: updateMailboxOwner,
|
||||
updateList: updateList,
|
||||
del: del,
|
||||
updateMailboxOwner,
|
||||
updateList,
|
||||
del,
|
||||
|
||||
listMailboxes: listMailboxes,
|
||||
getLists: getLists,
|
||||
getMailboxCount,
|
||||
listMailboxes,
|
||||
getLists,
|
||||
|
||||
listAllMailboxes: listAllMailboxes,
|
||||
listAllMailboxes,
|
||||
|
||||
get: get,
|
||||
getMailbox: getMailbox,
|
||||
getList: getList,
|
||||
getAlias: getAlias,
|
||||
get,
|
||||
getMailbox,
|
||||
getList,
|
||||
getAlias,
|
||||
|
||||
getAliasesForName: getAliasesForName,
|
||||
setAliasesForName: setAliasesForName,
|
||||
getAliasesForName,
|
||||
setAliasesForName,
|
||||
|
||||
getByOwnerId: getByOwnerId,
|
||||
delByOwnerId: delByOwnerId,
|
||||
delByDomain: delByDomain,
|
||||
getByOwnerId,
|
||||
delByOwnerId,
|
||||
delByDomain,
|
||||
|
||||
updateName: updateName,
|
||||
updateName,
|
||||
|
||||
_clear: clear,
|
||||
|
||||
@@ -37,10 +38,11 @@ exports = module.exports = {
|
||||
var assert = require('assert'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
database = require('./database.js'),
|
||||
mysql = require('mysql'),
|
||||
safe = require('safetydance'),
|
||||
util = require('util');
|
||||
|
||||
var MAILBOX_FIELDS = [ 'name', 'type', 'ownerId', 'aliasName', 'aliasDomain', 'creationTime', 'membersJson', 'membersOnly', 'domain' ].join(',');
|
||||
var MAILBOX_FIELDS = [ 'name', 'type', 'ownerId', 'ownerType', 'aliasName', 'aliasDomain', 'creationTime', 'membersJson', 'membersOnly', 'domain' ].join(',');
|
||||
|
||||
function postProcess(data) {
|
||||
data.members = safe.JSON.parse(data.membersJson) || [ ];
|
||||
@@ -51,13 +53,14 @@ function postProcess(data) {
|
||||
return data;
|
||||
}
|
||||
|
||||
function addMailbox(name, domain, ownerId, callback) {
|
||||
function addMailbox(name, domain, ownerId, ownerType, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof ownerId, 'string');
|
||||
assert.strictEqual(typeof ownerType, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('INSERT INTO mailboxes (name, type, domain, ownerId) VALUES (?, ?, ?, ?)', [ name, exports.TYPE_MAILBOX, domain, ownerId ], function (error) {
|
||||
database.query('INSERT INTO mailboxes (name, type, domain, ownerId, ownerType) VALUES (?, ?, ?, ?, ?)', [ name, exports.TYPE_MAILBOX, domain, ownerId, ownerType ], function (error) {
|
||||
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));
|
||||
|
||||
@@ -65,13 +68,14 @@ function addMailbox(name, domain, ownerId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function updateMailboxOwner(name, domain, ownerId, callback) {
|
||||
function updateMailboxOwner(name, domain, ownerId, ownerType, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof ownerId, 'string');
|
||||
assert.strictEqual(typeof ownerType, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('UPDATE mailboxes SET ownerId = ? WHERE name = ? AND domain = ?', [ ownerId, name, domain ], function (error, result) {
|
||||
database.query('UPDATE mailboxes SET ownerId = ?, ownerType = ? WHERE name = ? AND domain = ?', [ ownerId, ownerType, name, domain ], function (error, result) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (result.affectedRows === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found'));
|
||||
|
||||
@@ -86,8 +90,8 @@ function addList(name, domain, members, membersOnly, callback) {
|
||||
assert.strictEqual(typeof membersOnly, 'boolean');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('INSERT INTO mailboxes (name, type, domain, ownerId, membersJson, membersOnly) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[ name, exports.TYPE_LIST, domain, 'admin', JSON.stringify(members), membersOnly ], function (error) {
|
||||
database.query('INSERT INTO mailboxes (name, type, domain, ownerId, ownerType, membersJson, membersOnly) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||||
[ name, exports.TYPE_LIST, domain, 'admin', 'user', 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) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
@@ -203,20 +207,35 @@ function getMailbox(name, domain, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function listMailboxes(domain, page, perPage, callback) {
|
||||
function getMailboxCount(domain, callback) {
|
||||
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 perPage, 'number');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query(`SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE type = ? AND domain = ? ORDER BY name LIMIT ${(page-1)*perPage},${perPage}`,
|
||||
[ exports.TYPE_MAILBOX, domain ], function (error, results) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
let query = `SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE type = ? AND domain = ?`;
|
||||
if (search) query += ' AND (name LIKE ' + mysql.escape('%' + search + '%') + ')';
|
||||
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) {
|
||||
@@ -224,8 +243,8 @@ function listAllMailboxes(page, perPage, callback) {
|
||||
assert.strictEqual(typeof perPage, 'number');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query(`SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE type = ? ORDER BY name LIMIT ${(page-1)*perPage},${perPage}`,
|
||||
[ exports.TYPE_MAILBOX ], function (error, results) {
|
||||
database.query(`SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE type = ? ORDER BY name LIMIT ?,?`,
|
||||
[ exports.TYPE_MAILBOX, (page-1)*perPage, perPage ], function (error, results) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
results.forEach(function (result) { postProcess(result); });
|
||||
@@ -234,18 +253,25 @@ function listAllMailboxes(page, perPage, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getLists(domain, callback) {
|
||||
function getLists(domain, search, page, perPage, callback) {
|
||||
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');
|
||||
|
||||
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE type = ? AND domain = ?',
|
||||
[ exports.TYPE_LIST, domain ], function (error, results) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
let query = `SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE type = ? AND domain = ?`;
|
||||
if (search) query += ' AND (name LIKE ' + mysql.escape('%' + search + '%') + ' OR membersJson LIKE ' + mysql.escape('%' + search + '%') + ')';
|
||||
|
||||
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) {
|
||||
@@ -290,8 +316,8 @@ function setAliasesForName(name, domain, aliases, callback) {
|
||||
// clear existing aliases
|
||||
queries.push({ query: 'DELETE FROM mailboxes WHERE aliasName = ? AND aliasDomain = ? AND type = ?', args: [ name, domain, exports.TYPE_ALIAS ] });
|
||||
aliases.forEach(function (alias) {
|
||||
queries.push({ query: 'INSERT INTO mailboxes (name, domain, type, aliasName, aliasDomain, ownerId) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
args: [ alias.name, alias.domain, exports.TYPE_ALIAS, name, domain, results[0].ownerId ] });
|
||||
queries.push({ query: 'INSERT INTO mailboxes (name, domain, type, aliasName, aliasDomain, ownerId, ownerType) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||||
args: [ alias.name, alias.domain, exports.TYPE_ALIAS, name, domain, results[0].ownerId, results[0].ownerType ] });
|
||||
});
|
||||
|
||||
database.transaction(queries, function (error) {
|
||||
|
||||
+6
-3
@@ -17,7 +17,7 @@ var assert = require('assert'),
|
||||
database = require('./database.js'),
|
||||
safe = require('safetydance');
|
||||
|
||||
var MAILDB_FIELDS = [ 'domain', 'enabled', 'mailFromValidation', 'catchAllJson', 'relayJson', 'dkimSelector' ].join(',');
|
||||
var MAILDB_FIELDS = [ 'domain', 'enabled', 'mailFromValidation', 'catchAllJson', 'relayJson', 'dkimSelector', 'bannerJson' ].join(',');
|
||||
|
||||
function postProcess(data) {
|
||||
data.enabled = !!data.enabled; // int to boolean
|
||||
@@ -29,6 +29,9 @@ function postProcess(data) {
|
||||
data.relay = safe.JSON.parse(data.relayJson) || { provider: 'cloudron-smtp' };
|
||||
delete data.relayJson;
|
||||
|
||||
data.banner = safe.JSON.parse(data.bannerJson) || { text: null, html: null };
|
||||
delete data.bannerJson;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -74,8 +77,8 @@ function update(domain, data, callback) {
|
||||
var args = [ ];
|
||||
var fields = [ ];
|
||||
for (var k in data) {
|
||||
if (k === 'catchAll') {
|
||||
fields.push('catchAllJson = ?');
|
||||
if (k === 'catchAll' || k === 'banner') {
|
||||
fields.push(`${k}Json = ?`);
|
||||
args.push(JSON.stringify(data[k]));
|
||||
} else if (k === 'relay') {
|
||||
fields.push('relayJson = ?');
|
||||
|
||||
+46
-42
@@ -34,6 +34,7 @@ var assert = require('assert'),
|
||||
safe = require('safetydance'),
|
||||
settings = require('./settings.js'),
|
||||
showdown = require('showdown'),
|
||||
translation = require('./translation.js'),
|
||||
smtpTransport = require('nodemailer-smtp-transport'),
|
||||
util = require('util');
|
||||
|
||||
@@ -91,14 +92,21 @@ function sendMail(mailOptions, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function render(templateFile, params) {
|
||||
function render(templateFile, params, translationAssets) {
|
||||
assert.strictEqual(typeof templateFile, 'string');
|
||||
assert.strictEqual(typeof params, 'object');
|
||||
|
||||
var content = null;
|
||||
var raw = safe.fs.readFileSync(path.join(MAIL_TEMPLATES_DIR, templateFile), 'utf8');
|
||||
if (raw === null) {
|
||||
debug(`Error loading ${templateFile}`);
|
||||
return '';
|
||||
}
|
||||
|
||||
if (typeof translationAssets === 'object') raw = translation.translate(raw, translationAssets.translations || {}, translationAssets.fallback || {});
|
||||
|
||||
try {
|
||||
content = ejs.render(safe.fs.readFileSync(path.join(MAIL_TEMPLATES_DIR, templateFile), 'utf8'), params);
|
||||
content = ejs.render(raw, params);
|
||||
} catch (e) {
|
||||
debug(`Error rendering ${templateFile}`, e);
|
||||
}
|
||||
@@ -135,30 +143,28 @@ function sendInvite(user, invitor, inviteLink) {
|
||||
getMailConfig(function (error, mailConfig) {
|
||||
if (error) return debug('Error getting mail details:', error);
|
||||
|
||||
var templateData = {
|
||||
user: user,
|
||||
webadminUrl: settings.adminOrigin(),
|
||||
inviteLink: inviteLink,
|
||||
invitor: invitor,
|
||||
cloudronName: mailConfig.cloudronName,
|
||||
cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar'
|
||||
};
|
||||
translation.getTranslations(function (error, translationAssets) {
|
||||
if (error) return debug('Error getting translations:', error);
|
||||
|
||||
var templateDataText = JSON.parse(JSON.stringify(templateData));
|
||||
templateDataText.format = 'text';
|
||||
var templateData = {
|
||||
user: user.displayName || user.username || user.email,
|
||||
webadminUrl: settings.adminOrigin(),
|
||||
inviteLink: inviteLink,
|
||||
invitor: invitor ? invitor.email : null,
|
||||
cloudronName: mailConfig.cloudronName,
|
||||
cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar'
|
||||
};
|
||||
|
||||
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
|
||||
templateDataHTML.format = 'html';
|
||||
var mailOptions = {
|
||||
from: mailConfig.notificationFrom,
|
||||
to: user.fallbackEmail,
|
||||
subject: ejs.render(translation.translate('{{ welcomeEmail.subject }}', translationAssets.translations || {}, translationAssets.fallback || {}), { cloudron: mailConfig.cloudronName }),
|
||||
text: render('welcome_user-text.ejs', templateData, translationAssets),
|
||||
html: render('welcome_user-html.ejs', templateData, translationAssets)
|
||||
};
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig.notificationFrom,
|
||||
to: user.fallbackEmail,
|
||||
subject: util.format('Welcome to %s', mailConfig.cloudronName),
|
||||
text: render('welcome_user.ejs', templateDataText),
|
||||
html: render('welcome_user.ejs', templateDataHTML)
|
||||
};
|
||||
|
||||
sendMail(mailOptions);
|
||||
sendMail(mailOptions);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -221,28 +227,26 @@ function passwordReset(user) {
|
||||
getMailConfig(function (error, mailConfig) {
|
||||
if (error) return debug('Error getting mail details:', error);
|
||||
|
||||
var templateData = {
|
||||
user: user,
|
||||
resetLink: `${settings.adminOrigin()}/login.html?resetToken=${user.resetToken}`,
|
||||
cloudronName: mailConfig.cloudronName,
|
||||
cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar'
|
||||
};
|
||||
translation.getTranslations(function (error, translationAssets) {
|
||||
if (error) return debug('Error getting translations:', error);
|
||||
|
||||
var templateDataText = JSON.parse(JSON.stringify(templateData));
|
||||
templateDataText.format = 'text';
|
||||
var templateData = {
|
||||
user: user.displayName || user.username || user.email,
|
||||
resetLink: `${settings.adminOrigin()}/login.html?resetToken=${user.resetToken}`,
|
||||
cloudronName: mailConfig.cloudronName,
|
||||
cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar'
|
||||
};
|
||||
|
||||
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
|
||||
templateDataHTML.format = 'html';
|
||||
var mailOptions = {
|
||||
from: mailConfig.notificationFrom,
|
||||
to: user.fallbackEmail,
|
||||
subject: ejs.render(translation.translate('{{ passwordResetEmail.subject }}', translationAssets.translations || {}, translationAssets.fallback || {}), { cloudron: mailConfig.cloudronName }),
|
||||
text: render('password_reset-text.ejs', templateData, translationAssets),
|
||||
html: render('password_reset-html.ejs', templateData, translationAssets)
|
||||
};
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig.notificationFrom,
|
||||
to: user.fallbackEmail,
|
||||
subject: util.format('[%s] Password Reset', mailConfig.cloudronName),
|
||||
text: render('password_reset.ejs', templateDataText),
|
||||
html: render('password_reset.ejs', templateDataHTML)
|
||||
};
|
||||
|
||||
sendMail(mailOptions);
|
||||
sendMail(mailOptions);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
cookieParser: require('cookie-parser'),
|
||||
cors: require('./cors'),
|
||||
json: require('body-parser').json,
|
||||
morgan: require('morgan'),
|
||||
proxy: require('proxy-middleware'),
|
||||
proxy: require('./proxy-middleware.js'),
|
||||
lastMile: require('connect-lastmile'),
|
||||
multipart: require('./multipart.js'),
|
||||
timeout: require('connect-timeout'),
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
// https://github.com/cloudron-io/node-proxy-middleware
|
||||
// MIT license
|
||||
// contains https://github.com/gonzalocasas/node-proxy-middleware/pull/59
|
||||
|
||||
var os = require('os');
|
||||
var http = require('http');
|
||||
var https = require('https');
|
||||
var owns = {}.hasOwnProperty;
|
||||
|
||||
module.exports = function proxyMiddleware(options) {
|
||||
//enable ability to quickly pass a url for shorthand setup
|
||||
if(typeof options === 'string'){
|
||||
options = require('url').parse(options);
|
||||
}
|
||||
|
||||
var httpLib = options.protocol === 'https:' ? https : http;
|
||||
var request = httpLib.request;
|
||||
|
||||
options = options || {};
|
||||
options.hostname = options.hostname;
|
||||
options.port = options.port;
|
||||
options.pathname = options.pathname || '/';
|
||||
|
||||
return function (req, resp, next) {
|
||||
var url = req.url;
|
||||
// You can pass the route within the options, as well
|
||||
if (typeof options.route === 'string') {
|
||||
if (url === options.route) {
|
||||
url = '';
|
||||
} else if (url.slice(0, options.route.length) === options.route) {
|
||||
url = url.slice(options.route.length);
|
||||
} else {
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
//options for this request
|
||||
var opts = extend({}, options);
|
||||
if (url && url.charAt(0) === '?') { // prevent /api/resource/?offset=0
|
||||
if (options.pathname.length > 1 && options.pathname.charAt(options.pathname.length - 1) === '/') {
|
||||
opts.path = options.pathname.substring(0, options.pathname.length - 1) + url;
|
||||
} else {
|
||||
opts.path = options.pathname + url;
|
||||
}
|
||||
} else if (url) {
|
||||
opts.path = slashJoin(options.pathname, url);
|
||||
} else {
|
||||
opts.path = options.pathname;
|
||||
}
|
||||
opts.method = req.method;
|
||||
opts.headers = options.headers ? merge(req.headers, options.headers) : req.headers;
|
||||
|
||||
applyViaHeader(req.headers, opts, opts.headers);
|
||||
|
||||
if (!options.preserveHost) {
|
||||
// Forwarding the host breaks dotcloud
|
||||
delete opts.headers.host;
|
||||
}
|
||||
|
||||
var myReq = request(opts, function (myRes) {
|
||||
var statusCode = myRes.statusCode
|
||||
, headers = myRes.headers
|
||||
, location = headers.location;
|
||||
// Fix the location
|
||||
if (((statusCode > 300 && statusCode < 304) || statusCode === 201) && location && location.indexOf(options.href) > -1) {
|
||||
// absoulte path
|
||||
headers.location = location.replace(options.href, slashJoin('/', slashJoin((options.route || ''), '')));
|
||||
}
|
||||
applyViaHeader(myRes.headers, opts, myRes.headers);
|
||||
rewriteCookieHosts(myRes.headers, opts, myRes.headers, req);
|
||||
resp.writeHead(myRes.statusCode, myRes.headers);
|
||||
myRes.on('error', function (err) {
|
||||
next(err);
|
||||
});
|
||||
myRes.on('end', function (err) {
|
||||
next();
|
||||
});
|
||||
myRes.pipe(resp);
|
||||
});
|
||||
myReq.on('error', function (err) {
|
||||
next(err);
|
||||
});
|
||||
if (!req.readable) {
|
||||
myReq.end();
|
||||
} else {
|
||||
req.pipe(myReq);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
function applyViaHeader(existingHeaders, opts, applyTo) {
|
||||
if (!opts.via) return;
|
||||
|
||||
var viaName = (true === opts.via) ? os.hostname() : opts.via;
|
||||
var viaHeader = '1.1 ' + viaName;
|
||||
if(existingHeaders.via) {
|
||||
viaHeader = existingHeaders.via + ', ' + viaHeader;
|
||||
}
|
||||
|
||||
applyTo.via = viaHeader;
|
||||
}
|
||||
|
||||
function rewriteCookieHosts(existingHeaders, opts, applyTo, req) {
|
||||
if (!opts.cookieRewrite || !owns.call(existingHeaders, 'set-cookie')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var existingCookies = existingHeaders['set-cookie'],
|
||||
rewrittenCookies = [],
|
||||
rewriteHostname = (true === opts.cookieRewrite) ? os.hostname() : opts.cookieRewrite;
|
||||
|
||||
if (!Array.isArray(existingCookies)) {
|
||||
existingCookies = [ existingCookies ];
|
||||
}
|
||||
|
||||
for (var i = 0; i < existingCookies.length; i++) {
|
||||
var rewrittenCookie = existingCookies[i].replace(/(Domain)=[a-z\.-_]*?(;|$)/gi, '$1=' + rewriteHostname + '$2');
|
||||
|
||||
if (!req.connection.encrypted) {
|
||||
rewrittenCookie = rewrittenCookie.replace(/;\s*?(Secure)/i, '');
|
||||
}
|
||||
rewrittenCookies.push(rewrittenCookie);
|
||||
}
|
||||
|
||||
applyTo['set-cookie'] = rewrittenCookies;
|
||||
}
|
||||
|
||||
function slashJoin(p1, p2) {
|
||||
var trailing_slash = false;
|
||||
|
||||
if (p1.length && p1[p1.length - 1] === '/') { trailing_slash = true; }
|
||||
if (trailing_slash && p2.length && p2[0] === '/') {p2 = p2.substring(1); }
|
||||
|
||||
return p1 + p2;
|
||||
}
|
||||
|
||||
function extend(obj, src) {
|
||||
for (var key in src) if (owns.call(src, key)) obj[key] = src[key];
|
||||
return obj;
|
||||
}
|
||||
|
||||
//merges data without changing state in either argument
|
||||
function merge(src1, src2) {
|
||||
var merged = {};
|
||||
extend(merged, src1);
|
||||
extend(merged, src2);
|
||||
return merged;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
getBlocklist,
|
||||
setBlocklist
|
||||
};
|
||||
|
||||
const assert = require('assert'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
ipaddr = require('ipaddr.js'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
settings = require('./settings.js'),
|
||||
shell = require('./shell.js'),
|
||||
validator = require('validator');
|
||||
|
||||
const SET_BLOCKLIST_CMD = path.join(__dirname, 'scripts/setblocklist.sh');
|
||||
|
||||
function getBlocklist(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const data = safe.fs.readFileSync(paths.FIREWALL_BLOCKLIST_FILE, 'utf8');
|
||||
callback(null, data);
|
||||
}
|
||||
|
||||
function setBlocklist(blocklist, auditSource, callback) {
|
||||
assert.strictEqual(typeof blocklist, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const parsedIp = ipaddr.process(auditSource.ip);
|
||||
|
||||
for (const line of blocklist.split('\n')) {
|
||||
if (!line || line.startsWith('#')) continue;
|
||||
const rangeOrIP = line.trim();
|
||||
if (!validator.isIP(rangeOrIP) && !validator.isIPRange(rangeOrIP)) return callback(new BoxError(BoxError.BAD_FIELD, `${rangeOrIP} is not a valid IP or range`));
|
||||
|
||||
if (rangeOrIP.indexOf('/') === -1) {
|
||||
if (auditSource.ip === rangeOrIP) return callback(new BoxError(BoxError.BAD_FIELD, `${rangeOrIP} includes client IP. Cannot block yourself`));
|
||||
} else {
|
||||
const parsedRange = ipaddr.parseCIDR(rangeOrIP);
|
||||
if (parsedIp.match(parsedRange)) return callback(new BoxError(BoxError.BAD_FIELD, `${rangeOrIP} includes client IP. Cannot block yourself`));
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.isDemo()) return callback(new BoxError(BoxError.CONFLICT, 'Not allowed in demo mode'));
|
||||
|
||||
if (!safe.fs.writeFileSync(paths.FIREWALL_BLOCKLIST_FILE, blocklist + '\n', 'utf8')) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message));
|
||||
|
||||
shell.sudo('setBlocklist', [ SET_BLOCKLIST_CMD ], {}, function (error) {
|
||||
if (error) return callback(new BoxError(BoxError.IPTABLES_ERROR, `Error setting blocklist: ${error.message}`));
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
+92
-28
@@ -6,54 +6,61 @@ map $http_upgrade $connection_upgrade {
|
||||
|
||||
# http server
|
||||
server {
|
||||
listen 80;
|
||||
<% if (endpoint === 'ip' || endpoint === 'setup') { -%>
|
||||
listen 80 default_server;
|
||||
server_name _;
|
||||
<% if (hasIPv6) { -%>
|
||||
listen [::]:80;
|
||||
listen [::]:80 default_server;
|
||||
<% } -%>
|
||||
|
||||
<% if (vhost) { -%>
|
||||
server_name <%= vhost %>;
|
||||
<% } else { -%>
|
||||
# IP based access from collectd or initial cloudron setup. TODO: match the IPv6 address
|
||||
server_name "~^\d+\.\d+\.\d+\.\d+$";
|
||||
|
||||
# collectd
|
||||
location /nginx_status {
|
||||
stub_status on;
|
||||
access_log off;
|
||||
allow 127.0.0.1;
|
||||
deny all;
|
||||
}
|
||||
listen 80;
|
||||
server_name <%= vhost %>;
|
||||
<% if (hasIPv6) { -%>
|
||||
listen [::]:80;
|
||||
<% } -%>
|
||||
<% } -%>
|
||||
|
||||
# acme challenges (for cert renewal where the vhost config exists)
|
||||
server_tokens off; # hide version
|
||||
|
||||
# acme challenges
|
||||
location /.well-known/acme-challenge/ {
|
||||
default_type text/plain;
|
||||
alias /home/yellowtent/platformdata/acme/;
|
||||
}
|
||||
|
||||
# for default server, serve the splash page. for other endpoints, redirect to HTTPS
|
||||
location / {
|
||||
# redirect everything to HTTPS
|
||||
<% if ( endpoint === 'admin' || endpoint === 'setup' ) { %>
|
||||
return 301 https://$host$request_uri;
|
||||
<% } else if ( endpoint === 'app' ) { %>
|
||||
return 301 https://$host$request_uri;
|
||||
<% } else if ( endpoint === 'redirect' ) { %>
|
||||
return 301 https://<%= redirectTo %>$request_uri;
|
||||
<% } else if ( endpoint === 'ip' ) { %>
|
||||
root <%= sourceDir %>/dashboard/dist;
|
||||
try_files /splash.html =404;
|
||||
<% } %>
|
||||
}
|
||||
}
|
||||
|
||||
# https server
|
||||
server {
|
||||
<% if (vhost) { -%>
|
||||
server_name <%= vhost %>;
|
||||
listen 443 http2;
|
||||
<% if (endpoint === 'ip' || endpoint === 'setup') { -%>
|
||||
listen 443 ssl http2 default_server;
|
||||
server_name _;
|
||||
<% if (hasIPv6) { -%>
|
||||
listen [::]:443 http2;
|
||||
listen [::]:443 ssl http2 default_server;
|
||||
<% } -%>
|
||||
<% } else { -%>
|
||||
listen 443 http2 default_server;
|
||||
listen 443 ssl http2;
|
||||
server_name <%= vhost %>;
|
||||
<% if (hasIPv6) { -%>
|
||||
listen [::]:443 http2 default_server;
|
||||
listen [::]:443 ssl http2;
|
||||
<% } -%>
|
||||
<% } -%>
|
||||
|
||||
ssl on;
|
||||
server_tokens off; # hide version
|
||||
|
||||
# paths are relative to prefix and not to this file
|
||||
ssl_certificate <%= certFilePath %>;
|
||||
ssl_certificate_key <%= keyFilePath %>;
|
||||
@@ -93,7 +100,7 @@ server {
|
||||
# enable for proxied requests as well
|
||||
gzip_proxied any;
|
||||
|
||||
<% if ( endpoint === 'admin' ) { -%>
|
||||
<% if ( endpoint === 'admin' || endpoint === 'ip' || endpoint === 'setup' ) { -%>
|
||||
# CSP headers for the admin/dashboard resources
|
||||
add_header Content-Security-Policy "default-src 'none'; frame-src 'self' cloudron.io *.cloudron.io; connect-src wss: https: 'self' *.cloudron.io; script-src https: 'self' 'unsafe-inline' 'unsafe-eval'; img-src * data:; style-src https: 'unsafe-inline'; object-src 'none'; font-src https: 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self';";
|
||||
<% } else { %>
|
||||
@@ -140,7 +147,7 @@ server {
|
||||
<% if ( endpoint === 'admin' ) { %>
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
<% } else if ( endpoint === 'app' ) { %>
|
||||
proxy_pass http://127.0.0.1:<%= port %>;
|
||||
proxy_pass http://<%= ip %>:<%= port %>;
|
||||
<% } else if ( endpoint === 'redirect' ) { %>
|
||||
return 302 https://<%= redirectTo %>$request_uri;
|
||||
<% } %>
|
||||
@@ -152,6 +159,27 @@ server {
|
||||
try_files /$1 @wellknown-upstream;
|
||||
}
|
||||
|
||||
<% if (proxyAuth.enabled) { %>
|
||||
proxy_set_header X-App-ID "<%= proxyAuth.id %>";
|
||||
|
||||
location = /proxy-auth {
|
||||
internal;
|
||||
proxy_pass http://127.0.0.1:3001/auth;
|
||||
proxy_pass_request_body off;
|
||||
# repeat proxy headers since we addded proxy_set_header at this location level
|
||||
proxy_set_header X-App-ID "<%= proxyAuth.id %>";
|
||||
proxy_set_header Content-Length "";
|
||||
}
|
||||
|
||||
location ~ ^/(login|logout)$ {
|
||||
proxy_pass http://127.0.0.1:3001;
|
||||
}
|
||||
|
||||
location @proxy-auth-login {
|
||||
return 302 /login?redirect=$request_uri;
|
||||
}
|
||||
<% } %>
|
||||
|
||||
location / {
|
||||
# increase the proxy buffer sizes to not run into buffer issues (http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffers)
|
||||
proxy_buffer_size 128k;
|
||||
@@ -170,7 +198,7 @@ server {
|
||||
}
|
||||
<% } %>
|
||||
|
||||
<% if ( endpoint === 'admin' ) { %>
|
||||
<% if ( endpoint === 'admin' || endpoint === 'setup' ) { %>
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
client_max_body_size 1m;
|
||||
@@ -193,6 +221,16 @@ server {
|
||||
client_max_body_size 0;
|
||||
}
|
||||
|
||||
location ~ ^/api/v1/apps/.*/files/ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
client_max_body_size 0;
|
||||
}
|
||||
|
||||
location ~ ^/api/v1/volumes/.*/files/ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
client_max_body_size 0;
|
||||
}
|
||||
|
||||
# graphite paths (uncomment block below and visit /graphite-web/dashboard)
|
||||
# remember to comment out the CSP policy as well to access the graphite dashboard
|
||||
# location ~ ^/graphite-web/ {
|
||||
@@ -205,11 +243,37 @@ server {
|
||||
index index.html index.htm;
|
||||
}
|
||||
<% } else if ( endpoint === 'app' ) { %>
|
||||
proxy_pass http://127.0.0.1:<%= port %>;
|
||||
location = /appstatus.html {
|
||||
}
|
||||
|
||||
<% if (proxyAuth.enabled) { %>
|
||||
location "<%= proxyAuth.path %>" {
|
||||
auth_request /proxy-auth;
|
||||
auth_request_set $auth_cookie $upstream_http_set_cookie;
|
||||
add_header Set-Cookie $auth_cookie;
|
||||
error_page 401 = @proxy-auth-login;
|
||||
|
||||
proxy_pass http://<%= ip %>:<%= port %>;
|
||||
}
|
||||
<% } %>
|
||||
|
||||
<% Object.keys(httpPaths).forEach(function (path) { -%>
|
||||
location "<%= path %>" {
|
||||
# the trailing / will replace part of the original URI matched by the location.
|
||||
proxy_pass http://<%= ip %>:<%= httpPaths[path] %>/;
|
||||
}
|
||||
<% }); %>
|
||||
|
||||
proxy_pass http://<%= ip %>:<%= port %>;
|
||||
<% } else if ( endpoint === 'redirect' ) { %>
|
||||
# redirect everything to the app. this is temporary because there is no way
|
||||
# to clear a permanent redirect on the browser
|
||||
return 302 https://<%= redirectTo %>$request_uri;
|
||||
<% } else if ( endpoint === 'ip' ) { %>
|
||||
location / {
|
||||
root <%= sourceDir %>/dashboard/dist;
|
||||
try_files /splash.html =404;
|
||||
}
|
||||
<% } %>
|
||||
}
|
||||
}
|
||||
|
||||
+12
-11
@@ -155,12 +155,12 @@ function oomEvent(eventId, app, addon, containerId, event, callback) {
|
||||
let title, message, program;
|
||||
if (app) {
|
||||
program = `App ${app.fqdn}`;
|
||||
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/#memory-limit)';
|
||||
title = `The application at ${app.fqdn} ran out of memory.`;
|
||||
message = 'The application has been restarted automatically. If you see this notification often, consider increasing the [memory limit](https://docs.cloudron.io/apps/#memory-limit)';
|
||||
} else if (addon) {
|
||||
program = `${addon.name} service`;
|
||||
title = `The ${addon.name} service ran out of memory`;
|
||||
message = 'The service has been restarted automatically. If you see this notification often, consider increasing the [memory limit](https://cloudron.io/documentation/troubleshooting/#services)';
|
||||
message = 'The service has been restarted automatically. If you see this notification often, consider increasing the [memory limit](https://docs.cloudron.io/troubleshooting/#services)';
|
||||
} else { // this never happens currently
|
||||
program = `Container ${containerId}`;
|
||||
title = `The container ${containerId} ran out of memory`;
|
||||
@@ -181,7 +181,7 @@ function appUp(eventId, app, callback) {
|
||||
|
||||
actionForAllAdmins([], function (admin, done) {
|
||||
mailer.appUp(admin.email, app);
|
||||
add(admin.id, eventId, `App ${app.fqdn} is back online`, `The application ${app.manifest.title} installed at ${app.fqdn} is back online.`, done);
|
||||
add(admin.id, eventId, `App ${app.fqdn} is back online`, `The application installed at ${app.fqdn} is back online.`, done);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ function appDied(eventId, app, callback) {
|
||||
|
||||
actionForAllAdmins([], function (admin, callback) {
|
||||
mailer.appDied(admin.email, app);
|
||||
add(admin.id, eventId, `App ${app.fqdn} is down`, `The application ${app.manifest.title} installed at ${app.fqdn} is not responding.`, callback);
|
||||
add(admin.id, eventId, `App ${app.fqdn} is down`, `The application installed at ${app.fqdn} is not responding.`, callback);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
@@ -201,17 +201,19 @@ function appUpdated(eventId, app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!app.appStoreId) return callback(); // skip notification of dev apps
|
||||
|
||||
const tmp = app.manifest.description.match(/<upstream>(.*)<\/upstream>/i);
|
||||
const upstreamVersion = (tmp && tmp[1]) ? tmp[1] : '';
|
||||
const title = upstreamVersion ? `${app.manifest.title} at ${app.fqdn} updated to ${upstreamVersion} (package version ${app.manifest.version})`
|
||||
: `${app.manifest.title} at ${app.fqdn} updated to package version ${app.manifest.version}`;
|
||||
|
||||
actionForAllAdmins([], function (admin, done) {
|
||||
add(admin.id, eventId, title, `The application ${app.manifest.title} installed at https://${app.fqdn} was updated.\n\nChangelog:\n${app.manifest.changelog}\n`, function (error) {
|
||||
add(admin.id, eventId, title, `The application installed at https://${app.fqdn} was updated.\n\nChangelog:\n${app.manifest.changelog}\n`, function (error) {
|
||||
if (error) return callback(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();
|
||||
});
|
||||
});
|
||||
@@ -239,7 +241,7 @@ function boxUpdateError(eventId, errorMessage, callback) {
|
||||
|
||||
actionForAllAdmins([], function (admin, done) {
|
||||
mailer.boxUpdateError(admin.email, errorMessage);
|
||||
add(admin.id, eventId, 'Cloudron update failed', `Failed to update Cloudron: ${errorMessage}. Update will be retried in 4 hours`, done);
|
||||
add(admin.id, eventId, 'Cloudron update failed', `Failed to update Cloudron: ${errorMessage}.`, done);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
@@ -263,7 +265,7 @@ function backupFailed(eventId, taskId, errorMessage, callback) {
|
||||
|
||||
actionForAllAdmins([], function (admin, callback) {
|
||||
mailer.backupFailed(admin.email, errorMessage, `${settings.adminOrigin()}/logs.html?taskId=${taskId}`);
|
||||
add(admin.id, eventId, 'Backup failed', `Backup failed: ${errorMessage}. Logs are available [here](/logs.html?taskId=${taskId}). Will be retried in 4 hours`, callback);
|
||||
add(admin.id, eventId, 'Backup failed', `Backup failed: ${errorMessage}. Logs are available [here](/logs.html?taskId=${taskId}).`, callback);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
@@ -301,7 +303,7 @@ function alert(id, title, message, callback) {
|
||||
});
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) console.error(error);
|
||||
if (error) debug('alert: error notifying', error);
|
||||
|
||||
callback();
|
||||
});
|
||||
@@ -338,7 +340,6 @@ function onEvent(id, action, source, data, callback) {
|
||||
return appUp(id, data.app, callback);
|
||||
|
||||
case eventlog.ACTION_APP_UPDATE_FINISH:
|
||||
if (!data.app.appStoreId) return callback(); // skip notification of dev apps
|
||||
return appUpdated(id, data.app, callback);
|
||||
|
||||
case eventlog.ACTION_CERTIFICATE_RENEWAL:
|
||||
|
||||
+7
-1
@@ -16,6 +16,7 @@ exports = module.exports = {
|
||||
|
||||
CLOUDRON_DEFAULT_AVATAR_FILE: path.join(__dirname + '/../assets/avatar.png'),
|
||||
INFRA_VERSION_FILE: path.join(baseDir(), 'platformdata/INFRA_VERSION'),
|
||||
DASHBOARD_DIR: constants.TEST ? path.join(__dirname, '../../dashboard/src') : path.join(baseDir(), 'box/dashboard/dist'),
|
||||
|
||||
PROVIDER_FILE: '/etc/cloudron/PROVIDER',
|
||||
|
||||
@@ -35,25 +36,30 @@ exports = module.exports = {
|
||||
SNAPSHOT_INFO_FILE: path.join(baseDir(), 'platformdata/backup/snapshot-info.json'),
|
||||
DYNDNS_INFO_FILE: path.join(baseDir(), 'platformdata/dyndns-info.json'),
|
||||
FEATURES_INFO_FILE: path.join(baseDir(), 'platformdata/features-info.json'),
|
||||
PROXY_AUTH_TOKEN_SECRET_FILE: path.join(baseDir(), 'platformdata/proxy-auth-token-secret'),
|
||||
VERSION_FILE: path.join(baseDir(), 'platformdata/VERSION'),
|
||||
|
||||
// this is not part of appdata because an icon may be set before install
|
||||
APP_ICONS_DIR: path.join(baseDir(), 'boxdata/appicons'),
|
||||
PROFILE_ICONS_DIR: path.join(baseDir(), 'boxdata/profileicons'),
|
||||
MAIL_DATA_DIR: path.join(baseDir(), 'boxdata/mail'),
|
||||
SFTP_KEYS_DIR: path.join(baseDir(), 'boxdata/sftp/ssh'),
|
||||
ACME_ACCOUNT_KEY_FILE: path.join(baseDir(), 'boxdata/acme/acme.key'),
|
||||
APP_CERTS_DIR: path.join(baseDir(), 'boxdata/certs'),
|
||||
CLOUDRON_AVATAR_FILE: path.join(baseDir(), 'boxdata/avatar.png'),
|
||||
UPDATE_CHECKER_FILE: path.join(baseDir(), 'boxdata/updatechecker.json'),
|
||||
ADDON_TURN_SECRET_FILE: path.join(baseDir(), 'boxdata/addon-turn-secret'),
|
||||
FIREWALL_BLOCKLIST_FILE: path.join(baseDir(), 'boxdata/firewall/blocklist.txt'),
|
||||
FIREWALL_CONFIG_FILE: path.join(baseDir(), 'boxdata/firewall-config.json'),
|
||||
|
||||
LOG_DIR: path.join(baseDir(), 'platformdata/logs'),
|
||||
TASKS_LOG_DIR: path.join(baseDir(), 'platformdata/logs/tasks'),
|
||||
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'),
|
||||
|
||||
// this pattern is for the cloudron logs API route to work
|
||||
BACKUP_LOG_FILE: path.join(baseDir(), 'platformdata/logs/backup/app.log'),
|
||||
UPDATER_LOG_FILE: path.join(baseDir(), 'platformdata/logs/updater/app.log')
|
||||
UPDATER_LOG_FILE: path.join(baseDir(), 'platformdata/logs/updater/app.log'),
|
||||
};
|
||||
|
||||
+31
-55
@@ -2,7 +2,7 @@
|
||||
|
||||
exports = module.exports = {
|
||||
start: start,
|
||||
stop: stop,
|
||||
stopAllTasks: stopAllTasks,
|
||||
|
||||
// exported for testing
|
||||
_isReady: false
|
||||
@@ -26,8 +26,6 @@ var addons = require('./addons.js'),
|
||||
tasks = require('./tasks.js'),
|
||||
_ = require('underscore');
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
function start(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -45,7 +43,7 @@ function start(callback) {
|
||||
if (_.isEqual(infra, existingInfra)) {
|
||||
debug('platform is uptodate at version %s', infra.version);
|
||||
|
||||
onPlatformReady();
|
||||
onPlatformReady(false /* !infraChanged */);
|
||||
|
||||
return callback();
|
||||
}
|
||||
@@ -56,9 +54,8 @@ function start(callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.series([
|
||||
stopContainers.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),
|
||||
(next) => { if (existingInfra.version !== infra.version) removeAllContainers(next); else next(); },
|
||||
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
|
||||
graphs.startGraphite.bind(null, existingInfra),
|
||||
sftp.startSftp.bind(null, existingInfra),
|
||||
addons.startServices.bind(null, existingInfra),
|
||||
@@ -68,41 +65,36 @@ function start(callback) {
|
||||
|
||||
locker.unlock(locker.OP_PLATFORM_START);
|
||||
|
||||
onPlatformReady();
|
||||
onPlatformReady(true /* infraChanged */);
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function stop(callback) {
|
||||
function stopAllTasks(callback) {
|
||||
tasks.stopAllTasks(callback);
|
||||
}
|
||||
|
||||
function onPlatformReady() {
|
||||
debug('onPlatformReady: platform is ready');
|
||||
function onPlatformReady(infraChanged) {
|
||||
debug(`onPlatformReady: platform is ready. infra changed: ${infraChanged}`);
|
||||
exports._isReady = true;
|
||||
|
||||
apps.schedulePendingTasks(NOOP_CALLBACK);
|
||||
let tasks = [ apps.schedulePendingTasks ];
|
||||
if (infraChanged) tasks.push(applyPlatformConfig, pruneInfraImages);
|
||||
|
||||
applyPlatformConfig(NOOP_CALLBACK);
|
||||
pruneInfraImages(NOOP_CALLBACK);
|
||||
async.series(async.reflectAll(tasks), function (error, results) {
|
||||
results.forEach((result, idx) => {
|
||||
if (result.error) debug(`Startup task at index ${idx} failed: ${result.error.message}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function applyPlatformConfig(callback) {
|
||||
// scale back db containers, if possible. this is retried because updating memory constraints can fail
|
||||
// with failed to write to memory.memsw.limit_in_bytes: write /sys/fs/cgroup/memory/docker/xx/memory.memsw.limit_in_bytes: device or resource busy
|
||||
settings.getPlatformConfig(function (error, platformConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.retry({ times: 10, interval: 5 * 60 * 1000 }, function (retryCallback) {
|
||||
settings.getPlatformConfig(function (error, platformConfig) {
|
||||
if (error) return retryCallback(error);
|
||||
|
||||
addons.updateServiceConfig(platformConfig, function (error) {
|
||||
if (error) debug('Error updating services. Will rety in 5 minutes', platformConfig, error);
|
||||
|
||||
retryCallback(error);
|
||||
});
|
||||
});
|
||||
}, callback);
|
||||
addons.updateServiceConfig(platformConfig, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function pruneInfraImages(callback) {
|
||||
@@ -123,44 +115,28 @@ function pruneInfraImages(callback) {
|
||||
debug(`pruneInfraImages: removing unused image of ${image.repo}: tag: ${parts[1]} id: ${parts[0]}`);
|
||||
|
||||
let result = safe.child_process.execSync(`docker rmi ${parts[0]}`, { encoding: 'utf8' });
|
||||
if (result === null) debug(`Erroring removing image ${parts[0]}: ${safe.error.mesage}`);
|
||||
if (result === null) debug(`Error removing image ${parts[0]}: ${safe.error.mesage}`);
|
||||
}
|
||||
|
||||
iteratorCallback();
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function stopContainers(existingInfra, callback) {
|
||||
// always stop addons to restart them on any infra change, regardless of minor or major update
|
||||
if (existingInfra.version !== infra.version) {
|
||||
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);
|
||||
}
|
||||
function removeAllContainers(callback) {
|
||||
debug('removeAllContainers: removing all containers for infra upgrade');
|
||||
|
||||
debug('stopContainer: stopping addons for incremental infra update: %j', changedAddons);
|
||||
let filterArg = changedAddons.map(function (c) { return `--filter 'name=${c}'`; }).join(' '); // name=c matches *c*. required for redis-{appid}
|
||||
// ignore error if container not found (and fail later) so that this code works across restarts
|
||||
async.series([
|
||||
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);
|
||||
}
|
||||
async.series([
|
||||
shell.exec.bind(null, 'removeAllContainers', 'docker ps -qa --filter \'label=isCloudronManaged\' | xargs --no-run-if-empty docker stop'),
|
||||
shell.exec.bind(null, 'removeAllContainers', 'docker ps -qa --filter \'label=isCloudronManaged\' | xargs --no-run-if-empty docker rm -f')
|
||||
], callback);
|
||||
}
|
||||
|
||||
function startApps(existingInfra, callback) {
|
||||
function markApps(existingInfra, callback) {
|
||||
if (existingInfra.version === 'none') { // cloudron is being restored from backup
|
||||
debug('startApps: restoring installed apps');
|
||||
debug('markApps: restoring installed apps');
|
||||
apps.restoreInstalledApps(callback);
|
||||
} 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
|
||||
apps.configureInstalledApps(callback);
|
||||
} else {
|
||||
@@ -172,10 +148,10 @@ function startApps(existingInfra, callback) {
|
||||
|
||||
if (changedAddons.length) {
|
||||
// restart apps if docker image changes since the IP changes and any "persistent" connections fail
|
||||
debug(`startApps: changedAddons: ${JSON.stringify(changedAddons)}`);
|
||||
debug(`markApps: changedAddons: ${JSON.stringify(changedAddons)}`);
|
||||
apps.restartAppsUsingAddons(changedAddons, callback);
|
||||
} else {
|
||||
debug('startApps: apps are already uptodate');
|
||||
debug('markApps: apps are already uptodate');
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
+28
-19
@@ -11,6 +11,7 @@ var assert = require('assert'),
|
||||
async = require('async'),
|
||||
backups = require('./backups.js'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
branding = require('./branding.js'),
|
||||
constants = require('./constants.js'),
|
||||
cloudron = require('./cloudron.js'),
|
||||
debug = require('debug')('box:provision'),
|
||||
@@ -54,7 +55,7 @@ function unprovision(callback) {
|
||||
|
||||
// TODO: also cancel any existing configureWebadmin task
|
||||
async.series([
|
||||
settings.setAdmin.bind(null, '', ''),
|
||||
settings.setAdminLocation.bind(null, '', ''),
|
||||
mail.clearDomains,
|
||||
domains.clear
|
||||
], callback);
|
||||
@@ -98,24 +99,24 @@ function setup(dnsConfig, sysinfoConfig, auditSource, callback) {
|
||||
dkimSelector: 'cloudron'
|
||||
};
|
||||
|
||||
domains.add(domain, data, auditSource, function (error) {
|
||||
async.series([
|
||||
settings.setMailLocation.bind(null, domain, `${constants.ADMIN_LOCATION}.${domain}`), // default mail location. do this before we add the domain for upserting mail DNS
|
||||
domains.add.bind(null, domain, data, auditSource),
|
||||
sysinfo.testConfig.bind(null, sysinfoConfig)
|
||||
], function (error) {
|
||||
if (error) return done(error);
|
||||
|
||||
sysinfo.testConfig(sysinfoConfig, function (error) {
|
||||
if (error) return done(error);
|
||||
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([
|
||||
settings.setSysinfoConfig.bind(null, sysinfoConfig),
|
||||
domains.prepareDashboardDomain.bind(null, domain, auditSource, (progress) => setProgress('setup', progress.message, NOOP_CALLBACK)),
|
||||
cloudron.setDashboardDomain.bind(null, domain, auditSource),
|
||||
setProgress.bind(null, 'setup', 'Done'),
|
||||
eventlog.add.bind(null, eventlog.ACTION_PROVISION, auditSource, { })
|
||||
], function (error) {
|
||||
gProvisionStatus.setup.active = false;
|
||||
gProvisionStatus.setup.errorMessage = error ? error.message : '';
|
||||
});
|
||||
async.series([
|
||||
settings.setSysinfoConfig.bind(null, sysinfoConfig),
|
||||
cloudron.setupDnsAndCert.bind(null, constants.ADMIN_LOCATION, domain, auditSource, (progress) => setProgress('setup', progress.message, NOOP_CALLBACK)),
|
||||
cloudron.setDashboardDomain.bind(null, domain, auditSource),
|
||||
setProgress.bind(null, 'setup', 'Done'),
|
||||
eventlog.add.bind(null, eventlog.ACTION_PROVISION, auditSource, { })
|
||||
], function (error) {
|
||||
gProvisionStatus.setup.active = false;
|
||||
gProvisionStatus.setup.errorMessage = error ? error.message : '';
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -162,7 +163,7 @@ function restore(backupConfig, backupId, version, sysinfoConfig, auditSource, ca
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!semver.valid(version)) return callback(new BoxError(BoxError.BAD_FIELD, 'version is not a valid semver', { field: 'version' }));
|
||||
if (constants.VERSION !== version) return callback(new BoxError(BoxError.BAD_STATE, `Run cloudron-setup with --version ${version} to restore from this backup`));
|
||||
if (constants.VERSION !== version) return callback(new BoxError(BoxError.BAD_STATE, `Run "cloudron-setup --version ${version}" on a fresh Ubuntu installation to restore from this backup`));
|
||||
|
||||
if (gProvisionStatus.setup.active || gProvisionStatus.restore.active) return callback(new BoxError(BoxError.BAD_STATE, 'Already setting up or restoring'));
|
||||
|
||||
@@ -199,7 +200,13 @@ function restore(backupConfig, backupId, version, sysinfoConfig, auditSource, ca
|
||||
setProgress.bind(null, 'restore', 'Downloading backup'),
|
||||
backups.restore.bind(null, backupConfig, backupId, (progress) => setProgress('restore', progress.message, NOOP_CALLBACK)),
|
||||
settings.setSysinfoConfig.bind(null, sysinfoConfig),
|
||||
cloudron.setupDashboard.bind(null, auditSource, (progress) => setProgress('restore', progress.message, NOOP_CALLBACK)),
|
||||
(done) => {
|
||||
const adminDomain = settings.adminDomain(); // load this fresh from after the backup.restore
|
||||
async.series([
|
||||
cloudron.setupDnsAndCert.bind(null, constants.ADMIN_LOCATION, adminDomain, auditSource, (progress) => setProgress('restore', progress.message, NOOP_CALLBACK)),
|
||||
cloudron.setDashboardDomain.bind(null, adminDomain, auditSource)
|
||||
], done);
|
||||
},
|
||||
settings.setBackupCredentials.bind(null, backupConfig), // update just the credentials and not the policy and flags
|
||||
eventlog.add.bind(null, eventlog.ACTION_RESTORE, auditSource, { backupId }),
|
||||
], function (error) {
|
||||
@@ -227,9 +234,11 @@ function getStatus(callback) {
|
||||
apiServerOrigin: settings.apiServerOrigin(), // used by CaaS tool
|
||||
webServerOrigin: settings.webServerOrigin(), // used by CaaS tool
|
||||
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
|
||||
footer: allSettings[settings.FOOTER_KEY] || constants.FOOTER,
|
||||
footer: branding.renderFooter(allSettings[settings.FOOTER_KEY] || constants.FOOTER),
|
||||
adminFqdn: settings.adminDomain() ? settings.adminFqdn() : null,
|
||||
language: allSettings[settings.LANGUAGE_KEY],
|
||||
activated: activated,
|
||||
provider: settings.provider() // used by setup wizard of marketplace images
|
||||
}, gProvisionStatus));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
'use strict';
|
||||
|
||||
// heavily inspired from https://gock.net/blog/2020/nginx-subrequest-authentication-server/ and https://github.com/andygock/auth-server
|
||||
|
||||
exports = module.exports = {
|
||||
start,
|
||||
stop
|
||||
};
|
||||
|
||||
const apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
basicAuth = require('basic-auth'),
|
||||
constants = require('./constants.js'),
|
||||
debug = require('debug')('box:proxyAuth'),
|
||||
ejs = require('ejs'),
|
||||
express = require('express'),
|
||||
fs = require('fs'),
|
||||
hat = require('./hat.js'),
|
||||
http = require('http'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
jwt = require('jsonwebtoken'),
|
||||
middleware = require('./middleware'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
translation = require('./translation.js'),
|
||||
users = require('./users.js');
|
||||
|
||||
let gHttpServer = null;
|
||||
let TOKEN_SECRET = null;
|
||||
const EXPIRY_DAYS = 7;
|
||||
|
||||
function jwtVerify(req, res, next) {
|
||||
const token = req.cookies.authToken;
|
||||
|
||||
if (!token) return next();
|
||||
|
||||
jwt.verify(token, TOKEN_SECRET, function (error, decoded) {
|
||||
if (error) {
|
||||
debug('clearing token', error);
|
||||
res.clearCookie('authToken');
|
||||
return next(new HttpError(403, 'Malformed token or bad signature'));
|
||||
}
|
||||
|
||||
req.user = decoded.user || null;
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
function basicAuthVerify(req, res, next) {
|
||||
const appId = req.headers['x-app-id'] || '';
|
||||
const credentials = basicAuth(req);
|
||||
if (!appId || !credentials) return next();
|
||||
|
||||
const api = credentials.name.indexOf('@') !== -1 ? users.verifyWithEmail : users.verifyWithUsername;
|
||||
|
||||
api(credentials.name, credentials.pass, appId, function (error, user) {
|
||||
if (error) return next(new HttpError(403, 'Invalid username or password' ));
|
||||
|
||||
req.user = user;
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
function loginPage(req, res, next) {
|
||||
const appId = req.headers['x-app-id'] || '';
|
||||
if (!appId) return next(new HttpError(503, 'Nginx misconfiguration'));
|
||||
|
||||
translation.getTranslations(function (error, translationAssets) {
|
||||
if (error) return next(new HttpError(500, 'No translation found'));
|
||||
|
||||
const raw = safe.fs.readFileSync(path.join(paths.DASHBOARD_DIR, 'templates/proxyauth-login.ejs'), 'utf8');
|
||||
if (raw === null) return next(new HttpError(500, 'Login template not found'));
|
||||
|
||||
const translatedContent = translation.translate(raw, translationAssets.translations || {}, translationAssets.fallback || {});
|
||||
var finalContent = '';
|
||||
|
||||
apps.get(appId, function (error, app) {
|
||||
if (error) return next(new HttpError(503, error.message));
|
||||
|
||||
const title = app.label || app.manifest.title;
|
||||
|
||||
apps.getIconPath(app, {}, function (error, iconPath) {
|
||||
const icon = 'data:image/png;base64,' + safe.fs.readFileSync(iconPath || '', 'base64');
|
||||
|
||||
try {
|
||||
finalContent = ejs.render(translatedContent, { title, icon });
|
||||
} catch (e) {
|
||||
debug('Error rendering proxyauth-login.ejs', e);
|
||||
return next(new HttpError(500, 'Login template error'));
|
||||
}
|
||||
|
||||
res.set('Content-Type', 'text/html');
|
||||
return res.send(finalContent);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// called by nginx to authorize any protected route
|
||||
function auth(req, res, next) {
|
||||
if (!req.user) return next(new HttpError(401, 'Unauthorized'));
|
||||
|
||||
// user is already authenticated, refresh cookie
|
||||
const token = jwt.sign({ user: req.user }, TOKEN_SECRET, { expiresIn: `${EXPIRY_DAYS}d` });
|
||||
|
||||
res.cookie('authToken', token, {
|
||||
httpOnly: true,
|
||||
maxAge: EXPIRY_DAYS * 86400 * 1000, // milliseconds
|
||||
secure: true
|
||||
});
|
||||
|
||||
return next(new HttpSuccess(200, {}));
|
||||
}
|
||||
|
||||
// endpoint called by login page, username and password posted as JSON body
|
||||
function authenticate(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
const appId = req.headers['x-app-id'] || '';
|
||||
if (!appId) return next(new HttpError(503, 'Nginx misconfiguration'));
|
||||
|
||||
if (typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be non empty string' ));
|
||||
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be non empty string' ));
|
||||
|
||||
const { username, password } = req.body;
|
||||
|
||||
const api = username.indexOf('@') !== -1 ? users.verifyWithEmail : users.verifyWithUsername;
|
||||
|
||||
api(username, password, appId, function (error, user) {
|
||||
if (error) return next(new HttpError(403, 'Invalid username or password' ));
|
||||
|
||||
req.user = user;
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
function authorize(req, res, next) {
|
||||
const appId = req.headers['x-app-id'] || '';
|
||||
if (!appId) return next(new HttpError(503, 'Nginx misconfiguration'));
|
||||
|
||||
apps.get(appId, function (error, app) {
|
||||
if (error) return next(new HttpError(403, 'No such app' ));
|
||||
|
||||
apps.hasAccessTo(app, req.user, function (error, hasAccess) {
|
||||
if (error) return next(new HttpError(403, 'Forbidden' ));
|
||||
if (!hasAccess) return next(new HttpError(403, 'Forbidden' ));
|
||||
|
||||
const token = jwt.sign({ user: users.removePrivateFields(req.user) }, TOKEN_SECRET, { expiresIn: `${EXPIRY_DAYS}d` });
|
||||
|
||||
res.cookie('authToken', token, {
|
||||
httpOnly: true,
|
||||
maxAge: EXPIRY_DAYS * 86400 * 1000, // milliseconds
|
||||
secure: true
|
||||
});
|
||||
|
||||
res.redirect('/');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function logoutPage(req, res) {
|
||||
res.clearCookie('authToken');
|
||||
res.redirect('/'); // do not redirect to '/login' as it may not be protected
|
||||
}
|
||||
|
||||
function logout(req, res, next) {
|
||||
res.clearCookie('authToken');
|
||||
next(new HttpSuccess(200, {}));
|
||||
}
|
||||
|
||||
// provides webhooks for the auth wall
|
||||
function initializeAuthwallExpressSync() {
|
||||
let app = express();
|
||||
let httpServer = http.createServer(app);
|
||||
|
||||
let QUERY_LIMIT = '1mb'; // max size for json and urlencoded queries
|
||||
let REQUEST_TIMEOUT = 10000; // timeout for all requests
|
||||
|
||||
let json = middleware.json({ strict: true, limit: QUERY_LIMIT }); // application/json
|
||||
|
||||
if (process.env.BOX_ENV !== 'test') app.use(middleware.morgan('proxyauth :method :url :status :response-time ms - :res[content-length]', { immediate: false }));
|
||||
|
||||
var router = new express.Router();
|
||||
router.del = router.delete; // amend router.del for readability further on
|
||||
|
||||
app
|
||||
.use(middleware.timeout(REQUEST_TIMEOUT))
|
||||
.use(middleware.cookieParser())
|
||||
.use(router)
|
||||
.use(middleware.lastMile());
|
||||
|
||||
router.get ('/login', loginPage);
|
||||
router.get ('/auth', jwtVerify, basicAuthVerify, auth);
|
||||
router.post('/login', json, authenticate, authorize);
|
||||
router.get ('/logout', logoutPage);
|
||||
router.post('/logout', json, logout);
|
||||
|
||||
return httpServer;
|
||||
}
|
||||
|
||||
function start(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
assert.strictEqual(gHttpServer, null, 'Authwall is already up and running.');
|
||||
|
||||
if (!fs.existsSync(paths.PROXY_AUTH_TOKEN_SECRET_FILE)) {
|
||||
TOKEN_SECRET = hat(64);
|
||||
fs.writeFileSync(paths.PROXY_AUTH_TOKEN_SECRET_FILE, TOKEN_SECRET, 'utf8');
|
||||
} else {
|
||||
TOKEN_SECRET = fs.readFileSync(paths.PROXY_AUTH_TOKEN_SECRET_FILE, 'utf8').trim();
|
||||
}
|
||||
|
||||
gHttpServer = initializeAuthwallExpressSync();
|
||||
|
||||
gHttpServer.listen(constants.AUTHWALL_PORT, '127.0.0.1', callback);
|
||||
}
|
||||
|
||||
function stop(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!gHttpServer) return callback(null);
|
||||
|
||||
gHttpServer.close(callback);
|
||||
gHttpServer = null;
|
||||
}
|
||||
Binary file not shown.
+151
-119
@@ -1,33 +1,33 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
setFallbackCertificate: setFallbackCertificate,
|
||||
getFallbackCertificate: getFallbackCertificate,
|
||||
setFallbackCertificate,
|
||||
getFallbackCertificate,
|
||||
|
||||
generateFallbackCertificateSync: generateFallbackCertificateSync,
|
||||
setAppCertificateSync: setAppCertificateSync,
|
||||
generateFallbackCertificateSync,
|
||||
setAppCertificateSync,
|
||||
|
||||
validateCertificate: validateCertificate,
|
||||
validateCertificate,
|
||||
|
||||
getCertificate: getCertificate,
|
||||
ensureCertificate: ensureCertificate,
|
||||
getCertificate,
|
||||
ensureCertificate,
|
||||
|
||||
renewCerts: renewCerts,
|
||||
renewCerts,
|
||||
|
||||
// the 'configure' ensure a certificate and generate nginx config
|
||||
configureAdmin: configureAdmin,
|
||||
configureApp: configureApp,
|
||||
unconfigureApp: unconfigureApp,
|
||||
configureAdmin,
|
||||
configureApp,
|
||||
unconfigureApp,
|
||||
|
||||
// these only generate nginx config
|
||||
writeDefaultConfig: writeDefaultConfig,
|
||||
writeAdminConfig: writeAdminConfig,
|
||||
writeAppConfig: writeAppConfig,
|
||||
writeDefaultConfig,
|
||||
writeDashboardConfig,
|
||||
writeAppConfig,
|
||||
|
||||
removeAppConfigs: removeAppConfigs,
|
||||
removeAppConfigs,
|
||||
|
||||
// exported for testing
|
||||
_getCertApi: getCertApi
|
||||
_getAcmeApi: getAcmeApi
|
||||
};
|
||||
|
||||
var acme2 = require('./cert/acme2.js'),
|
||||
@@ -35,14 +35,12 @@ var acme2 = require('./cert/acme2.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
caas = require('./cert/caas.js'),
|
||||
constants = require('./constants.js'),
|
||||
crypto = require('crypto'),
|
||||
debug = require('debug')('box:reverseproxy'),
|
||||
domains = require('./domains.js'),
|
||||
ejs = require('ejs'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
fallback = require('./cert/fallback.js'),
|
||||
fs = require('fs'),
|
||||
mail = require('./mail.js'),
|
||||
os = require('os'),
|
||||
@@ -59,20 +57,16 @@ var acme2 = require('./cert/acme2.js'),
|
||||
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/nginxconfig.ejs', { encoding: 'utf8' }),
|
||||
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 callback, 'function');
|
||||
|
||||
if (domainObject.tlsConfig.provider === 'fallback') return callback(null, fallback, { fallback: true });
|
||||
const api = acme2;
|
||||
|
||||
var api = domainObject.tlsConfig.provider === 'caas' ? caas : acme2;
|
||||
|
||||
var options = { prod: false, performHttpAuthorization: false, wildcard: false, email: '' };
|
||||
if (domainObject.tlsConfig.provider !== 'caas') {
|
||||
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;
|
||||
}
|
||||
let options = { prod: false, performHttpAuthorization: false, wildcard: false, email: '' };
|
||||
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)
|
||||
// we cannot use admin@fqdn because the user might not have set it up.
|
||||
@@ -108,8 +102,6 @@ function providerMatchesSync(domainObject, certFilePath, apiOptions) {
|
||||
|
||||
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' });
|
||||
if (!subjectAndIssuer) return false; // something bad happenned
|
||||
|
||||
@@ -117,7 +109,7 @@ function providerMatchesSync(domainObject, certFilePath, apiOptions) {
|
||||
const domain = subject.substr(subject.indexOf('=') + 1).trim(); // subject can be /CN=, CN=, CN = and other forms
|
||||
const issuer = subjectAndIssuer.match(/^issuer=(.*)$/m)[1];
|
||||
const isWildcardCert = domain.includes('*');
|
||||
const isLetsEncryptProd = issuer.includes('Let\'s Encrypt Authority');
|
||||
const isLetsEncryptProd = issuer.includes('Let\'s Encrypt');
|
||||
|
||||
const issuerMismatch = (apiOptions.prod && !isLetsEncryptProd) || (!apiOptions.prod && isLetsEncryptProd);
|
||||
// bare domain is not part of wildcard SAN
|
||||
@@ -125,7 +117,9 @@ function providerMatchesSync(domainObject, certFilePath, apiOptions) {
|
||||
|
||||
const mismatch = issuerMismatch || wildcardMismatch;
|
||||
|
||||
debug(`providerMatchesSync: ${certFilePath} subject=${subject} domain=${domain} issuer=${issuer} wildcard=${isWildcardCert}/${apiOptions.wildcard} prod=${isLetsEncryptProd}/${apiOptions.prod} match=${!mismatch}`);
|
||||
debug(`providerMatchesSync: ${certFilePath} subject=${subject} domain=${domain} issuer=${issuer} `
|
||||
+ `wildcard=${isWildcardCert}/${apiOptions.wildcard} prod=${isLetsEncryptProd}/${apiOptions.prod} `
|
||||
+ `issuerMismatch=${issuerMismatch} wildcardMismatch=${wildcardMismatch} match=${!mismatch}`);
|
||||
|
||||
return !mismatch;
|
||||
}
|
||||
@@ -168,7 +162,7 @@ function validateCertificate(location, domainObject, certificate) {
|
||||
}
|
||||
|
||||
function reload(callback) {
|
||||
if (process.env.BOX_ENV === 'test') return callback();
|
||||
if (constants.TEST) return callback();
|
||||
|
||||
shell.sudo('reload', [ RELOAD_NGINX_CMD ], {}, function (error) {
|
||||
if (error) return callback(new BoxError(BoxError.NGINX_ERROR, `Error reloading nginx: ${error.message}`));
|
||||
@@ -187,14 +181,15 @@ function generateFallbackCertificateSync(domainObject) {
|
||||
let opensslConf = safe.fs.readFileSync('/etc/ssl/openssl.cnf', 'utf8');
|
||||
// SAN must contain all the domains since CN check is based on implementation if SAN is found. -checkhost also checks only SAN if present!
|
||||
let opensslConfWithSan;
|
||||
let cn = domainObject.config.hyphenatedSubdomains ? domains.parentDomain(domain) : domain;
|
||||
let cn = domain;
|
||||
|
||||
debug(`generateFallbackCertificateSync: domain=${domainObject.domain} cn=${cn} hyphenated=${domainObject.config.hyphenatedSubdomains}`);
|
||||
debug(`generateFallbackCertificateSync: domain=${domainObject.domain} cn=${cn}`);
|
||||
|
||||
opensslConfWithSan = `${opensslConf}\n[SAN]\nsubjectAltName=DNS:${domain},DNS:*.${cn}\n`;
|
||||
let configFile = path.join(os.tmpdir(), 'openssl-' + crypto.randomBytes(4).readUInt32LE(0) + '.conf');
|
||||
safe.fs.writeFileSync(configFile, opensslConfWithSan, 'utf8');
|
||||
let certCommand = util.format(`openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 3650 -subj /CN=*.${cn} -extensions SAN -config ${configFile} -nodes`);
|
||||
// the days field is chosen to be less than 825 days per apple requirement (https://support.apple.com/en-us/HT210176)
|
||||
let certCommand = util.format(`openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 800 -subj /CN=*.${cn} -extensions SAN -config ${configFile} -nodes`);
|
||||
if (!safe.child_process.execSync(certCommand)) return { error: new BoxError(BoxError.OPENSSL_ERROR, safe.error.message) };
|
||||
safe.fs.unlinkSync(configFile);
|
||||
|
||||
@@ -215,15 +210,9 @@ function setFallbackCertificate(domain, fallback, callback) {
|
||||
assert.strictEqual(typeof fallback, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (fallback.restricted) { // restricted certs are not backed up
|
||||
debug(`setFallbackCertificate: setting restricted certs for domain ${domain}`);
|
||||
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.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));
|
||||
}
|
||||
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
|
||||
reload(function (error) {
|
||||
@@ -237,15 +226,8 @@ function getFallbackCertificate(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// check for any pre-provisioned (caas) certs. they get first priority
|
||||
var certFilePath = path.join(paths.NGINX_CERT_DIR, `${domain}.host.cert`);
|
||||
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`);
|
||||
const certFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`);
|
||||
const keyFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.key`);
|
||||
|
||||
callback(null, { certFilePath, keyFilePath });
|
||||
}
|
||||
@@ -267,15 +249,12 @@ function setAppCertificateSync(location, domainObject, certificate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function getCertificateByHostname(hostname, domainObject, callback) {
|
||||
function getAcmeCertificate(hostname, domainObject, callback) {
|
||||
assert.strictEqual(typeof hostname, 'string');
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
let certFilePath = path.join(paths.APP_CERTS_DIR, `${hostname}.user.cert`);
|
||||
let keyFilePath = path.join(paths.APP_CERTS_DIR, `${hostname}.user.key`);
|
||||
|
||||
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
|
||||
let certFilePath, keyFilePath;
|
||||
|
||||
if (hostname !== domainObject.domain && domainObject.tlsConfig.wildcard) { // bare domain is not part of wildcard SAN
|
||||
let certName = domains.makeWildcard(hostname).replace('*.', '_.');
|
||||
@@ -298,10 +277,22 @@ function getCertificate(fqdn, domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
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) {
|
||||
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);
|
||||
|
||||
return getFallbackCertificate(domain, callback);
|
||||
@@ -309,17 +300,6 @@ function getCertificate(fqdn, domain, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function notifyCertChanged(vhost, callback) {
|
||||
assert.strictEqual(typeof vhost, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`notifyCertChanged: vhost: ${vhost} mailFqdn: ${settings.mailFqdn()}`);
|
||||
|
||||
if (vhost !== settings.mailFqdn()) return callback();
|
||||
|
||||
mail.handleCertChanged(callback);
|
||||
}
|
||||
|
||||
function ensureCertificate(vhost, domain, auditSource, callback) {
|
||||
assert.strictEqual(typeof vhost, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
@@ -329,23 +309,41 @@ function ensureCertificate(vhost, domain, auditSource, callback) {
|
||||
domains.get(domain, function (error, domainObject) {
|
||||
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);
|
||||
|
||||
getCertificateByHostname(vhost, domainObject, function (_error, currentBundle) {
|
||||
getAcmeCertificate(vhost, domainObject, function (_error, currentBundle) {
|
||||
if (currentBundle) {
|
||||
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 });
|
||||
debug(`ensureCertificate: ${vhost} cert require renewal`);
|
||||
debug(`ensureCertificate: ${vhost} cert requires renewal`);
|
||||
} else {
|
||||
debug(`ensureCertificate: ${vhost} cert does not exist`);
|
||||
}
|
||||
|
||||
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'}`);
|
||||
|
||||
eventlog.add(currentBundle ? eventlog.ACTION_CERTIFICATE_RENEWAL : eventlog.ACTION_CERTIFICATE_NEW, auditSource, { domain: vhost, errorMessage: error ? error.message : '' });
|
||||
@@ -355,19 +353,14 @@ function ensureCertificate(vhost, domain, auditSource, callback) {
|
||||
return callback(null, currentBundle, { renewed: false });
|
||||
}
|
||||
|
||||
notifyCertChanged(vhost, function (error) {
|
||||
if (certFilePath && keyFilePath) return callback(null, { certFilePath, keyFilePath }, { renewed: true });
|
||||
|
||||
debug(`ensureCertificate: renewal of ${vhost} failed. using fallback certificates for ${domain}`);
|
||||
|
||||
getFallbackCertificate(domain, function (error, bundle) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (certFilePath && keyFilePath) return callback(null, { certFilePath, keyFilePath }, { renewed: true });
|
||||
|
||||
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) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, bundle, { renewed: false });
|
||||
});
|
||||
callback(null, bundle, { renewed: false });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -375,7 +368,7 @@ function ensureCertificate(vhost, domain, auditSource, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function writeAdminNginxConfig(bundle, configFileName, vhost, callback) {
|
||||
function writeDashboardNginxConfig(bundle, configFileName, vhost, callback) {
|
||||
assert.strictEqual(typeof bundle, 'object');
|
||||
assert.strictEqual(typeof configFileName, 'string');
|
||||
assert.strictEqual(typeof vhost, 'string');
|
||||
@@ -384,12 +377,13 @@ function writeAdminNginxConfig(bundle, configFileName, vhost, callback) {
|
||||
var data = {
|
||||
sourceDir: path.resolve(__dirname, '..'),
|
||||
adminOrigin: settings.adminOrigin(),
|
||||
vhost: vhost, // if vhost is empty it will become the default_server
|
||||
vhost: vhost,
|
||||
hasIPv6: sysinfo.hasIPv6(),
|
||||
endpoint: 'admin',
|
||||
certFilePath: bundle.certFilePath,
|
||||
keyFilePath: bundle.keyFilePath,
|
||||
robotsTxtQuoted: JSON.stringify('User-agent: *\nDisallow: /\n')
|
||||
robotsTxtQuoted: JSON.stringify('User-agent: *\nDisallow: /\n'),
|
||||
proxyAuth: { enabled: false, id: null, path: '/' }
|
||||
};
|
||||
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
|
||||
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, configFileName);
|
||||
@@ -412,16 +406,16 @@ function configureAdmin(domain, auditSource, callback) {
|
||||
ensureCertificate(adminFqdn, domainObject.domain, auditSource, function (error, bundle) {
|
||||
if (error) return callback(error);
|
||||
|
||||
writeAdminNginxConfig(bundle, `${adminFqdn}.conf`, adminFqdn, callback);
|
||||
writeDashboardNginxConfig(bundle, `${adminFqdn}.conf`, adminFqdn, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function writeAdminConfig(domain, callback) {
|
||||
function writeDashboardConfig(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`writeAdminConfig: writing admin config for ${domain}`);
|
||||
debug(`writeDashboardConfig: writing admin config for ${domain}`);
|
||||
|
||||
domains.get(domain, function (error, domainObject) {
|
||||
if (error) return callback(error);
|
||||
@@ -431,7 +425,7 @@ function writeAdminConfig(domain, callback) {
|
||||
getCertificate(adminFqdn, domainObject.domain, function (error, bundle) {
|
||||
if (error) return callback(error);
|
||||
|
||||
writeAdminNginxConfig(bundle, `${adminFqdn}.conf`, adminFqdn, callback);
|
||||
writeDashboardNginxConfig(bundle, `${adminFqdn}.conf`, adminFqdn, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -458,13 +452,20 @@ function writeAppNginxConfig(app, bundle, callback) {
|
||||
adminOrigin: settings.adminOrigin(),
|
||||
vhost: app.fqdn,
|
||||
hasIPv6: sysinfo.hasIPv6(),
|
||||
port: app.httpPort,
|
||||
ip: app.containerIp,
|
||||
port: app.manifest.httpPort,
|
||||
endpoint: endpoint,
|
||||
certFilePath: bundle.certFilePath,
|
||||
keyFilePath: bundle.keyFilePath,
|
||||
robotsTxtQuoted,
|
||||
cspQuoted,
|
||||
hideHeaders
|
||||
hideHeaders,
|
||||
proxyAuth: {
|
||||
enabled: app.sso && app.manifest.addons && app.manifest.addons.proxyAuth,
|
||||
id: app.id,
|
||||
path: safe.query(app.manifest, 'addons.proxyAuth.path') || '/'
|
||||
},
|
||||
httpPaths: app.manifest.httpPaths || {}
|
||||
};
|
||||
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
|
||||
|
||||
@@ -495,7 +496,8 @@ function writeAppRedirectNginxConfig(app, fqdn, bundle, callback) {
|
||||
keyFilePath: bundle.keyFilePath,
|
||||
robotsTxtQuoted: null,
|
||||
cspQuoted: null,
|
||||
hideHeaders: []
|
||||
hideHeaders: [],
|
||||
proxyAuth: { enabled: false, id: app.id, path: '/' }
|
||||
};
|
||||
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
|
||||
|
||||
@@ -577,8 +579,13 @@ function renewCerts(options, auditSource, progressCallback, callback) {
|
||||
|
||||
var appDomains = [];
|
||||
|
||||
// add webadmin domain
|
||||
appDomains.push({ domain: settings.adminDomain(), fqdn: settings.adminFqdn(), type: 'webadmin', nginxConfigFilename: path.join(paths.NGINX_APPCONFIG_DIR, `${settings.adminFqdn()}.conf`) });
|
||||
// add webadmin and mail domain
|
||||
if (settings.mailFqdn() === settings.adminFqdn()) {
|
||||
appDomains.push({ domain: settings.adminDomain(), fqdn: settings.adminFqdn(), type: 'webadmin+mail', nginxConfigFilename: path.join(paths.NGINX_APPCONFIG_DIR, `${settings.adminFqdn()}.conf`) });
|
||||
} else {
|
||||
appDomains.push({ domain: settings.adminDomain(), fqdn: settings.adminFqdn(), type: 'webadmin', nginxConfigFilename: path.join(paths.NGINX_APPCONFIG_DIR, `${settings.adminFqdn()}.conf`) });
|
||||
appDomains.push({ domain: settings.mailDomain(), fqdn: settings.mailFqdn(), type: 'mail' });
|
||||
}
|
||||
|
||||
// add app main
|
||||
allApps.forEach(function (app) {
|
||||
@@ -587,8 +594,8 @@ function renewCerts(options, auditSource, progressCallback, callback) {
|
||||
appDomains.push({ domain: app.domain, fqdn: app.fqdn, type: 'main', app: app, nginxConfigFilename: path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf') });
|
||||
|
||||
app.alternateDomains.forEach(function (alternateDomain) {
|
||||
let nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, `${app.id}-redirect-${alternateDomain.fqdn}.conf`);
|
||||
appDomains.push({ domain: alternateDomain.domain, fqdn: alternateDomain.fqdn, type: 'alternate', app: app, nginxConfigFilename: nginxConfigFilename });
|
||||
const nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, `${app.id}-redirect-${alternateDomain.fqdn}.conf`);
|
||||
appDomains.push({ domain: alternateDomain.domain, fqdn: alternateDomain.fqdn, type: 'alternate', app: app, nginxConfigFilename });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -605,6 +612,8 @@ function renewCerts(options, auditSource, progressCallback, callback) {
|
||||
|
||||
if (state.renewed) renewed.push(appDomain.fqdn);
|
||||
|
||||
if (appDomain.type === 'mail') return iteratorCallback(); // mail has no nginx config to check current cert
|
||||
|
||||
// hack to check if the app's cert changed or not. this doesn't handle prod/staging le change since they use same file name
|
||||
let currentNginxConfig = safe.fs.readFileSync(appDomain.nginxConfigFilename, 'utf8') || '';
|
||||
if (currentNginxConfig.includes(bundle.certFilePath)) return iteratorCallback();
|
||||
@@ -612,13 +621,20 @@ function renewCerts(options, auditSource, progressCallback, callback) {
|
||||
debug(`renewCerts: creating new nginx config since ${appDomain.nginxConfigFilename} does not have ${bundle.certFilePath}`);
|
||||
|
||||
// reconfigure since the cert changed
|
||||
var configureFunc;
|
||||
if (appDomain.type === 'webadmin') configureFunc = writeAdminNginxConfig.bind(null, bundle, `${settings.adminFqdn()}.conf`, settings.adminFqdn());
|
||||
else if (appDomain.type === 'main') configureFunc = writeAppNginxConfig.bind(null, appDomain.app, bundle);
|
||||
else if (appDomain.type === 'alternate') configureFunc = writeAppRedirectNginxConfig.bind(null, appDomain.app, appDomain.fqdn, bundle);
|
||||
else return iteratorCallback(new BoxError(BoxError.INTERNAL_ERROR, `Unknown domain type for ${appDomain.fqdn}. This should never happen`));
|
||||
if (appDomain.type === 'webadmin') {
|
||||
return writeDashboardNginxConfig(bundle, `${settings.adminFqdn()}.conf`, settings.adminFqdn(), iteratorCallback);
|
||||
} else if (appDomain.type === 'webadmin+mail') {
|
||||
return async.series([
|
||||
mail.handleCertChanged,
|
||||
writeDashboardNginxConfig.bind(null, bundle, `${settings.adminFqdn()}.conf`, settings.adminFqdn())
|
||||
], iteratorCallback);
|
||||
} else if (appDomain.type === 'main') {
|
||||
return writeAppNginxConfig(appDomain.app, bundle, iteratorCallback);
|
||||
} else if (appDomain.type === 'alternate') {
|
||||
return writeAppRedirectNginxConfig(appDomain.app, appDomain.fqdn, bundle, iteratorCallback);
|
||||
}
|
||||
|
||||
configureFunc(iteratorCallback);
|
||||
iteratorCallback(new BoxError(BoxError.INTERNAL_ERROR, `Unknown domain type for ${appDomain.fqdn}. This should never happen`));
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
@@ -626,8 +642,10 @@ function renewCerts(options, auditSource, progressCallback, callback) {
|
||||
debug(`renewCerts: Renewed certs of ${JSON.stringify(renewed)}`);
|
||||
if (renewed.length === 0) return callback(null);
|
||||
|
||||
// reload nginx if any certs were updated but the config was not rewritten
|
||||
reload(callback);
|
||||
async.series([
|
||||
(next) => { return renewed.includes(settings.mailFqdn()) ? mail.handleCertChanged(next) : next(); },// mail cert renewed
|
||||
reload // reload nginx if any certs were updated but the config was not rewritten
|
||||
], callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -640,27 +658,41 @@ function removeAppConfigs() {
|
||||
}
|
||||
}
|
||||
|
||||
function writeDefaultConfig(callback) {
|
||||
function writeDefaultConfig(options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var certFilePath = path.join(paths.NGINX_CERT_DIR, 'default.cert');
|
||||
var keyFilePath = path.join(paths.NGINX_CERT_DIR, 'default.key');
|
||||
const certFilePath = path.join(paths.NGINX_CERT_DIR, 'default.cert');
|
||||
const keyFilePath = path.join(paths.NGINX_CERT_DIR, 'default.key');
|
||||
|
||||
if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) {
|
||||
debug('writeDefaultConfig: create new cert');
|
||||
|
||||
var cn = 'cloudron-' + (new Date()).toISOString(); // randomize date a bit to keep firefox happy
|
||||
if (!safe.child_process.execSync(`openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 3650 -subj /CN=${cn} -nodes`)) {
|
||||
const cn = 'cloudron-' + (new Date()).toISOString(); // randomize date a bit to keep firefox happy
|
||||
// the days field is chosen to be less than 825 days per apple requirement (https://support.apple.com/en-us/HT210176)
|
||||
if (!safe.child_process.execSync(`openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 800 -subj /CN=${cn} -nodes`)) {
|
||||
debug(`writeDefaultConfig: could not generate certificate: ${safe.error.message}`);
|
||||
return callback(new BoxError(BoxError.OPENSSL_ERROR, safe.error));
|
||||
}
|
||||
}
|
||||
|
||||
writeAdminNginxConfig({ certFilePath, keyFilePath }, constants.NGINX_DEFAULT_CONFIG_FILE_NAME, '', function (error) {
|
||||
if (error) return callback(error);
|
||||
const data = {
|
||||
sourceDir: path.resolve(__dirname, '..'),
|
||||
adminOrigin: settings.adminOrigin(),
|
||||
vhost: '',
|
||||
hasIPv6: sysinfo.hasIPv6(),
|
||||
endpoint: options.activated ? 'ip' : 'setup',
|
||||
certFilePath,
|
||||
keyFilePath,
|
||||
robotsTxtQuoted: JSON.stringify('User-agent: *\nDisallow: /\n'),
|
||||
proxyAuth: { enabled: false, id: null, path: '/' }
|
||||
};
|
||||
const nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
|
||||
const nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, constants.NGINX_DEFAULT_CONFIG_FILE_NAME);
|
||||
|
||||
debug('writeDefaultConfig: done');
|
||||
debug(`writeDefaultConfig: writing configs for endpoint "${data.endpoint}"`);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) return callback(new BoxError(BoxError.FS_ERROR, safe.error));
|
||||
|
||||
reload(callback);
|
||||
}
|
||||
|
||||
+58
-48
@@ -1,49 +1,50 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
getApp: getApp,
|
||||
getApps: getApps,
|
||||
getAppIcon: getAppIcon,
|
||||
install: install,
|
||||
uninstall: uninstall,
|
||||
restore: restore,
|
||||
importApp: importApp,
|
||||
backup: backup,
|
||||
update: update,
|
||||
getLogs: getLogs,
|
||||
getLogStream: getLogStream,
|
||||
listBackups: listBackups,
|
||||
repair: repair,
|
||||
getApp,
|
||||
getApps,
|
||||
getAppIcon,
|
||||
install,
|
||||
uninstall,
|
||||
restore,
|
||||
importApp,
|
||||
exportApp,
|
||||
backup,
|
||||
update,
|
||||
getLogs,
|
||||
getLogStream,
|
||||
listBackups,
|
||||
repair,
|
||||
|
||||
setAccessRestriction: setAccessRestriction,
|
||||
setLabel: setLabel,
|
||||
setTags: setTags,
|
||||
setIcon: setIcon,
|
||||
setMemoryLimit: setMemoryLimit,
|
||||
setCpuShares: setCpuShares,
|
||||
setAutomaticBackup: setAutomaticBackup,
|
||||
setAutomaticUpdate: setAutomaticUpdate,
|
||||
setReverseProxyConfig: setReverseProxyConfig,
|
||||
setCertificate: setCertificate,
|
||||
setDebugMode: setDebugMode,
|
||||
setEnvironment: setEnvironment,
|
||||
setMailbox: setMailbox,
|
||||
setLocation: setLocation,
|
||||
setDataDir: setDataDir,
|
||||
setBinds: setBinds,
|
||||
setAccessRestriction,
|
||||
setLabel,
|
||||
setTags,
|
||||
setIcon,
|
||||
setMemoryLimit,
|
||||
setCpuShares,
|
||||
setAutomaticBackup,
|
||||
setAutomaticUpdate,
|
||||
setReverseProxyConfig,
|
||||
setCertificate,
|
||||
setDebugMode,
|
||||
setEnvironment,
|
||||
setMailbox,
|
||||
setLocation,
|
||||
setDataDir,
|
||||
setMounts,
|
||||
|
||||
stop: stop,
|
||||
start: start,
|
||||
restart: restart,
|
||||
exec: exec,
|
||||
execWebSocket: execWebSocket,
|
||||
stop,
|
||||
start,
|
||||
restart,
|
||||
exec,
|
||||
execWebSocket,
|
||||
|
||||
clone: clone,
|
||||
clone,
|
||||
|
||||
uploadFile: uploadFile,
|
||||
downloadFile: downloadFile,
|
||||
uploadFile,
|
||||
downloadFile,
|
||||
|
||||
load: load
|
||||
load
|
||||
};
|
||||
|
||||
var apps = require('../apps.js'),
|
||||
@@ -443,6 +444,17 @@ function importApp(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function exportApp(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
|
||||
apps.exportApp(req.resource, {}, auditSource.fromRequest(req), function (error, result) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, { taskId: result.taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
function clone(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
assert.strictEqual(typeof req.resource, 'object');
|
||||
@@ -597,7 +609,7 @@ function getLogs(req, res, next) {
|
||||
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/x-logs',
|
||||
'Content-Disposition': 'attachment; filename="log.txt"',
|
||||
'Content-Disposition': `attachment; filename="${req.resource.id}.log"`,
|
||||
'Cache-Control': 'no-cache',
|
||||
'X-Accel-Buffering': 'no' // disable nginx buffering
|
||||
});
|
||||
@@ -766,22 +778,20 @@ function downloadFile(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function setBinds(req, res, next) {
|
||||
function setMounts(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'));
|
||||
if (!Array.isArray(req.body.mounts)) return next(new HttpError(400, 'mounts should be an array'));
|
||||
for (let m of req.body.mounts) {
|
||||
if (!m || typeof m !== 'object') return next(new HttpError(400, 'mounts must be an object'));
|
||||
if (typeof m.volumeId !== 'string') return next(new HttpError(400, 'volumeId must be a string'));
|
||||
if (typeof m.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) {
|
||||
apps.setMounts(req.resource, req.body.mounts, auditSource.fromRequest(req), function (error, result) {
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, { taskId: result.taskId }));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+10
-1
@@ -3,7 +3,8 @@
|
||||
exports = module.exports = {
|
||||
list: list,
|
||||
startBackup: startBackup,
|
||||
cleanup: cleanup
|
||||
cleanup: cleanup,
|
||||
check: check
|
||||
};
|
||||
|
||||
let auditSource = require('../auditsource.js'),
|
||||
@@ -41,3 +42,11 @@ function cleanup(req, res, next) {
|
||||
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 }));
|
||||
});
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user