Compare commits
470 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ca60d4c8b8 | |||
| 2ce00ca0d7 | |||
| a57db15b63 | |||
| f14a8b0ab0 | |||
| 5f207716e5 | |||
| 010d48035b | |||
| 7e6a83df84 | |||
| ec4910a45e | |||
| 6558c78094 | |||
| 5df92d1903 | |||
| 05affa7d26 | |||
| 46c6c5a5a8 | |||
| 75da751c72 | |||
| b84f60671e | |||
| 8dcb06cb02 | |||
| 83bf739081 | |||
| 48a52fae2e | |||
| 0ddbda6068 | |||
| 360fa058ea | |||
| 489d2022e6 | |||
| f762d0c0a1 | |||
| 98cad0678d | |||
| 92acb2954f | |||
| 00a6e4c982 | |||
| bf9eb4bd87 | |||
| 2f4940acbd | |||
| 9f7ca552a6 | |||
| 4272d5be8a | |||
| 1babfb6e87 | |||
| 5663cf45f8 | |||
| d8cb2d1d25 | |||
| 174a60bb07 | |||
| 3d7094bf28 | |||
| 4d6616930a | |||
| 24875ba292 | |||
| c58b2677b6 | |||
| 25146e1134 | |||
| c0c35964fe | |||
| 0bf9ab0a2b | |||
| 6d86f4cbda | |||
| d2741bbeb9 | |||
| 690d02a353 | |||
| c629db9597 | |||
| 67fcf85abb | |||
| 527eace8f8 | |||
| e65230b833 | |||
| 3e8334040b | |||
| 2bcd3a8e4d | |||
| e75b85fc3a | |||
| c4362d3339 | |||
| 85e492a632 | |||
| b8d4b67043 | |||
| ffacd31259 | |||
| 19f6da88da | |||
| c0faae4e27 | |||
| a19c566eea | |||
| 3ec806452c | |||
| 0c73cd5219 | |||
| 9b6bf719ff | |||
| 25431d3cc4 | |||
| e0805df3b1 | |||
| 8392fec570 | |||
| 1c173ca83f | |||
| 05a67db761 | |||
| bb24d5cf9e | |||
| 8d2fbe931f | |||
| 0a8adaac9f | |||
| fa6d151325 | |||
| a7296a0339 | |||
| a6aee53ec2 | |||
| 963ab2e791 | |||
| ca724b8b03 | |||
| 88a929c85e | |||
| 2bc0270880 | |||
| 014b77b7aa | |||
| 06f8aa8f29 | |||
| a8c64bf9f7 | |||
| 41ef16fbec | |||
| 2a848a481b | |||
| 3963d76a80 | |||
| 8ede37a43d | |||
| 36534f6bb2 | |||
| 7eddcaf708 | |||
| d8d2572aa1 | |||
| 96a98a74ac | |||
| d0a244e392 | |||
| f09c89e33f | |||
| d53f0679e5 | |||
| 527093ebcb | |||
| bd5835b866 | |||
| 6dd70c0ef2 | |||
| acc90e16d7 | |||
| 4b3aca7413 | |||
| 8daee764d2 | |||
| 3dedda32d4 | |||
| d127b25f0f | |||
| 6a2b0eedb3 | |||
| 8c81a97a4b | |||
| d9ab1a78d5 | |||
| 593df8ed49 | |||
| b30def3620 | |||
| 9c02785d49 | |||
| f747343159 | |||
| 2971910ccf | |||
| 56534b9647 | |||
| a8d26067ee | |||
| 4212e4bb00 | |||
| 7b27ace7bf | |||
| d8944da68d | |||
| 433d797cb7 | |||
| 0b1d940128 | |||
| 6016024026 | |||
| e199293229 | |||
| 2ebe92fec3 | |||
| 628cf1e3de | |||
| 9e9aaf68f0 | |||
| b595ca422c | |||
| 9273a6c726 | |||
| 76d00d4e65 | |||
| 668c03a11b | |||
| 1e72d2d651 | |||
| 89fc8efc67 | |||
| 241dbf160e | |||
| e46bdc2caa | |||
| e1cb91ca76 | |||
| 709c742c46 | |||
| ecad9c499c | |||
| ed0879ffcd | |||
| 61e2878b08 | |||
| d97034bfb2 | |||
| 21942552d6 | |||
| dd68c8f91f | |||
| 28ce5f41e3 | |||
| 5694e676bd | |||
| db8c5a116f | |||
| fa39f0fbf3 | |||
| 1444bb038f | |||
| ac9e421ecf | |||
| b60cbe5a55 | |||
| 56d794745b | |||
| fd3b73bea2 | |||
| 78807782df | |||
| 754b29b263 | |||
| 9f97f48634 | |||
| 815e5d9d9a | |||
| 91ec2eaaf5 | |||
| f8d3a7cadd | |||
| d04a09b015 | |||
| 5d997bcc89 | |||
| f0dd90a1f5 | |||
| ee8ee8e786 | |||
| ee1a4411f8 | |||
| df6e6cb071 | |||
| ba5645a20e | |||
| ca502a2d55 | |||
| ecd53b48db | |||
| b9efb0b50b | |||
| 3fb5034ebd | |||
| afed3f3725 | |||
| b4f14575d7 | |||
| f437a1f48c | |||
| c3d7d867be | |||
| 96c16cd5d2 | |||
| af182e3df6 | |||
| d70ff7cd5b | |||
| 38331e71e2 | |||
| 322a9a18d7 | |||
| 423ef546a9 | |||
| e3f3241966 | |||
| eaef384ea5 | |||
| b85bc3aa01 | |||
| 01154d0ae6 | |||
| 6494050d66 | |||
| 8c7223ceed | |||
| 21afc71d89 | |||
| 7bf70956a1 | |||
| 9e9b8b095e | |||
| 0f543e6703 | |||
| f9973e765c | |||
| e089851ae9 | |||
| c524d68c2f | |||
| 5cccb50a31 | |||
| 3d375b687a | |||
| a93d453963 | |||
| f8ac2d4628 | |||
| d5ba73716b | |||
| 954224dafb | |||
| 8b341e2bf8 | |||
| 78fb9401ee | |||
| 4a5cbab194 | |||
| 19999abc50 | |||
| 5123b669d7 | |||
| 565c8445e1 | |||
| 404a019c56 | |||
| 24dee80aa6 | |||
| ce6df4bf96 | |||
| f8f6c7d93e | |||
| bafc6dce98 | |||
| 56ee4d8e25 | |||
| eeef221b4e | |||
| 4674653982 | |||
| a34180c27b | |||
| aa8ce2c62e | |||
| b3c6b8aa15 | |||
| 44a7a2579c | |||
| 39f0e476f2 | |||
| 003dc0dbaf | |||
| e39329218d | |||
| 8d3fbc5432 | |||
| 2780de631e | |||
| 399c756735 | |||
| 859311f9e5 | |||
| a9e89b57d9 | |||
| 4e68abe51d | |||
| 12083f5608 | |||
| d1efb2db56 | |||
| adde28523f | |||
| f122f46fe2 | |||
| ad7fadb4a9 | |||
| be383582e0 | |||
| 0a60365143 | |||
| 2f6cb3e913 | |||
| b0f85678d4 | |||
| e43413e063 | |||
| e39a5c8872 | |||
| fb4b75dd2a | |||
| 3c1ccc5cf4 | |||
| abd66d6524 | |||
| b61b7f80b5 | |||
| efa850614d | |||
| 21c534c806 | |||
| 7e4ff2440c | |||
| f415e19f6f | |||
| 97da8717ca | |||
| cbddb79d15 | |||
| bffb935f0f | |||
| e50e0f730b | |||
| 26f33a8e9b | |||
| 952b1f6304 | |||
| a3293c4c35 | |||
| 4892473eff | |||
| 221d5f95e1 | |||
| 84649b9471 | |||
| 44435559ab | |||
| c351660a9a | |||
| 0a24130fd4 | |||
| ea13f8f97e | |||
| d00801d020 | |||
| 8ced0aa78e | |||
| f5d32a9178 | |||
| 7fc45b3215 | |||
| 9bed14a3e8 | |||
| 71233ecd95 | |||
| 02097298c6 | |||
| be03dd0821 | |||
| 5b77d2f0cf | |||
| 781f543e87 | |||
| 6525a467a2 | |||
| 6cddd61a24 | |||
| b0ee116004 | |||
| 867a59d5d8 | |||
| 6f5085ebc3 | |||
| e8a93dcb1b | |||
| 09fe957cc7 | |||
| 020ccc8a99 | |||
| 7ed304bed8 | |||
| db1e39be11 | |||
| f163577264 | |||
| 9c7080aea1 | |||
| c05a7c188f | |||
| 72e912770a | |||
| 28c06d0a72 | |||
| 9805daa835 | |||
| a920fd011c | |||
| 1b979ee1e9 | |||
| 70eae477dc | |||
| c16f7c7891 | |||
| 63b8a5b658 | |||
| c0bf51b79f | |||
| 3d4178b35c | |||
| 34878bbc6a | |||
| e78d976c8f | |||
| ba9662f3fa | |||
| c8750a3bed | |||
| 9710f74250 | |||
| 52095cb8ab | |||
| c612966b41 | |||
| 90cf4f0784 | |||
| ec93d564e9 | |||
| 37f9e60978 | |||
| ca199961d5 | |||
| fd811ac334 | |||
| 609c1d3b78 | |||
| 9906ed37ae | |||
| dcdce6d995 | |||
| 9026c555f9 | |||
| 547a80f17b | |||
| 300d3dd545 | |||
| 6fce729ed2 | |||
| d233ee2a83 | |||
| 3240a71feb | |||
| 322be9e5ba | |||
| e67ecae2d2 | |||
| 75b3e7fc78 | |||
| 74c8d8cc6b | |||
| 51659a8d2d | |||
| 70acf1a719 | |||
| 8d2f3b0217 | |||
| e498678488 | |||
| 513517b15e | |||
| a96f8abaca | |||
| f7bcd54ef5 | |||
| d58e4f58c7 | |||
| 45f0f2adbe | |||
| 36c72dd935 | |||
| df9e2a7856 | |||
| 2b043aa95f | |||
| c0a09d1494 | |||
| 1c5c4b5705 | |||
| b56dcaac68 | |||
| fd91ccc844 | |||
| fca1a70eaa | |||
| ed81b7890c | |||
| cb8dcbf3dd | |||
| 4bdbf1f62e | |||
| 47a8b4fdc2 | |||
| 5720e90580 | |||
| f98e13d701 | |||
| d5d924861b | |||
| b81a92d407 | |||
| 22b0100354 | |||
| 6eb6eab3f4 | |||
| 57d5c2cc47 | |||
| 6a9eac7a24 | |||
| e4760a07f0 | |||
| 257e594de0 | |||
| 6fea022a04 | |||
| f34840d127 | |||
| f9706d6a05 | |||
| 61f7c1af48 | |||
| 00786dda05 | |||
| 8b9f44addc | |||
| 56c7dbb6e4 | |||
| c47f878203 | |||
| 8a2107e6eb | |||
| cd9f0f69d8 | |||
| 1da91b64f6 | |||
| a87dd65c1d | |||
| 7c63d9e758 | |||
| 329bf596ac | |||
| 2a57c4269a | |||
| ca8813dce3 | |||
| 3aebf51360 | |||
| 103f8db8cb | |||
| 04c127b78d | |||
| 9bef1bcf64 | |||
| 718413c089 | |||
| a34691df44 | |||
| 795e38fe82 | |||
| 1d348fb0f3 | |||
| 91f3318879 | |||
| c61808f4c6 | |||
| 991b2dad28 | |||
| f3d9a70de7 | |||
| 60758de10a | |||
| 6a0ef7a1c1 | |||
| 7cb451c157 | |||
| 3c31c96ad4 | |||
| 5d73f58631 | |||
| 4ca7cccdae | |||
| 82380b6b7c | |||
| 979c4e77e3 | |||
| e318fb0c01 | |||
| 77d2fb97e5 | |||
| 24e6c4d963 | |||
| 064c5cf7f2 | |||
| 891542bfb9 | |||
| 599702d410 | |||
| 3cb39754fd | |||
| f04345a99a | |||
| 3d59b8a5b0 | |||
| cf518b0285 | |||
| 52832c881a | |||
| 537fbff4aa | |||
| e3040b334d | |||
| 6c2879d567 | |||
| 595c89076f | |||
| c85f5b15c6 | |||
| 8fbed7e84b | |||
| ee3c5f67af | |||
| 52db28e876 | |||
| 65bc3491f6 | |||
| 82f512dc27 | |||
| 4b41378d08 | |||
| 1fd4e27d92 | |||
| 2420fef6b1 | |||
| 50074b936a | |||
| f98e68edc1 | |||
| 83e5daf08c | |||
| 53b43ca36b | |||
| d11842a7f8 | |||
| 6746781b46 | |||
| 78ec8e5c0c | |||
| 67a2ba957e | |||
| 9e558924bb | |||
| afcb3dd237 | |||
| 054de4813d | |||
| 57891c64b5 | |||
| 26361c037d | |||
| 2048b03431 | |||
| c12aba6c00 | |||
| 0bd0857189 | |||
| 978893250f | |||
| d0f4a76ca2 | |||
| 755c87b079 | |||
| 1da073c9bf | |||
| 96ead77520 | |||
| 178b04fead | |||
| 335631ac28 | |||
| 42778cb84d | |||
| 2f51088e67 | |||
| 378d7aee91 | |||
| ac53f8c747 | |||
| 5fe73c5a46 | |||
| a6f13eee14 | |||
| 86d23a4d35 | |||
| b25bb76792 | |||
| 7ba5d1e0d6 | |||
| f17bde2d97 | |||
| 93cafebfdb | |||
| 5538a91585 | |||
| 09cb468290 | |||
| 3b98eb0543 | |||
| 59936c6fbf | |||
| 5a7e636f2d | |||
| 401dc37a50 | |||
| 9f1af572a0 | |||
| 96e2fa159c | |||
| bc49a3e18a | |||
| ae19d8d754 | |||
| 13b067eb88 | |||
| af08f4e7b6 | |||
| 534e5781ba | |||
| 737e266729 | |||
| 07a133ebe9 | |||
| b0444edf7e | |||
| bcf37d833f | |||
| e7db2ab137 | |||
| 125b416463 | |||
| 800468fbe6 | |||
| 0c1e3ec6a0 | |||
| d97ee5d425 | |||
| a1be30c35a | |||
| ba3cb3b646 | |||
| daadefe6b9 | |||
| 12c849398e | |||
| 392492be04 | |||
| 4fd0c3c66c | |||
| 7d2e6d8d4d | |||
| f3e7249bdc | |||
| 53afb2606a | |||
| fbce71d031 | |||
| 1adde7d8e8 | |||
| d23599ba24 | |||
| ac35bcf9f0 | |||
| e4c5dfda60 | |||
| 99cfe564ae | |||
| 70a3cdc9bc | |||
| bd52068695 | |||
| ae54b57ca7 |
@@ -888,10 +888,25 @@
|
||||
[1.0.0]
|
||||
* Make selfhosting great again
|
||||
|
||||
[1.0.1]
|
||||
* Notification improvements
|
||||
|
||||
[1.0.2]
|
||||
* Notification improvements
|
||||
|
||||
[1.1.0]
|
||||
* Add support for email catch-all
|
||||
* Support Cloudrons on subdomains
|
||||
|
||||
[1.1.1]
|
||||
* Notification improvements
|
||||
|
||||
[1.1.2]
|
||||
* Notification improvements
|
||||
|
||||
[1.1.3]
|
||||
* Notification improvements
|
||||
|
||||
[1.2.0]
|
||||
* Relay emails optionally via external SMTP server email (mailgun, sendgrid etc)
|
||||
* (experimental) Preserver the docker storage driver across updates
|
||||
@@ -918,3 +933,208 @@
|
||||
* Ensure Cloudron is only be installed on EXT4 root file system (required by Docker)
|
||||
* Mark app package major releases as blocking and require approval by Cloudron admin
|
||||
|
||||
[1.4.1]
|
||||
* Do not display backup region when using minio and exoscale SOS
|
||||
* Fix javascript error in email view
|
||||
* Add html version of the digest email
|
||||
* Fix issue where collectd was collecting information about devicemapper mounts
|
||||
|
||||
[1.5.0]
|
||||
* Update node to 6.11.2
|
||||
* Add a new view to display platform and app logs
|
||||
* Rework web UI to use flexbox
|
||||
* Add motd message to warn admins that to not run 'apt upgrade'
|
||||
* Switch default storage backend for new Cloudrons to overlay2
|
||||
* Add a custom graphite plugin to collect disk usage statistics
|
||||
* Rotate logs of all apps automatically
|
||||
|
||||
[1.6.0]
|
||||
* Allow apps to have 'network' capability (thanks @mehdi)
|
||||
* Fix crash in collectd disk usage collection script
|
||||
* Fix layout issues in update and oauth views
|
||||
* Use maxsize rule instead of size in lograte configs
|
||||
* Make it possible to skip backups per-app
|
||||
* Hide restore button for noop backend
|
||||
* Add popups and warnings for noop backend
|
||||
* Add webterminal to shell into apps from the admin UI
|
||||
* Update Haraka for a few crash fixes
|
||||
|
||||
[1.6.1]
|
||||
* Patch release for 1.6.0 to fix regressions
|
||||
* Allow apps to have 'network' capability (thanks @mehdi)
|
||||
* Fix crash in collectd disk usage collection script
|
||||
* Fix layout issues in update and oauth views
|
||||
* Use maxsize rule instead of size in lograte configs
|
||||
* Make it possible to skip backups per-app
|
||||
* Hide restore button for noop backend
|
||||
* Add popups and warnings for noop backend
|
||||
* Add webterminal to shell into apps from the admin UI
|
||||
* Update Haraka for a few crash fixes
|
||||
|
||||
[1.6.2]
|
||||
* Allow apps to have 'network' capability (thanks @mehdi)
|
||||
* Fix crash in collectd disk usage collection script
|
||||
* Fix layout issues in update and oauth views
|
||||
* Use maxsize rule instead of size in lograte configs
|
||||
* Make it possible to skip backups per-app
|
||||
* Hide restore button for noop backend
|
||||
* Add popups and warnings for noop backend
|
||||
* Add webterminal to shell into apps from the admin UI
|
||||
* Update Haraka for a few crash fixes
|
||||
|
||||
[1.6.3]
|
||||
* Fixes selection issue while clicking on empty flexbox space
|
||||
* Indicate directories can be downloaded in the web terminal
|
||||
* Do not show app update indicator for normal users
|
||||
* Display email notice when using Cloudflare DNS
|
||||
* Set MX records correctly when using Cloudflare DNS
|
||||
* Fix bug where webterminal can incorrectly appear in main view
|
||||
* Do not crash if DNS credentials are invalid
|
||||
|
||||
[1.6.4]
|
||||
* More descriptive Postmark email relay form
|
||||
* Fix file upload in chrome
|
||||
* Support Ctrl/Cmd+v webterminal pasting
|
||||
* Ensure unbound always starts up
|
||||
* Add option to run app in repair mode
|
||||
|
||||
[1.6.5]
|
||||
* DigitalOcean DNS: Add pagination
|
||||
* Cloudflare DNS: Optimize listing of DNS entries
|
||||
* Update node to 6.11.3
|
||||
* App volumes can now be symlinked individually to external storage
|
||||
* Periodically check if IP is blacklisted and notify admins
|
||||
* Do not ask password when re-configuring app (since it is non-destructive)
|
||||
* Move mail data inside boxdata directory. This makes the no-op backend more useful
|
||||
* Remove collectd stats when app is uninstalled
|
||||
|
||||
[1.7.0]
|
||||
* Add rsync format for backups. This feature allows incremental backups
|
||||
* Add Google DNS backend (thanks @syn)
|
||||
* Add DigitalOcean spaces backup storage backend
|
||||
* Add Cloudscale and Exoscale as supported VPS providers
|
||||
* Display backup progress and status in the web interface
|
||||
* Preliminary IPv6 support
|
||||
* Add IP RBL status to web interface
|
||||
* Add auto-update pattern `Every wednesday night`
|
||||
* Update Haraka to 2.8.15. This fixes the issue where emails were bounced with the message 'Send MAIL FROM first'
|
||||
* Do not overwrite existing subdomain when app's location is changed
|
||||
* Add button to send test email
|
||||
* Fix crash in carbon which made graphs disappear on some Cloudrons
|
||||
|
||||
[1.7.1]
|
||||
* Add rsync format for backups. This feature allows incremental backups
|
||||
* Add Google DNS backend (thanks @syn)
|
||||
* Add DigitalOcean spaces backup storage backend
|
||||
* Add Cloudscale and Exoscale as supported VPS providers
|
||||
* Display backup progress and status in the web interface
|
||||
* Preliminary IPv6 support
|
||||
* Add IP RBL status to web interface
|
||||
* Add auto-update pattern `Every wednesday night`
|
||||
* Update Haraka to 2.8.15. This fixes the issue where emails were bounced with the message 'Send MAIL FROM first'
|
||||
* Do not overwrite existing subdomain when app's location is changed
|
||||
* Add button to send test email
|
||||
* Fix crash in carbon which made graphs disappear on some Cloudrons
|
||||
|
||||
[1.7.2]
|
||||
* Add rsync format for backups. This feature allows incremental backups
|
||||
* Add Google DNS backend (thanks @syn)
|
||||
* Add Cloudscale and Exoscale as supported VPS providers
|
||||
* Display backup progress and status in the web interface
|
||||
* Preliminary IPv6 support
|
||||
* Add IP RBL status to web interface
|
||||
* Add auto-update pattern `Every wednesday night`
|
||||
* Update Haraka to 2.8.15. This fixes the issue where emails were bounced with the message 'Send MAIL FROM first'
|
||||
* Do not overwrite existing subdomain when app's location is changed
|
||||
* Add button to send test email
|
||||
* Fix crash in carbon which made graphs disappear on some Cloudrons
|
||||
* Fix issue where OAuth SSO did not work when alternate domain was used
|
||||
|
||||
[1.7.3]
|
||||
* Add rsync format for backups. This feature allows incremental backups
|
||||
* Add Google DNS backend (thanks @syn)
|
||||
* Add Cloudscale and Exoscale as supported VPS providers
|
||||
* Display backup progress and status in the web interface
|
||||
* Preliminary IPv6 support
|
||||
* Add IP RBL status to web interface
|
||||
* Add auto-update pattern `Every wednesday night`
|
||||
* Update Haraka to 2.8.15. This fixes the issue where emails were bounced with the message 'Send MAIL FROM first'
|
||||
* Do not overwrite existing subdomain when app's location is changed
|
||||
* Add button to send test email
|
||||
* Fix crash in carbon which made graphs disappear on some Cloudrons
|
||||
* Fix issue where OAuth SSO did not work when alternate domain was used
|
||||
|
||||
[1.7.4]
|
||||
* Add rsync format for backups. This feature allows incremental backups
|
||||
* Add Google DNS backend (thanks @syn)
|
||||
* Add DigitalOcean spaces backup storage backend
|
||||
* Add Cloudscale and Exoscale as supported VPS providers
|
||||
* Display backup progress and status in the web interface
|
||||
* Preliminary IPv6 support
|
||||
* Add IP RBL status to web interface
|
||||
* Add auto-update pattern `Every wednesday night`
|
||||
* Update Haraka to 2.8.15. This fixes the issue where emails were bounced with the message 'Send MAIL FROM first'
|
||||
* Do not overwrite existing subdomain when app's location is changed
|
||||
* Add button to send test email
|
||||
* Fix crash in carbon which made graphs disappear on some Cloudrons
|
||||
* Fix issue where OAuth SSO did not work when alternate domain was used
|
||||
* Changelog is now rendered in markdown format
|
||||
|
||||
[1.7.5]
|
||||
* Expose a TLS relay port from mail container for Go applications
|
||||
|
||||
[1.7.6]
|
||||
* Port bindings cannot be configured in update route anymore
|
||||
* Implement LDAP group compare
|
||||
* Pre-releases are now offered by appstore and not handled in box code anymore
|
||||
* LDAP pagination support. This will fix the warnings in NextCloud and Rocket.Chat
|
||||
* Check if directories can be created in the backup directory
|
||||
* Do not set the HTTPS agent when using HTTP with minio backup backend
|
||||
* Fix regression where a new domain config could not be set in the UI
|
||||
* New mail container release that fixes email sending with SOGo
|
||||
* Show 404 page for unknown domains
|
||||
|
||||
[1.7.7]
|
||||
* Allow setting app memory till memory limit
|
||||
* Make the dkim selector dynamic
|
||||
* Fix issue where app update dialog did not close
|
||||
* Fix LE cert renewal failures
|
||||
* Send user and cert info in digest emails
|
||||
* Send oom, app failures and other important mails to cloudron owner's alt mail
|
||||
|
||||
[1.8.0]
|
||||
* Fix group email bounce when a group has users that have not signed up yet
|
||||
* Do not restrict app memory limit to 4GB
|
||||
* Fix display of the latest backup in the weekly digest
|
||||
* Add UI to select users for access restriction
|
||||
* Update docker to 17.09
|
||||
* Update node to 6.11.5
|
||||
* Display package version of installed apps in the info dialog
|
||||
|
||||
[1.8.1]
|
||||
* Update node modules
|
||||
* Allow a restore operation if app is already restoring
|
||||
* Remove pre-install bundle support since it was hardly used
|
||||
* Make the test email mail address configurable
|
||||
* Allow admins to access all apps
|
||||
* Send feedback via appstore API (instead of email)
|
||||
* Show documentation URL in the app info dialog
|
||||
* Update Lets Encrypt agrement URL (https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf)
|
||||
|
||||
[1.8.2]
|
||||
* Update node modules
|
||||
* Allow a restore operation if app is already restoring
|
||||
* Remove pre-install bundle support since it was hardly used
|
||||
* Make the test email mail address configurable
|
||||
* Allow admins to access all apps
|
||||
* Send feedback via appstore API (instead of email)
|
||||
* Show documentation URL in the app info dialog
|
||||
* Update Lets Encrypt agrement URL (https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf)
|
||||
|
||||
[1.8.3]
|
||||
* Ensure domain database record exists
|
||||
|
||||
[1.8.4]
|
||||
* Fix issue where internal email was not delivered when email relay is enabled
|
||||
* Fix display of DNS records when email relay is enabled
|
||||
|
||||
|
||||
@@ -630,7 +630,7 @@ state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
box
|
||||
Copyright (C) 2016 Cloudron UG
|
||||
Copyright (C) 2016,2017 Cloudron UG
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
|
||||
@@ -39,17 +39,18 @@ apt-get -y install \
|
||||
rcconf \
|
||||
swaks \
|
||||
unattended-upgrades \
|
||||
unbound
|
||||
unbound \
|
||||
xfsprogs
|
||||
|
||||
# this ensures that unattended upgades are enabled, if it was disabled during ubuntu install time (see #346)
|
||||
# debconf-set-selection of unattended-upgrades/enable_auto_updates + dpkg-reconfigure does not work
|
||||
cp /usr/share/unattended-upgrades/20auto-upgrades /etc/apt/apt.conf.d/20auto-upgrades
|
||||
|
||||
echo "==> Installing node.js"
|
||||
mkdir -p /usr/local/node-6.11.1
|
||||
curl -sL https://nodejs.org/dist/v6.11.1/node-v6.11.1-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-6.11.1
|
||||
ln -sf /usr/local/node-6.11.1/bin/node /usr/bin/node
|
||||
ln -sf /usr/local/node-6.11.1/bin/npm /usr/bin/npm
|
||||
mkdir -p /usr/local/node-6.11.5
|
||||
curl -sL https://nodejs.org/dist/v6.11.5/node-v6.11.5-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-6.11.5
|
||||
ln -sf /usr/local/node-6.11.5/bin/node /usr/bin/node
|
||||
ln -sf /usr/local/node-6.11.5/bin/npm /usr/bin/npm
|
||||
apt-get install -y python # Install python which is required for npm rebuild
|
||||
[[ "$(python --version 2>&1)" == "Python 2.7."* ]] || die "Expecting python version to be 2.7.x"
|
||||
|
||||
@@ -58,16 +59,16 @@ echo "==> Installing Docker"
|
||||
|
||||
# create systemd drop-in file
|
||||
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=devicemapper" > /etc/systemd/system/docker.service.d/cloudron.conf
|
||||
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
|
||||
|
||||
curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_17.03.1~ce-0~ubuntu-xenial_amd64.deb -o /tmp/docker.deb
|
||||
curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_17.09.0~ce-0~ubuntu_amd64.deb -o /tmp/docker.deb
|
||||
# apt install with install deps (as opposed to dpkg -i)
|
||||
apt install -y /tmp/docker.deb
|
||||
rm /tmp/docker.deb
|
||||
|
||||
storage_driver=$(docker info | grep "Storage Driver" | sed 's/.*: //')
|
||||
if [[ "${storage_driver}" != "devicemapper" ]]; then
|
||||
echo "Docker is using "${storage_driver}" instead of devicemapper"
|
||||
if [[ "${storage_driver}" != "overlay2" ]]; then
|
||||
echo "Docker is using "${storage_driver}" instead of overlay2"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
+3
-2
@@ -6,9 +6,9 @@ var argv = require('yargs').argv,
|
||||
autoprefixer = require('gulp-autoprefixer'),
|
||||
concat = require('gulp-concat'),
|
||||
cssnano = require('gulp-cssnano'),
|
||||
del = require('del'),
|
||||
ejs = require('gulp-ejs'),
|
||||
gulp = require('gulp'),
|
||||
rimraf = require('rimraf'),
|
||||
sass = require('gulp-sass'),
|
||||
serve = require('gulp-serve'),
|
||||
sourcemaps = require('gulp-sourcemaps'),
|
||||
@@ -196,7 +196,8 @@ gulp.task('watch', ['default'], function () {
|
||||
});
|
||||
|
||||
gulp.task('clean', function () {
|
||||
del.sync(['webadmin/dist', 'setup/splash/website']);
|
||||
rimraf.sync('webadmin/dist');
|
||||
rimraf.sync('setup/splash/website');
|
||||
});
|
||||
|
||||
gulp.task('default', ['clean', 'html', 'js', '3rdparty', 'images', 'css'], function () {});
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps ADD COLUMN enableBackup BOOLEAN DEFAULT 1', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN enableBackup', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE settings MODIFY value TEXT', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE settings MODIFY value VARCHAR(512)', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
'use strict';
|
||||
|
||||
// ensure backupFolder and format are not empty
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT * FROM settings WHERE name=?', [ 'backup_config' ], function (error, result) {
|
||||
if (error || result.length === 0) return callback(error);
|
||||
|
||||
var value = JSON.parse(result[0].value);
|
||||
value.format = 'tgz'; // set the format
|
||||
|
||||
if (value.provider === 'filesystem' && !value.backupFolder) {
|
||||
value.backupFolder = '/var/backups'; // set the backupFolder
|
||||
}
|
||||
|
||||
db.runSql('UPDATE settings SET value = ? WHERE name = ?', [ JSON.stringify(value), 'backup_config' ], function (error) {
|
||||
if (error) console.error('Error setting ownerid ' + JSON.stringify(u) + error);
|
||||
callback();
|
||||
});
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE backups ADD COLUMN format VARCHAR(16) DEFAULT "tgz"', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE backups DROP COLUMN format', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps ADD COLUMN newConfigJson TEXT', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN newConfigJson', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE backups ADD COLUMN manifestJson TEXT'),
|
||||
|
||||
db.runSql.bind(db, 'START TRANSACTION;'),
|
||||
|
||||
// fill all the backups with restoreConfigs from current apps
|
||||
function addManifests(callback) {
|
||||
console.log('Importing manifests');
|
||||
|
||||
db.all('SELECT * FROM backups WHERE type="app"', function (error, backups) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.eachSeries(backups, function (backup, next) {
|
||||
var m = backup.restoreConfigJson ? JSON.parse(backup.restoreConfigJson) : null;
|
||||
if (m) m = JSON.stringify(m.manifest);
|
||||
|
||||
db.runSql('UPDATE backups SET manifestJson=? WHERE id=?', [ m, backup.id ], next);
|
||||
}, callback);
|
||||
});
|
||||
},
|
||||
|
||||
db.runSql.bind(db, 'COMMIT'),
|
||||
|
||||
// remove the restoreConfig
|
||||
db.runSql.bind(db, 'ALTER TABLE backups DROP COLUMN restoreConfigJson')
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE backups DROP COLUMN manifestJson'),
|
||||
db.runSql.bind(db, 'ALTER TABLE backups ADD COLUMN restoreConfigJson TEXT'),
|
||||
], callback);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps CHANGE newConfigJson updateConfigJson TEXT', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps CHANGE updateConfigJson newConfigJson TEXT', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps CHANGE lastBackupId restoreConfigJson TEXT', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps CHANGE restoreConfigJson lastBackupId TEXT', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -60,7 +60,7 @@ CREATE TABLE IF NOT EXISTS apps(
|
||||
manifestJson TEXT,
|
||||
httpPort INTEGER, // this is the nginx proxy port and not manifest.httpPort
|
||||
location VARCHAR(128) NOT NULL UNIQUE,
|
||||
dnsRecordId VARCHAR(512), // tracks any id that we got back to track dns updates (unused)
|
||||
dnsRecordId VARCHAR(512), // tracks any id that we got back to track dns updates
|
||||
accessRestrictionJson TEXT, // { users: [ ], groups: [ ] }
|
||||
createdAt TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
memoryLimit BIGINT DEFAULT 0,
|
||||
@@ -69,10 +69,12 @@ CREATE TABLE IF NOT EXISTS apps(
|
||||
sso BOOLEAN DEFAULT 1, // whether user chose to enable SSO
|
||||
debugModeJson TEXT, // options for development mode
|
||||
robotsTxt TEXT,
|
||||
enableBackup BOOLEAN DEFAULT 1,
|
||||
|
||||
// the following fields do not belong here, they can be removed when we use a queue for apptask
|
||||
lastBackupId VARCHAR(128), // used to pass backupId to restore from to apptask
|
||||
oldConfigJson TEXT, // used to pass old config for apptask
|
||||
restoreConfigJson VARCHAR(256), // used to pass backupId to restore from to apptask
|
||||
oldConfigJson TEXT, // used to pass old config for apptask (configure, restore)
|
||||
updateConfigJson TEXT, // used to pass new config for apptask (update)
|
||||
|
||||
PRIMARY KEY(id));
|
||||
|
||||
@@ -92,7 +94,7 @@ CREATE TABLE IF NOT EXISTS authcodes(
|
||||
|
||||
CREATE TABLE IF NOT EXISTS settings(
|
||||
name VARCHAR(128) NOT NULL UNIQUE,
|
||||
value VARCHAR(512),
|
||||
value TEXT,
|
||||
PRIMARY KEY(name));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS appAddonConfigs(
|
||||
@@ -109,7 +111,8 @@ CREATE TABLE IF NOT EXISTS backups(
|
||||
type VARCHAR(16) NOT NULL, /* 'box' or 'app' */
|
||||
dependsOn TEXT, /* comma separate list of objects this backup depends on */
|
||||
state VARCHAR(16) NOT NULL,
|
||||
restoreConfigJson TEXT, /* JSON including the manifest of the backed up app */
|
||||
manifestJson TEXT, /* to validate if the app can be installed in this version of box */
|
||||
format VARCHAR(16) DEFAULT "tgz",
|
||||
|
||||
PRIMARY KEY (id));
|
||||
|
||||
|
||||
Generated
+1718
-1198
File diff suppressed because it is too large
Load Diff
+56
-52
@@ -1,99 +1,103 @@
|
||||
{
|
||||
"name": "Cloudron",
|
||||
"name": "cloudron",
|
||||
"description": "Main code for a cloudron",
|
||||
"version": "0.0.1",
|
||||
"private": "true",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"author": {
|
||||
"name": "Cloudron authors"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git"
|
||||
"type": "git",
|
||||
"url": "https://git.cloudron.io/cloudron/box.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0.0 <=4.1.1"
|
||||
},
|
||||
"engines": [
|
||||
"node >=4.0.0 <=4.1.1"
|
||||
],
|
||||
"dependencies": {
|
||||
"@google-cloud/dns": "^0.7.0",
|
||||
"@sindresorhus/df": "^2.1.0",
|
||||
"async": "^2.1.4",
|
||||
"aws-sdk": "^2.41.0",
|
||||
"body-parser": "^1.13.1",
|
||||
"cloudron-manifestformat": "^2.8.0",
|
||||
"async": "^2.6.0",
|
||||
"aws-sdk": "^2.151.0",
|
||||
"body-parser": "^1.18.2",
|
||||
"cloudron-manifestformat": "^2.10.0",
|
||||
"connect-ensure-login": "^0.1.1",
|
||||
"connect-lastmile": "^0.1.0",
|
||||
"connect-timeout": "^1.5.0",
|
||||
"connect-lastmile": "^1.0.2",
|
||||
"connect-timeout": "^1.9.0",
|
||||
"cookie-parser": "^1.3.5",
|
||||
"cookie-session": "^1.1.0",
|
||||
"cron": "^1.0.9",
|
||||
"cookie-session": "^1.3.2",
|
||||
"cron": "^1.3.0",
|
||||
"csurf": "^1.6.6",
|
||||
"db-migrate": "^0.10.0-beta.20",
|
||||
"db-migrate": "^0.10.0-beta.24",
|
||||
"db-migrate-mysql": "^1.1.10",
|
||||
"debug": "^2.2.0",
|
||||
"dockerode": "^2.4.3",
|
||||
"ejs": "^2.2.4",
|
||||
"ejs-cli": "^1.2.0",
|
||||
"express": "^4.12.4",
|
||||
"express-session": "^1.11.3",
|
||||
"debug": "^3.1.0",
|
||||
"dockerode": "^2.5.3",
|
||||
"ejs": "^2.5.7",
|
||||
"ejs-cli": "^2.0.0",
|
||||
"express": "^4.16.2",
|
||||
"express-session": "^1.15.6",
|
||||
"gulp-sass": "^3.0.0",
|
||||
"hat": "0.0.3",
|
||||
"hock": "https://registry.npmjs.org/hock/-/hock-1.3.2.tgz",
|
||||
"json": "^9.0.3",
|
||||
"ldapjs": "^1.0.0",
|
||||
"mime": "^1.3.4",
|
||||
"moment-timezone": "^0.5.5",
|
||||
"morgan": "^1.7.0",
|
||||
"lodash.chunk": "^4.2.0",
|
||||
"mime": "^2.0.3",
|
||||
"moment-timezone": "^0.5.14",
|
||||
"morgan": "^1.9.0",
|
||||
"multiparty": "^4.1.2",
|
||||
"mysql": "^2.7.0",
|
||||
"node-uuid": "^1.4.3",
|
||||
"nodemailer": "^4.0.1",
|
||||
"mysql": "^2.15.0",
|
||||
"nodemailer": "^4.4.0",
|
||||
"nodemailer-smtp-transport": "^2.7.4",
|
||||
"oauth2orize": "^1.0.1",
|
||||
"oauth2orize": "^1.11.0",
|
||||
"once": "^1.3.2",
|
||||
"parse-links": "^0.1.0",
|
||||
"passport": "^0.2.2",
|
||||
"passport-http": "^0.2.2",
|
||||
"passport": "^0.4.0",
|
||||
"passport-http": "^0.3.0",
|
||||
"passport-http-bearer": "^1.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"passport-oauth2-client-password": "^0.1.2",
|
||||
"password-generator": "^2.0.2",
|
||||
"password-generator": "^2.2.0",
|
||||
"progress-stream": "^2.0.0",
|
||||
"proxy-middleware": "^0.13.0",
|
||||
"proxy-middleware": "^0.15.0",
|
||||
"request": "^2.83.0",
|
||||
"s3-block-read-stream": "^0.2.0",
|
||||
"safetydance": "^0.2.0",
|
||||
"semver": "^4.3.6",
|
||||
"showdown": "^1.6.0",
|
||||
"safetydance": "^0.7.1",
|
||||
"semver": "^5.4.1",
|
||||
"showdown": "^1.8.2",
|
||||
"split": "^1.0.0",
|
||||
"superagent": "^1.8.3",
|
||||
"superagent": "^3.8.1",
|
||||
"supererror": "^0.7.1",
|
||||
"tar-fs": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.15.2.tgz",
|
||||
"tldjs": "^1.6.2",
|
||||
"tar-fs": "^1.16.0",
|
||||
"tar-stream": "^1.5.5",
|
||||
"tldjs": "^2.2.0",
|
||||
"underscore": "^1.7.0",
|
||||
"uuid": "^3.1.0",
|
||||
"valid-url": "^1.0.9",
|
||||
"validator": "^4.9.0"
|
||||
"validator": "^9.1.1",
|
||||
"ws": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bootstrap-sass": "^3.3.3",
|
||||
"deep-extend": "^0.4.1",
|
||||
"del": "^1.1.1",
|
||||
"expect.js": "*",
|
||||
"gulp": "^3.8.11",
|
||||
"gulp-autoprefixer": "^2.3.0",
|
||||
"gulp": "^3.9.1",
|
||||
"gulp-autoprefixer": "^4.0.0",
|
||||
"gulp-concat": "^2.4.3",
|
||||
"gulp-cssnano": "^2.1.0",
|
||||
"gulp-ejs": "^1.0.0",
|
||||
"gulp-ejs": "^3.1.0",
|
||||
"gulp-sass": "^3.0.0",
|
||||
"gulp-serve": "^1.0.0",
|
||||
"gulp-sourcemaps": "^1.5.2",
|
||||
"gulp-uglify": "^1.1.0",
|
||||
"gulp-sourcemaps": "^2.6.1",
|
||||
"gulp-uglify": "^3.0.0",
|
||||
"hock": "~1.2.0",
|
||||
"istanbul": "*",
|
||||
"js2xmlparser": "^1.0.0",
|
||||
"js2xmlparser": "^3.0.0",
|
||||
"mocha": "*",
|
||||
"mock-aws-s3": "^2.4.0",
|
||||
"nock": "^9.0.2",
|
||||
"node-sass": "^3.0.0-alpha.0",
|
||||
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
|
||||
"nock": "^9.0.14",
|
||||
"node-sass": "^4.6.1",
|
||||
"readdirp": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz",
|
||||
"request": "^2.65.0",
|
||||
"yargs": "^3.15.0"
|
||||
"yargs": "^10.0.3"
|
||||
},
|
||||
"scripts": {
|
||||
"migrate_local": "DATABASE_URL=mysql://root:@localhost/box node_modules/.bin/db-migrate up",
|
||||
|
||||
+14
-18
@@ -45,6 +45,7 @@ fi
|
||||
initBaseImage="true"
|
||||
# provisioning data
|
||||
domain=""
|
||||
adminLocation="my"
|
||||
zoneName=""
|
||||
provider=""
|
||||
encryptionKey=""
|
||||
@@ -63,13 +64,14 @@ baseDataDir=""
|
||||
# TODO this is still there for the restore case, see other occasions below
|
||||
versionsUrl="https://s3.amazonaws.com/prod-cloudron-releases/versions.json"
|
||||
|
||||
args=$(getopt -o "" -l "domain:,help,skip-baseimage-init,data:,data-dir:,provider:,encryption-key:,restore-url:,tls-provider:,version:,dns-provider:,env:,prerelease,skip-reboot,source-url:" -n "$0" -- "$@")
|
||||
args=$(getopt -o "" -l "domain:,help,skip-baseimage-init,data:,data-dir:,provider:,encryption-key:,restore-url:,tls-provider:,version:,dns-provider:,env:,admin-location:,prerelease,skip-reboot,source-url:" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--domain) domain="$2"; shift 2;;
|
||||
--help) echo "See https://cloudron.io/references/selfhosting.html on how to install Cloudron"; exit 0;;
|
||||
--admin-location) adminLocation="$2"; shift 2;;
|
||||
--help) echo "See https://cloudron.io/documentation/installation/ on how to install Cloudron"; exit 0;;
|
||||
--provider) provider="$2"; shift 2;;
|
||||
--encryption-key) encryptionKey="$2"; shift 2;;
|
||||
--restore-url) restoreUrl="$2"; shift 2;;
|
||||
@@ -105,13 +107,15 @@ done
|
||||
# validate arguments in the absence of data
|
||||
if [[ -z "${dataJson}" ]]; then
|
||||
if [[ -z "${provider}" ]]; then
|
||||
echo "--provider is required (azure, digitalocean, ec2, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic)"
|
||||
echo "--provider is required (azure, cloudscale.ch, digitalocean, ec2, exoscale, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic)"
|
||||
exit 1
|
||||
elif [[ \
|
||||
"${provider}" != "ami" && \
|
||||
"${provider}" != "azure" && \
|
||||
"${provider}" != "cloudscale.ch" && \
|
||||
"${provider}" != "digitalocean" && \
|
||||
"${provider}" != "ec2" && \
|
||||
"${provider}" != "exoscale" && \
|
||||
"${provider}" != "gce" && \
|
||||
"${provider}" != "lightsail" && \
|
||||
"${provider}" != "linode" && \
|
||||
@@ -121,7 +125,7 @@ if [[ -z "${dataJson}" ]]; then
|
||||
"${provider}" != "vultr" && \
|
||||
"${provider}" != "generic" \
|
||||
]]; then
|
||||
echo "--provider must be one of: azure, digitalocean, ec2, gce, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic"
|
||||
echo "--provider must be one of: azure, cloudscale.ch, digitalocean, ec2, exoscale, gce, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -195,6 +199,7 @@ if [[ -z "${dataJson}" ]]; then
|
||||
{
|
||||
"boxVersionsUrl": "${versionsUrl}",
|
||||
"fqdn": "${domain}",
|
||||
"adminLocation": "${adminLocation}",
|
||||
"zoneName": "${zoneName}",
|
||||
"provider": "${provider}",
|
||||
"apiServerOrigin": "${apiServerOrigin}",
|
||||
@@ -209,11 +214,9 @@ if [[ -z "${dataJson}" ]]; then
|
||||
"provider": "filesystem",
|
||||
"backupFolder": "/var/backups",
|
||||
"key": "${encryptionKey}",
|
||||
"format": "tgz",
|
||||
"retentionSecs": 172800
|
||||
},
|
||||
"updateConfig": {
|
||||
"prerelease": ${prerelease}
|
||||
},
|
||||
"version": "${version}"
|
||||
}
|
||||
EOF
|
||||
@@ -223,6 +226,7 @@ EOF
|
||||
{
|
||||
"boxVersionsUrl": "${versionsUrl}",
|
||||
"fqdn": "${domain}",
|
||||
"adminLocation": "${adminLocation}",
|
||||
"zoneName": "${zoneName}",
|
||||
"provider": "${provider}",
|
||||
"apiServerOrigin": "${apiServerOrigin}",
|
||||
@@ -259,17 +263,9 @@ fi
|
||||
|
||||
echo "=> Installing version ${version} (this takes some time) ..."
|
||||
echo "${data}" > "${DATA_FILE}"
|
||||
# poor mans semver
|
||||
if [[ ${version} == "0.10"* ]]; then
|
||||
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" --data-file "${DATA_FILE}" &>> "${LOG_FILE}"; then
|
||||
echo "Failed to install cloudron. See ${LOG_FILE} for details"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" --data-file "${DATA_FILE}" --data-dir "${baseDataDir}" &>> "${LOG_FILE}"; then
|
||||
echo "Failed to install cloudron. See ${LOG_FILE} for details"
|
||||
exit 1
|
||||
fi
|
||||
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" --data-file "${DATA_FILE}" --data-dir "${baseDataDir}" &>> "${LOG_FILE}"; then
|
||||
echo "Failed to install cloudron. See ${LOG_FILE} for details"
|
||||
exit 1
|
||||
fi
|
||||
rm "${DATA_FILE}"
|
||||
|
||||
|
||||
@@ -31,8 +31,8 @@ if ! $(cd "${SOURCE_DIR}" && git diff --exit-code >/dev/null); then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$(node --version)" != "v6.11.1" ]]; then
|
||||
echo "This script requires node 6.11.1"
|
||||
if [[ "$(node --version)" != "v6.11.5" ]]; then
|
||||
echo "This script requires node 6.11.5"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
+34
-9
@@ -34,13 +34,41 @@ while true; do
|
||||
esac
|
||||
done
|
||||
|
||||
echo "==> installer: updating docker"
|
||||
if [[ $(docker version --format {{.Client.Version}}) != "17.09.0-ce" ]]; then
|
||||
$curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_17.09.0~ce-0~ubuntu_amd64.deb -o /tmp/docker.deb
|
||||
|
||||
# https://download.docker.com/linux/ubuntu/dists/xenial/stable/binary-amd64/Packages
|
||||
if [[ $(sha256sum /tmp/docker.deb | cut -d' ' -f1) != "d33f6eb134f0ab0876148bd96de95ea47d583d7f2cddfdc6757979453f9bd9bf" ]]; then
|
||||
echo "docker binary download is corrupt"
|
||||
exit 5
|
||||
fi
|
||||
|
||||
echo "Waiting for all dpkg tasks to finish..."
|
||||
while fuser /var/lib/dpkg/lock; do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
while ! dpkg --force-confold --configure -a; do
|
||||
echo "Failed to fix packages. Retry"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
while ! apt install -y /tmp/docker.deb; do
|
||||
echo "Failed to install docker. Retry"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
rm /tmp/docker.deb
|
||||
fi
|
||||
|
||||
echo "==> installer: updating node"
|
||||
if [[ "$(node --version)" != "v6.11.1" ]]; then
|
||||
mkdir -p /usr/local/node-6.11.1
|
||||
$curl -sL https://nodejs.org/dist/v6.11.1/node-v6.11.1-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-6.11.1
|
||||
ln -sf /usr/local/node-6.11.1/bin/node /usr/bin/node
|
||||
ln -sf /usr/local/node-6.11.1/bin/npm /usr/bin/npm
|
||||
rm -rf /usr/local/node-6.9.2
|
||||
if [[ "$(node --version)" != "v6.11.5" ]]; then
|
||||
mkdir -p /usr/local/node-6.11.5
|
||||
$curl -sL https://nodejs.org/dist/v6.11.5/node-v6.11.5-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-6.11.5
|
||||
ln -sf /usr/local/node-6.11.5/bin/node /usr/bin/node
|
||||
ln -sf /usr/local/node-6.11.5/bin/npm /usr/bin/npm
|
||||
rm -rf /usr/local/node-6.11.3
|
||||
fi
|
||||
|
||||
for try in `seq 1 10`; do
|
||||
@@ -81,9 +109,6 @@ fi
|
||||
# ensure we are not inside the source directory, which we will remove now
|
||||
cd /root
|
||||
|
||||
echo "==> installer: updating packages"
|
||||
# add logic to update apt packages here
|
||||
|
||||
echo "==> installer: switching the box code"
|
||||
rm -rf "${BOX_SRC_DIR}"
|
||||
mv "${box_src_tmp_dir}" "${BOX_SRC_DIR}"
|
||||
|
||||
+5
-9
@@ -6,6 +6,7 @@ json="${source_dir}/../node_modules/.bin/json"
|
||||
# IMPORTANT: Fix cloudron.js:doUpdate if you add/remove any arg. keep these sorted for readability
|
||||
arg_api_server_origin=""
|
||||
arg_fqdn=""
|
||||
arg_admin_location=""
|
||||
arg_zone_name=""
|
||||
arg_is_custom_domain="false"
|
||||
arg_restore_key=""
|
||||
@@ -20,9 +21,7 @@ arg_version=""
|
||||
arg_web_server_origin=""
|
||||
arg_backup_config=""
|
||||
arg_dns_config=""
|
||||
arg_update_config=""
|
||||
arg_provider=""
|
||||
arg_app_bundle=""
|
||||
arg_is_demo="false"
|
||||
|
||||
args=$(getopt -o "" -l "data:,retire-reason:,retire-info:" -n "$0" -- "$@")
|
||||
@@ -46,19 +45,19 @@ while true; do
|
||||
arg_is_custom_domain=$(echo "$2" | $json isCustomDomain)
|
||||
[[ "${arg_is_custom_domain}" == "" ]] && arg_is_custom_domain="true"
|
||||
|
||||
arg_admin_location=$(echo "$2" | $json adminLocation)
|
||||
[[ "${arg_admin_location}" == "" ]] && arg_admin_location="my"
|
||||
|
||||
# only update/restore have this valid (but not migrate)
|
||||
arg_api_server_origin=$(echo "$2" | $json apiServerOrigin)
|
||||
[[ "${arg_api_server_origin}" == "" ]] && arg_api_server_origin="https://api.cloudron.io"
|
||||
arg_web_server_origin=$(echo "$2" | $json webServerOrigin)
|
||||
[[ "${arg_web_server_origin}" == "" ]] && arg_web_server_origin="https://cloudron.io"
|
||||
|
||||
# TODO check if an where this is used
|
||||
# TODO check if and where this is used
|
||||
arg_version=$(echo "$2" | $json version)
|
||||
|
||||
# read possibly empty parameters here
|
||||
arg_app_bundle=$(echo "$2" | $json appBundle)
|
||||
[[ "${arg_app_bundle}" == "" ]] && arg_app_bundle="[]"
|
||||
|
||||
arg_is_demo=$(echo "$2" | $json isDemo)
|
||||
[[ "${arg_is_demo}" == "" ]] && arg_is_demo="false"
|
||||
|
||||
@@ -86,9 +85,6 @@ while true; do
|
||||
arg_dns_config=$(echo "$2" | $json dnsConfig)
|
||||
[[ "${arg_dns_config}" == "null" ]] && arg_dns_config=""
|
||||
|
||||
arg_update_config=$(echo "$2" | $json updateConfig)
|
||||
[[ "${arg_update_config}" == "null" ]] && arg_update_config=""
|
||||
|
||||
shift 2
|
||||
;;
|
||||
--) break;;
|
||||
|
||||
+3
-4
@@ -7,7 +7,6 @@ readonly SETUP_WEBSITE_DIR="/home/yellowtent/setup/website"
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly box_src_dir="$(realpath ${script_dir}/..)"
|
||||
readonly PLATFORM_DATA_DIR="/home/yellowtent/platformdata"
|
||||
readonly ADMIN_LOCATION="my" # keep this in sync with constants.js
|
||||
|
||||
echo "Setting up nginx update page"
|
||||
|
||||
@@ -19,7 +18,7 @@ fi
|
||||
source "${script_dir}/argparser.sh" "$@" # this injects the arg_* variables used below
|
||||
|
||||
# keep this is sync with config.js appFqdn()
|
||||
admin_fqdn=$([[ "${arg_is_custom_domain}" == "true" ]] && echo "${ADMIN_LOCATION}.${arg_fqdn}" || echo "${ADMIN_LOCATION}-${arg_fqdn}")
|
||||
admin_fqdn=$([[ "${arg_is_custom_domain}" == "true" ]] && echo "${arg_admin_location}.${arg_fqdn}" || echo "${arg_admin_location}-${arg_fqdn}")
|
||||
admin_origin="https://${admin_fqdn}"
|
||||
|
||||
# copy the website
|
||||
@@ -34,11 +33,11 @@ if [[ "${arg_retire_reason}" != "" || "${existing_infra}" != "${current_infra}"
|
||||
echo "Showing progress bar on all subdomains in retired mode or infra update. retire: ${arg_retire_reason} existing: ${existing_infra} current: ${current_infra}"
|
||||
rm -f ${PLATFORM_DATA_DIR}/nginx/applications/*
|
||||
${box_src_dir}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
|
||||
-O "{ \"vhost\": \"~^(.+)\$\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\", \"xFrameOptions\": \"SAMEORIGIN\", \"robotsTxtQuoted\": null }" > "${PLATFORM_DATA_DIR}/nginx/applications/admin.conf"
|
||||
-O "{ \"vhost\": \"~^(.+)\$\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\", \"xFrameOptions\": \"SAMEORIGIN\", \"robotsTxtQuoted\": null, \"hasIPv6\": false }" > "${PLATFORM_DATA_DIR}/nginx/applications/admin.conf"
|
||||
else
|
||||
echo "Show progress bar only on admin domain for normal update"
|
||||
${box_src_dir}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
|
||||
-O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\", \"xFrameOptions\": \"SAMEORIGIN\", \"robotsTxtQuoted\": null }" > "${PLATFORM_DATA_DIR}/nginx/applications/admin.conf"
|
||||
-O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\", \"xFrameOptions\": \"SAMEORIGIN\", \"robotsTxtQuoted\": null, \"hasIPv6\": false }" > "${PLATFORM_DATA_DIR}/nginx/applications/admin.conf"
|
||||
fi
|
||||
|
||||
if [[ "${arg_retire_reason}" == "migrate" ]]; then
|
||||
|
||||
+80
-83
@@ -7,7 +7,6 @@ echo "==> Cloudron Start"
|
||||
readonly USER="yellowtent"
|
||||
readonly HOME_DIR="/home/${USER}"
|
||||
readonly BOX_SRC_DIR="${HOME_DIR}/box"
|
||||
readonly OLD_DATA_DIR="${HOME_DIR}/data";
|
||||
readonly PLATFORM_DATA_DIR="${HOME_DIR}/platformdata" # platform data
|
||||
readonly APPS_DATA_DIR="${HOME_DIR}/appsdata" # app data
|
||||
readonly BOX_DATA_DIR="${HOME_DIR}/boxdata" # box data
|
||||
@@ -42,7 +41,7 @@ systemctl restart apparmor
|
||||
usermod ${USER} -a -G docker
|
||||
# preserve the existing storage driver (user might be using overlay2)
|
||||
storage_driver=$(docker info | grep "Storage Driver" | sed 's/.*: //')
|
||||
[[ -n "${storage_driver}" ]] || storage_driver="devicemapper" # if the above command fails
|
||||
[[ -n "${storage_driver}" ]] || storage_driver="overlay2" # if the above command fails
|
||||
|
||||
temp_file=$(mktemp)
|
||||
# create systemd drop-in. some apps do not work with aufs
|
||||
@@ -73,53 +72,29 @@ fi
|
||||
|
||||
mkdir -p "${BOX_DATA_DIR}"
|
||||
mkdir -p "${APPS_DATA_DIR}"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}"
|
||||
|
||||
# keep these in sync with paths.js
|
||||
echo "==> Ensuring directories"
|
||||
if [[ ! -d "${PLATFORM_DATA_DIR}/mail" ]]; then
|
||||
if [[ -d "${OLD_DATA_DIR}/mail" ]]; then
|
||||
echo "==> Migrate old mail data"
|
||||
# Migrate mail data to new format
|
||||
docker stop mail || true # otherwise the move below might fail if mail container writes in the middle
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/mail"
|
||||
# we can't move the whole folder as it is a btrfs subvolume mount
|
||||
mv -f "${OLD_DATA_DIR}/mail/"* "${PLATFORM_DATA_DIR}/mail/" # this used to be mail container's run directory
|
||||
else
|
||||
echo "==> Create new mail data dir"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/mail"
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/graphite"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/mail/dkim"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/mysql"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/postgresql"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/mongodb"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/snapshots"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/addons/mail"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/collectd/collectd.conf.d"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/logrotate.d"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/acme"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/backup"
|
||||
|
||||
mkdir -p "${BOX_DATA_DIR}/appicons"
|
||||
mkdir -p "${BOX_DATA_DIR}/certs"
|
||||
mkdir -p "${BOX_DATA_DIR}/acme" # acme keys
|
||||
mkdir -p "${BOX_DATA_DIR}/mail/dkim"
|
||||
|
||||
# ensure backups folder exists and is writeable
|
||||
mkdir -p /var/backups
|
||||
chmod 777 /var/backups
|
||||
|
||||
echo "==> Check for old btrfs volumes"
|
||||
if mountpoint -q "${OLD_DATA_DIR}"; then
|
||||
echo "==> Cleanup btrfs volumes"
|
||||
# First stop all container to be able to unmount
|
||||
docker ps -q | xargs docker stop
|
||||
umount "${OLD_DATA_DIR}"
|
||||
rm -rf "/root/user_data.img"
|
||||
else
|
||||
echo "==> No btrfs volumes found";
|
||||
fi
|
||||
|
||||
echo "==> Configuring journald"
|
||||
sed -e "s/^#SystemMaxUse=.*$/SystemMaxUse=100M/" \
|
||||
-e "s/^#ForwardToSyslog=.*$/ForwardToSyslog=no/" \
|
||||
@@ -145,7 +120,10 @@ echo "==> Setting up unbound"
|
||||
# DO uses Google nameservers by default. This causes RBL queries to fail (host 2.0.0.127.zen.spamhaus.org)
|
||||
# We do not use dnsmasq because it is not a recursive resolver and defaults to the value in the interfaces file (which is Google DNS!)
|
||||
# We listen on 0.0.0.0 because there is no way control ordering of docker (which creates the 172.18.0.0/16) and unbound
|
||||
echo -e "server:\n\tinterface: 0.0.0.0\n\taccess-control: 127.0.0.1 allow\n\taccess-control: 172.18.0.1/16 allow\n\tcache-max-negative-ttl: 30\n\tcache-max-ttl: 300" > /etc/unbound/unbound.conf.d/cloudron-network.conf
|
||||
# If IP6 is not enabled, dns queries seem to fail on some hosts
|
||||
echo -e "server:\n\tinterface: 0.0.0.0\n\tdo-ip6: yes\n\taccess-control: 127.0.0.1 allow\n\taccess-control: 172.18.0.1/16 allow\n\tcache-max-negative-ttl: 30\n\tcache-max-ttl: 300\n\t#logfile: /var/log/unbound.log\n\t#verbosity: 10" > /etc/unbound/unbound.conf.d/cloudron-network.conf
|
||||
# update the root anchor after a out-of-disk-space situation (see #269)
|
||||
unbound-anchor -a /var/lib/unbound/root.key
|
||||
|
||||
echo "==> Adding systemd services"
|
||||
cp -r "${script_dir}/start/systemd/." /etc/systemd/system/
|
||||
@@ -170,9 +148,17 @@ cp "${script_dir}/start/sudoers" /etc/sudoers.d/${USER}
|
||||
echo "==> Configuring collectd"
|
||||
rm -rf /etc/collectd
|
||||
ln -sfF "${PLATFORM_DATA_DIR}/collectd" /etc/collectd
|
||||
cp "${script_dir}/start/collectd.conf" "${PLATFORM_DATA_DIR}/collectd/collectd.conf"
|
||||
cp "${script_dir}/start/collectd/collectd.conf" "${PLATFORM_DATA_DIR}/collectd/collectd.conf"
|
||||
systemctl restart collectd
|
||||
|
||||
echo "==> Configuring logrotate"
|
||||
if ! grep -q "^include ${PLATFORM_DATA_DIR}/logrotate.d" /etc/logrotate.conf; then
|
||||
echo -e "\ninclude ${PLATFORM_DATA_DIR}/logrotate.d\n" >> /etc/logrotate.conf
|
||||
fi
|
||||
|
||||
echo "==> Adding motd message for admins"
|
||||
cp "${script_dir}/start/cloudron-motd" /etc/update-motd.d/92-cloudron
|
||||
|
||||
echo "==> Configuring nginx"
|
||||
# link nginx config to system config
|
||||
unlink /etc/nginx 2>/dev/null || rm -rf /etc/nginx
|
||||
@@ -200,7 +186,11 @@ if [[ ! -f /etc/mysql/mysql.cnf ]] || ! diff -q "${script_dir}/start/mysql.cnf"
|
||||
echo "Waiting for mysql jobs..."
|
||||
sleep 1
|
||||
done
|
||||
systemctl restart mysql
|
||||
while true; do
|
||||
if systemctl restart mysql; then break; fi
|
||||
echo "Restarting MySql again after sometime since this fails randomly"
|
||||
sleep 1
|
||||
done
|
||||
else
|
||||
systemctl start mysql
|
||||
fi
|
||||
@@ -212,20 +202,31 @@ mysql -u root -p${mysql_root_password} -e 'CREATE DATABASE IF NOT EXISTS box'
|
||||
if [[ -n "${arg_restore_url}" ]]; then
|
||||
set_progress "30" "Downloading restore data"
|
||||
|
||||
decrypt=""
|
||||
if [[ "${arg_restore_url}" == *.tar.gz.enc || -n "${arg_restore_key}" ]]; then
|
||||
echo "==> Downloading encrypted backup: ${arg_restore_url} and key: ${arg_restore_key}"
|
||||
decrypt=(openssl aes-256-cbc -d -nosalt -pass "pass:${arg_restore_key}")
|
||||
else
|
||||
echo "==> Downloading backup: ${arg_restore_url}"
|
||||
decrypt=(cat -)
|
||||
fi
|
||||
readonly restore_dir="${arg_restore_url#file://}"
|
||||
|
||||
while true; do
|
||||
if $curl -L "${arg_restore_url}" | "${decrypt[@]}" \
|
||||
| tar -zxf - --overwrite --transform="s,^box/\?,boxdata/," --transform="s,^mail/\?,platformdata/mail/," --show-transformed-names -C "${HOME_DIR}"; then break; fi
|
||||
echo "Failed to download data, trying again"
|
||||
done
|
||||
if [[ -d "${restore_dir}" ]]; then # rsync backup
|
||||
echo "==> Copying backup: ${restore_dir}"
|
||||
if [[ $(stat -c "%d" "${BOX_DATA_DIR}") == $(stat -c "%d" "${restore_dir}") ]]; then
|
||||
cp -rfl "${restore_dir}/." "${BOX_DATA_DIR}"
|
||||
else
|
||||
cp -rf "${restore_dir}/." "${BOX_DATA_DIR}"
|
||||
fi
|
||||
else # tgz backup
|
||||
decrypt=""
|
||||
if [[ "${arg_restore_url}" == *.tar.gz.enc || -n "${arg_restore_key}" ]]; then
|
||||
echo "==> Downloading encrypted backup: ${arg_restore_url} and key: ${arg_restore_key}"
|
||||
decrypt=(openssl aes-256-cbc -d -nosalt -pass "pass:${arg_restore_key}")
|
||||
elif [[ "${arg_restore_url}" == *.tar.gz ]]; then
|
||||
echo "==> Downloading backup: ${arg_restore_url}"
|
||||
decrypt=(cat -)
|
||||
fi
|
||||
|
||||
while true; do
|
||||
if $curl -L "${arg_restore_url}" | "${decrypt[@]}" \
|
||||
| tar -zxf - --overwrite -C "${BOX_DATA_DIR}"; then break; fi
|
||||
echo "Failed to download data, trying again"
|
||||
done
|
||||
fi
|
||||
|
||||
set_progress "35" "Setting up MySQL"
|
||||
if [[ -f "${BOX_DATA_DIR}/box.mysqldump" ]]; then
|
||||
@@ -238,9 +239,27 @@ set_progress "40" "Migrating data"
|
||||
sudo -u "${USER}" -H bash <<EOF
|
||||
set -eu
|
||||
cd "${BOX_SRC_DIR}"
|
||||
BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@localhost/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up
|
||||
BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@127.0.0.1/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up
|
||||
EOF
|
||||
|
||||
echo "==> Adding automated configs"
|
||||
mysql -u root -p${mysql_root_password} -e "REPLACE INTO settings (name, value) VALUES (\"domain\", '{ \"fqdn\": \"$arg_fqdn\", \"zoneName\": \"$arg_zone_name\", \"adminLocation\": \"$arg_admin_location\" }')" box
|
||||
|
||||
if [[ ! -z "${arg_backup_config}" ]]; then
|
||||
mysql -u root -p${mysql_root_password} \
|
||||
-e "REPLACE INTO settings (name, value) VALUES (\"backup_config\", '$arg_backup_config')" box
|
||||
fi
|
||||
|
||||
if [[ ! -z "${arg_dns_config}" ]]; then
|
||||
mysql -u root -p${mysql_root_password} \
|
||||
-e "REPLACE INTO settings (name, value) VALUES (\"dns_config\", '$arg_dns_config')" box
|
||||
fi
|
||||
|
||||
if [[ ! -z "${arg_tls_config}" ]]; then
|
||||
mysql -u root -p${mysql_root_password} \
|
||||
-e "REPLACE INTO settings (name, value) VALUES (\"tls_config\", '$arg_tls_config')" box
|
||||
fi
|
||||
|
||||
echo "==> Creating cloudron.conf"
|
||||
cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
|
||||
{
|
||||
@@ -249,18 +268,11 @@ cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
|
||||
"apiServerOrigin": "${arg_api_server_origin}",
|
||||
"webServerOrigin": "${arg_web_server_origin}",
|
||||
"fqdn": "${arg_fqdn}",
|
||||
"adminLocation": "${arg_admin_location}",
|
||||
"zoneName": "${arg_zone_name}",
|
||||
"isCustomDomain": ${arg_is_custom_domain},
|
||||
"provider": "${arg_provider}",
|
||||
"isDemo": ${arg_is_demo},
|
||||
"database": {
|
||||
"hostname": "localhost",
|
||||
"username": "root",
|
||||
"password": "${mysql_root_password}",
|
||||
"port": 3306,
|
||||
"name": "box"
|
||||
},
|
||||
"appBundle": ${arg_app_bundle}
|
||||
"isDemo": ${arg_is_demo}
|
||||
}
|
||||
CONF_END
|
||||
# pass these out-of-band because they have new lines which interfere with json
|
||||
@@ -276,39 +288,24 @@ cat > "${BOX_SRC_DIR}/webadmin/dist/config.json" <<CONF_END
|
||||
}
|
||||
CONF_END
|
||||
|
||||
if [[ ! -f "${BOX_DATA_DIR}/dhparams.pem" ]]; then
|
||||
echo "==> Generating dhparams (takes forever)"
|
||||
openssl dhparam -out "${BOX_DATA_DIR}/dhparams.pem" 2048
|
||||
cp "${BOX_DATA_DIR}/dhparams.pem" "${PLATFORM_DATA_DIR}/addons/mail/dhparams.pem"
|
||||
else
|
||||
cp "${BOX_DATA_DIR}/dhparams.pem" "${PLATFORM_DATA_DIR}/addons/mail/dhparams.pem"
|
||||
fi
|
||||
|
||||
echo "==> Changing ownership"
|
||||
chown "${USER}:${USER}" -R "${CONFIG_DIR}"
|
||||
chown "${USER}:${USER}" -R "${PLATFORM_DATA_DIR}/nginx" "${PLATFORM_DATA_DIR}/collectd" "${PLATFORM_DATA_DIR}/addons" "${PLATFORM_DATA_DIR}/acme"
|
||||
chown "${USER}:${USER}" -R "${BOX_DATA_DIR}"
|
||||
chown "${USER}:${USER}" -R "${PLATFORM_DATA_DIR}/mail/dkim" # this is owned by box currently since it generates the keys
|
||||
chown "${USER}:${USER}" -R "${PLATFORM_DATA_DIR}/nginx" "${PLATFORM_DATA_DIR}/collectd" "${PLATFORM_DATA_DIR}/logrotate.d" "${PLATFORM_DATA_DIR}/addons" "${PLATFORM_DATA_DIR}/acme" "${PLATFORM_DATA_DIR}/backup"
|
||||
chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}/INFRA_VERSION" 2>/dev/null || true
|
||||
chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}"
|
||||
|
||||
echo "==> Adding automated configs"
|
||||
if [[ ! -z "${arg_backup_config}" ]]; then
|
||||
mysql -u root -p${mysql_root_password} \
|
||||
-e "REPLACE INTO settings (name, value) VALUES (\"backup_config\", '$arg_backup_config')" box
|
||||
fi
|
||||
|
||||
if [[ ! -z "${arg_dns_config}" ]]; then
|
||||
mysql -u root -p${mysql_root_password} \
|
||||
-e "REPLACE INTO settings (name, value) VALUES (\"dns_config\", '$arg_dns_config')" box
|
||||
fi
|
||||
|
||||
if [[ ! -z "${arg_update_config}" ]]; then
|
||||
mysql -u root -p${mysql_root_password} \
|
||||
-e "REPLACE INTO settings (name, value) VALUES (\"update_config\", '$arg_update_config')" box
|
||||
fi
|
||||
|
||||
if [[ ! -z "${arg_tls_config}" ]]; then
|
||||
mysql -u root -p${mysql_root_password} \
|
||||
-e "REPLACE INTO settings (name, value) VALUES (\"tls_config\", '$arg_tls_config')" box
|
||||
fi
|
||||
|
||||
echo "==> Generating dhparams (takes forever)"
|
||||
if [[ ! -f "${BOX_DATA_DIR}/dhparams.pem" ]]; then
|
||||
openssl dhparam -out "${BOX_DATA_DIR}/dhparams.pem" 2048
|
||||
fi
|
||||
# do not chown the boxdata/mail directory; dovecot gets upset
|
||||
chown "${USER}:${USER}" "${BOX_DATA_DIR}"
|
||||
find "${BOX_DATA_DIR}" -mindepth 1 -maxdepth 1 -not -path "${BOX_DATA_DIR}/mail" -exec chown -R "${USER}:${USER}" {} \;
|
||||
chown "${USER}:${USER}" -R "${BOX_DATA_DIR}/mail/dkim" # this is owned by box currently since it generates the keys
|
||||
|
||||
set_progress "60" "Starting Cloudron"
|
||||
systemctl start cloudron.target
|
||||
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/bin/sh
|
||||
# motd hook to remind admins about updates
|
||||
printf "\t\t\tNOTE TO CLOUDRON ADMINS\n"
|
||||
printf "\t\t\t-----------------------\n"
|
||||
printf "Please do not run apt upgrade manually as it will update packages that\n"
|
||||
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"
|
||||
|
||||
if grep -q "^PasswordAuthentication yes" /etc/ssh/sshd_config; then
|
||||
printf "\nPlease disable password based SSH access to secure your server. Read more at\n"
|
||||
printf "https://cloudron.io/documentation/security/#securing-ssh-access\n"
|
||||
fi
|
||||
|
||||
@@ -89,7 +89,7 @@ LoadPlugin cpu
|
||||
#LoadPlugin curl_json
|
||||
#LoadPlugin curl_xml
|
||||
#LoadPlugin dbi
|
||||
LoadPlugin df
|
||||
#LoadPlugin df
|
||||
#LoadPlugin disk
|
||||
#LoadPlugin dns
|
||||
#LoadPlugin email
|
||||
@@ -138,9 +138,9 @@ LoadPlugin nginx
|
||||
#LoadPlugin powerdns
|
||||
#LoadPlugin processes
|
||||
#LoadPlugin protocols
|
||||
#<LoadPlugin python>
|
||||
# Globals true
|
||||
#</LoadPlugin>
|
||||
<LoadPlugin python>
|
||||
Globals true
|
||||
</LoadPlugin>
|
||||
#LoadPlugin rrdcached
|
||||
#LoadPlugin rrdtool
|
||||
#LoadPlugin sensors
|
||||
@@ -192,16 +192,6 @@ LoadPlugin write_graphite
|
||||
</Aggregation>
|
||||
</Plugin>
|
||||
|
||||
<Plugin df>
|
||||
FSType "ext4"
|
||||
|
||||
ReportByDevice true
|
||||
IgnoreSelected false
|
||||
|
||||
ValuesAbsolute true
|
||||
ValuesPercentage true
|
||||
</Plugin>
|
||||
|
||||
<Plugin interface>
|
||||
Interface "eth0"
|
||||
IgnoreSelected false
|
||||
@@ -243,6 +233,17 @@ LoadPlugin write_graphite
|
||||
</File>
|
||||
</Plugin>
|
||||
|
||||
<Plugin python>
|
||||
# https://blog.dbrgn.ch/2017/3/10/write-a-collectd-python-plugin/
|
||||
ModulePath "/home/yellowtent/box/setup/start/collectd/"
|
||||
LogTraces false # enable this to get traces in /var/log/collectd.log
|
||||
Interactive false
|
||||
|
||||
Import "df"
|
||||
# <Module df>
|
||||
# </Module>
|
||||
</Plugin>
|
||||
|
||||
<Plugin write_graphite>
|
||||
<Node "graphing">
|
||||
Host "localhost"
|
||||
@@ -0,0 +1,36 @@
|
||||
import collectd,os,subprocess
|
||||
|
||||
# https://blog.dbrgn.ch/2017/3/10/write-a-collectd-python-plugin/
|
||||
|
||||
disks = []
|
||||
|
||||
def init():
|
||||
global disks
|
||||
lines = [s.split() for s in subprocess.check_output(["df", "--type=ext4", "--output=source,target,size,used,avail"]).splitlines()]
|
||||
disks = lines[1:] # strip header
|
||||
collectd.info('custom df plugin initialized with %s' % disks)
|
||||
|
||||
def read():
|
||||
for d in disks:
|
||||
device = d[0]
|
||||
if 'devicemapper' in d[1] or not device.startswith('/dev/'): continue
|
||||
instance = device[len('/dev/'):].replace('/', '_') # see #348
|
||||
|
||||
try:
|
||||
st = os.statvfs(d[1]) # handle disk removal
|
||||
except:
|
||||
continue
|
||||
|
||||
val = collectd.Values(type='df_complex', plugin='df', plugin_instance=instance)
|
||||
|
||||
free = st.f_bavail * st.f_frsize # bavail is for non-root user. bfree is total
|
||||
val.dispatch(values=[free], type_instance='free')
|
||||
|
||||
reserved = (st.f_bfree - st.f_bavail) * st.f_frsize # root took these
|
||||
val.dispatch(values=[reserved], type_instance='reserved')
|
||||
|
||||
used = (st.f_blocks - st.f_bfree) * st.f_frsize
|
||||
val.dispatch(values=[used], type_instance='used')
|
||||
|
||||
collectd.register_init(init)
|
||||
collectd.register_read(read)
|
||||
@@ -4,13 +4,54 @@ map $http_upgrade $connection_upgrade {
|
||||
'' close;
|
||||
}
|
||||
|
||||
# http server
|
||||
server {
|
||||
<% if (vhost) { %>
|
||||
listen 443 http2;
|
||||
listen 80;
|
||||
<% if (hasIPv6) { -%>
|
||||
listen [::]:80;
|
||||
<% } -%>
|
||||
|
||||
<% if (vhost) { -%>
|
||||
server_name <%= vhost %>;
|
||||
<% } else { %>
|
||||
<% } 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;
|
||||
}
|
||||
<% } -%>
|
||||
|
||||
# acme challenges (for cert renewal where the vhost config exists)
|
||||
location /.well-known/acme-challenge/ {
|
||||
default_type text/plain;
|
||||
alias /home/yellowtent/platformdata/acme/;
|
||||
}
|
||||
|
||||
location / {
|
||||
# redirect everything to HTTPS
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
# https server
|
||||
server {
|
||||
<% if (vhost) { -%>
|
||||
server_name <%= vhost %>;
|
||||
listen 443 http2;
|
||||
<% if (hasIPv6) { -%>
|
||||
listen [::]:443 http2;
|
||||
<% } -%>
|
||||
<% } else { -%>
|
||||
listen 443 http2 default_server;
|
||||
<% } %>
|
||||
<% if (hasIPv6) { -%>
|
||||
listen [::]:443 http2 default_server;
|
||||
<% } -%>
|
||||
<% } -%>
|
||||
|
||||
ssl on;
|
||||
# paths are relative to prefix and not to this file
|
||||
@@ -80,7 +121,7 @@ server {
|
||||
# No buffering to temp files, it fails for large downloads
|
||||
proxy_max_temp_file_size 0;
|
||||
|
||||
# Disable check to allow unlimited body sizes
|
||||
# Disable check to allow unlimited body sizes. this allows apps to accept whatever size they want
|
||||
client_max_body_size 0;
|
||||
|
||||
<% if (robotsTxtQuoted) { %>
|
||||
@@ -107,6 +148,11 @@ server {
|
||||
proxy_read_timeout 30m;
|
||||
}
|
||||
|
||||
location ~ ^/api/v1/apps/.*/upload$ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
client_max_body_size 0;
|
||||
}
|
||||
|
||||
# graphite paths (uncomment block below and visit /graphite/index.html)
|
||||
# location ~ ^/(graphite|content|metrics|dashboard|render|browser|composer)/ {
|
||||
# proxy_pass http://127.0.0.1:8000;
|
||||
|
||||
@@ -36,27 +36,21 @@ http {
|
||||
# zones for rate limiting
|
||||
limit_req_zone $binary_remote_addr zone=admin_login:10m rate=10r/s; # 10 request a second
|
||||
|
||||
# HTTP server
|
||||
|
||||
# default http server that returns 404 for any domain we are not listening on
|
||||
server {
|
||||
listen 80;
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
server_name does_not_match_anything;
|
||||
|
||||
# collectd
|
||||
location /nginx_status {
|
||||
stub_status on;
|
||||
access_log off;
|
||||
allow 127.0.0.1;
|
||||
deny all;
|
||||
}
|
||||
|
||||
# acme challenges
|
||||
# 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 / {
|
||||
# redirect everything to HTTPS
|
||||
return 301 https://$host$request_uri;
|
||||
return 404;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+8
-4
@@ -13,8 +13,8 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/reloadnginx.sh
|
||||
Defaults!/home/yellowtent/box/src/scripts/reboot.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/reboot.sh
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/reloadcollectd.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/reloadcollectd.sh
|
||||
Defaults!/home/yellowtent/box/src/scripts/configurecollectd.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/configurecollectd.sh
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/collectlogs.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/collectlogs.sh
|
||||
@@ -28,5 +28,9 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/update.sh
|
||||
Defaults!/home/yellowtent/box/src/scripts/authorized_keys.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/authorized_keys.sh
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/node.sh env_keep="HOME BOX_ENV NODE_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/node.sh
|
||||
Defaults!/home/yellowtent/box/src/scripts/configurelogrotate.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/configurelogrotate.sh
|
||||
|
||||
Defaults!/home/yellowtent/box/src/backuptask.js env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD:SETENV: /home/yellowtent/box/src/backuptask.js
|
||||
|
||||
|
||||
+5
-3
@@ -20,7 +20,6 @@ var appdb = require('./appdb.js'),
|
||||
async = require('async'),
|
||||
clients = require('./clients.js'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
ClientsError = clients.ClientsError,
|
||||
debug = require('debug')('box:addons'),
|
||||
docker = require('./docker.js'),
|
||||
@@ -249,7 +248,7 @@ function setupOauth(app, options, callback) {
|
||||
if (!app.sso) return callback(null);
|
||||
|
||||
var appId = app.id;
|
||||
var redirectURI = 'https://' + config.appFqdn(app.location);
|
||||
var redirectURI = 'https://' + (app.altDomain || config.appFqdn(app.location));
|
||||
var scope = 'profile';
|
||||
|
||||
clients.delByAppIdAndType(appId, clients.TYPE_OAUTH, function (error) { // remove existing creds
|
||||
@@ -364,6 +363,7 @@ function setupSendMail(app, options, callback) {
|
||||
var env = [
|
||||
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_SMTP_PORT', value: '2525' },
|
||||
{ name: 'MAIL_SMTPS_PORT', value: '4650' },
|
||||
{ name: 'MAIL_SMTP_USERNAME', value: mailbox.name },
|
||||
{ name: 'MAIL_SMTP_PASSWORD', value: password },
|
||||
{ name: 'MAIL_FROM', value: mailbox.name + '@' + config.fqdn() },
|
||||
@@ -645,7 +645,9 @@ function setupRedis(app, options, callback) {
|
||||
}
|
||||
|
||||
const tag = infra.images.redis.tag, redisName = 'redis-' + app.id;
|
||||
// note that we do not add appId label because this interferes with the stop/start app logic
|
||||
const cmd = `docker run --restart=always -d --name=${redisName} \
|
||||
--label=location=${app.location} \
|
||||
--net cloudron \
|
||||
--net-alias ${redisName} \
|
||||
-m ${memoryLimit/2} \
|
||||
@@ -692,7 +694,7 @@ function teardownRedis(app, options, callback) {
|
||||
|
||||
safe.fs.unlinkSync(paths.ADDON_CONFIG_DIR, 'redis-' + app.id + '_vars.sh');
|
||||
|
||||
shell.sudo('teardownRedis', [ RMAPPDIR_CMD, app.id + '/redis' ], function (error, stdout, stderr) {
|
||||
shell.sudo('teardownRedis', [ RMAPPDIR_CMD, app.id + '/redis', true /* delete directory */ ], function (error, stdout, stderr) {
|
||||
if (error) return callback(new Error('Error removing redis data:' + error));
|
||||
|
||||
appdb.unsetAddonConfig(app.id, 'redis', callback);
|
||||
|
||||
+31
-18
@@ -10,6 +10,7 @@ exports = module.exports = {
|
||||
update: update,
|
||||
getAll: getAll,
|
||||
getPortBindings: getPortBindings,
|
||||
delPortBinding: delPortBinding,
|
||||
|
||||
setAddonConfig: setAddonConfig,
|
||||
getAddonConfig: getAddonConfig,
|
||||
@@ -59,8 +60,8 @@ var assert = require('assert'),
|
||||
|
||||
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.installationProgress', 'apps.runState',
|
||||
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'apps.location', 'apps.dnsRecordId',
|
||||
'apps.accessRestrictionJson', 'apps.lastBackupId', 'apps.oldConfigJson', 'apps.memoryLimit', 'apps.altDomain',
|
||||
'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt' ].join(',');
|
||||
'apps.accessRestrictionJson', 'apps.restoreConfigJson', 'apps.oldConfigJson', 'apps.updateConfigJson', 'apps.memoryLimit', 'apps.altDomain',
|
||||
'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup' ].join(',');
|
||||
|
||||
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'environmentVariable', 'appId' ].join(',');
|
||||
|
||||
@@ -75,6 +76,14 @@ function postProcess(result) {
|
||||
result.oldConfig = safe.JSON.parse(result.oldConfigJson);
|
||||
delete result.oldConfigJson;
|
||||
|
||||
assert(result.updateConfigJson === null || typeof result.updateConfigJson === 'string');
|
||||
result.updateConfig = safe.JSON.parse(result.updateConfigJson);
|
||||
delete result.updateConfigJson;
|
||||
|
||||
assert(result.restoreConfigJson === null || typeof result.restoreConfigJson === 'string');
|
||||
result.restoreConfig = safe.JSON.parse(result.restoreConfigJson);
|
||||
delete result.restoreConfigJson;
|
||||
|
||||
assert(result.hostPorts === null || typeof result.hostPorts === 'string');
|
||||
assert(result.environmentVariables === null || typeof result.environmentVariables === 'string');
|
||||
|
||||
@@ -98,6 +107,7 @@ function postProcess(result) {
|
||||
result.xFrameOptions = result.xFrameOptions || 'SAMEORIGIN';
|
||||
|
||||
result.sso = !!result.sso; // make it bool
|
||||
result.enableBackup = !!result.enableBackup; // make it bool
|
||||
|
||||
assert(result.debugModeJson === null || typeof result.debugModeJson === 'string');
|
||||
result.debugMode = safe.JSON.parse(result.debugModeJson);
|
||||
@@ -187,14 +197,14 @@ function add(id, appStoreId, manifest, location, portBindings, data, callback) {
|
||||
var altDomain = data.altDomain || null;
|
||||
var xFrameOptions = data.xFrameOptions || '';
|
||||
var installationState = data.installationState || exports.ISTATE_PENDING_INSTALL;
|
||||
var lastBackupId = data.lastBackupId || null; // used when cloning
|
||||
var restoreConfigJson = data.restoreConfig ? JSON.stringify(data.restoreConfig) : null; // used when cloning
|
||||
var sso = 'sso' in data ? data.sso : null;
|
||||
var debugModeJson = data.debugMode ? JSON.stringify(data.debugMode) : null;
|
||||
|
||||
var queries = [];
|
||||
queries.push({
|
||||
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, lastBackupId, sso, debugModeJson) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
args: [ id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, lastBackupId, sso, debugModeJson ]
|
||||
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, restoreConfigJson, sso, debugModeJson) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
args: [ id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, restoreConfigJson, sso, debugModeJson ]
|
||||
});
|
||||
|
||||
Object.keys(portBindings).forEach(function (env) {
|
||||
@@ -247,6 +257,18 @@ function getPortBindings(id, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function delPortBinding(hostPort, callback) {
|
||||
assert.strictEqual(typeof hostPort, 'number');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM appPortBindings WHERE hostPort=?', [ hostPort ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function del(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -304,17 +326,8 @@ function updateWithConstraints(id, app, constraints, callback) {
|
||||
|
||||
var fields = [ ], values = [ ];
|
||||
for (var p in app) {
|
||||
if (p === 'manifest') {
|
||||
fields.push('manifestJson = ?');
|
||||
values.push(JSON.stringify(app[p]));
|
||||
} else if (p === 'oldConfig') {
|
||||
fields.push('oldConfigJson = ?');
|
||||
values.push(JSON.stringify(app[p]));
|
||||
} else if (p === 'accessRestriction') {
|
||||
fields.push('accessRestrictionJson = ?');
|
||||
values.push(JSON.stringify(app[p]));
|
||||
} else if (p === 'debugMode') {
|
||||
fields.push('debugModeJson = ?');
|
||||
if (p === 'manifest' || p === 'oldConfig' || p === 'updateConfig' || p === 'restoreConfig' || p === 'accessRestriction' || p === 'debugMode') {
|
||||
fields.push(`${p}Json = ?`);
|
||||
values.push(JSON.stringify(app[p]));
|
||||
} else if (p !== 'portBindings') {
|
||||
fields.push(p + ' = ?');
|
||||
@@ -367,14 +380,14 @@ function setInstallationCommand(appId, installationState, values, callback) {
|
||||
// Rules are:
|
||||
// uninstall is allowed in any state
|
||||
// force update is allowed in any state including pending_uninstall! (for better or worse)
|
||||
// restore is allowed from installed or error state
|
||||
// restore is allowed from installed or error state or currently restoring
|
||||
// configure is allowed in installed state or currently configuring or in error state
|
||||
// update and backup are allowed only in installed state
|
||||
|
||||
if (installationState === exports.ISTATE_PENDING_UNINSTALL || installationState === exports.ISTATE_PENDING_FORCE_UPDATE) {
|
||||
updateWithConstraints(appId, values, '', callback);
|
||||
} else if (installationState === exports.ISTATE_PENDING_RESTORE) {
|
||||
updateWithConstraints(appId, values, 'AND (installationState = "installed" OR installationState = "error")', callback);
|
||||
updateWithConstraints(appId, values, 'AND (installationState = "installed" OR installationState = "error" OR installationState = "pending_restore")', callback);
|
||||
} else if (installationState === exports.ISTATE_PENDING_UPDATE || installationState === exports.ISTATE_PENDING_BACKUP) {
|
||||
updateWithConstraints(appId, values, 'AND installationState = "installed"', callback);
|
||||
} else if (installationState === exports.ISTATE_PENDING_CONFIGURE) {
|
||||
|
||||
+11
-11
@@ -94,19 +94,20 @@ function checkAppHealth(app, callback) {
|
||||
superagent
|
||||
.get(healthCheckUrl)
|
||||
.set('Host', app.fqdn) // required for some apache configs with rewrite rules
|
||||
.set('User-Agent', 'Mozilla') // required for some apps (e.g. minio)
|
||||
.redirects(0)
|
||||
.timeout(HEALTHCHECK_INTERVAL)
|
||||
.end(function (error, res) {
|
||||
if (error && !error.response) {
|
||||
debugApp(app, 'not alive (network error): %s', error.message);
|
||||
setHealth(app, appdb.HEALTH_UNHEALTHY, callback);
|
||||
} else if (res.statusCode >= 400) { // 2xx and 3xx are ok
|
||||
debugApp(app, 'not alive : %s', error || res.status);
|
||||
setHealth(app, appdb.HEALTH_UNHEALTHY, callback);
|
||||
} else {
|
||||
setHealth(app, appdb.HEALTH_HEALTHY, callback);
|
||||
}
|
||||
});
|
||||
if (error && !error.response) {
|
||||
debugApp(app, 'not alive (network error): %s', error.message);
|
||||
setHealth(app, appdb.HEALTH_UNHEALTHY, callback);
|
||||
} else if (res.statusCode >= 400) { // 2xx and 3xx are ok
|
||||
debugApp(app, 'not alive : %s', error || res.status);
|
||||
setHealth(app, appdb.HEALTH_UNHEALTHY, callback);
|
||||
} else {
|
||||
setHealth(app, appdb.HEALTH_HEALTHY, callback);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -156,7 +157,6 @@ function processDockerEvents() {
|
||||
stream.setEncoding('utf8');
|
||||
stream.on('data', function (data) {
|
||||
var ev = JSON.parse(data);
|
||||
debug('Container ' + ev.id + ' went OOM');
|
||||
appdb.getByContainerId(ev.id, function (error, app) { // this can error for addons
|
||||
var program = error || !app.appStoreId ? ev.id : app.appStoreId;
|
||||
var context = JSON.stringify(ev);
|
||||
|
||||
+150
-52
@@ -30,13 +30,16 @@ exports = module.exports = {
|
||||
|
||||
checkManifestConstraints: checkManifestConstraints,
|
||||
|
||||
updateApps: updateApps,
|
||||
autoupdateApps: autoupdateApps,
|
||||
|
||||
restoreInstalledApps: restoreInstalledApps,
|
||||
configureInstalledApps: configureInstalledApps,
|
||||
|
||||
getAppConfig: getAppConfig,
|
||||
|
||||
downloadFile: downloadFile,
|
||||
uploadFile: uploadFile,
|
||||
|
||||
// exported for testing
|
||||
_validateHostname: validateHostname,
|
||||
_validatePortBindings: validatePortBindings,
|
||||
@@ -62,6 +65,7 @@ var addons = require('./addons.js'),
|
||||
groups = require('./groups.js'),
|
||||
mailboxdb = require('./mailboxdb.js'),
|
||||
manifestFormat = require('cloudron-manifestformat'),
|
||||
os = require('os'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
@@ -70,10 +74,11 @@ var addons = require('./addons.js'),
|
||||
split = require('split'),
|
||||
superagent = require('superagent'),
|
||||
taskmanager = require('./taskmanager.js'),
|
||||
TransformStream = require('stream').Transform,
|
||||
updateChecker = require('./updatechecker.js'),
|
||||
url = require('url'),
|
||||
util = require('util'),
|
||||
uuid = require('node-uuid'),
|
||||
uuid = require('uuid'),
|
||||
validator = require('validator');
|
||||
|
||||
// http://dustinsenos.com/articles/customErrorsInNode
|
||||
@@ -114,7 +119,7 @@ AppsError.BAD_CERTIFICATE = 'Invalid certificate';
|
||||
// https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
|
||||
// We are validating the validity of the location-fqdn as host name
|
||||
function validateHostname(location, fqdn) {
|
||||
var RESERVED_LOCATIONS = [ constants.ADMIN_LOCATION, constants.API_LOCATION, constants.SMTP_LOCATION, constants.IMAP_LOCATION, constants.MAIL_LOCATION, constants.POSTMAN_LOCATION ];
|
||||
var RESERVED_LOCATIONS = [ config.adminLocation(), constants.API_LOCATION, constants.SMTP_LOCATION, constants.IMAP_LOCATION, config.mailLocation(), constants.POSTMAN_LOCATION ];
|
||||
|
||||
if (RESERVED_LOCATIONS.indexOf(location) !== -1) return new AppsError(AppsError.BAD_FIELD, location + ' is reserved');
|
||||
|
||||
@@ -204,7 +209,7 @@ function validateMemoryLimit(manifest, memoryLimit) {
|
||||
assert.strictEqual(typeof memoryLimit, 'number');
|
||||
|
||||
var min = manifest.memoryLimit || constants.DEFAULT_MEMORY_LIMIT;
|
||||
var max = (4096 * 1024 * 1024);
|
||||
var max = os.totalmem() * 2; // this will overallocate since we don't allocate equal swap always (#466)
|
||||
|
||||
// allow 0, which indicates that it is not set, the one from the manifest will be choosen but we don't commit any user value
|
||||
// this is needed so an app update can change the value in the manifest, and if not set by the user, the new value should be used
|
||||
@@ -254,6 +259,12 @@ function validateRobotsTxt(robotsTxt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateBackupFormat(format) {
|
||||
if (format === 'tgz' || format == 'rsync') return null;
|
||||
|
||||
return new AppsError(AppsError.BAD_FIELD, 'Invalid backup format');
|
||||
}
|
||||
|
||||
function getDuplicateErrorDetails(location, portBindings, error) {
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
@@ -304,12 +315,18 @@ function hasAccessTo(app, user, callback) {
|
||||
if (app.accessRestriction.users.some(function (e) { return e === user.id; })) return callback(null, true);
|
||||
|
||||
// check group access
|
||||
if (!app.accessRestriction.groups) return callback(null, false);
|
||||
groups.getGroups(user.id, function (error, groupIds) {
|
||||
if (error) return callback(null, false);
|
||||
|
||||
async.some(app.accessRestriction.groups, function (groupId, iteratorDone) {
|
||||
groups.isMember(groupId, user.id, iteratorDone);
|
||||
}, function (error, result) {
|
||||
callback(null, !error && result);
|
||||
const isAdmin = groupIds.indexOf(constants.ADMIN_GROUP_ID) !== -1;
|
||||
|
||||
if (isAdmin) return callback(null, true); // admins can always access any app
|
||||
|
||||
if (!app.accessRestriction.groups) return callback(null, false);
|
||||
|
||||
if (app.accessRestriction.groups.some(function (gid) { return groupIds.indexOf(gid) !== -1; })) return callback(null, true);
|
||||
|
||||
callback(null, false);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -415,7 +432,9 @@ function install(data, auditSource, callback) {
|
||||
sso = 'sso' in data ? data.sso : null,
|
||||
debugMode = data.debugMode || null,
|
||||
robotsTxt = data.robotsTxt || null,
|
||||
backupId = data.backupId || null;
|
||||
enableBackup = 'enableBackup' in data ? data.enableBackup : true,
|
||||
backupId = data.backupId || null,
|
||||
backupFormat = data.backupFormat || 'tgz';
|
||||
|
||||
assert(data.appStoreId || data.manifest); // atleast one of them is required
|
||||
|
||||
@@ -449,6 +468,9 @@ function install(data, auditSource, callback) {
|
||||
error = validateRobotsTxt(robotsTxt);
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validateBackupFormat(backupFormat);
|
||||
if (error) return callback(error);
|
||||
|
||||
if ('sso' in data && !('optionalSso' in manifest)) return callback(new AppsError(AppsError.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'];
|
||||
@@ -484,7 +506,9 @@ function install(data, auditSource, callback) {
|
||||
sso: sso,
|
||||
debugMode: debugMode,
|
||||
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app',
|
||||
lastBackupId: backupId
|
||||
restoreConfig: backupId ? { backupId: backupId, backupFormat: backupFormat } : null,
|
||||
enableBackup: enableBackup,
|
||||
robotsTxt: robotsTxt
|
||||
};
|
||||
|
||||
appdb.add(appId, appStoreId, manifest, location, portBindings, data, function (error) {
|
||||
@@ -583,6 +607,8 @@ function configure(appId, data, auditSource, callback) {
|
||||
}
|
||||
}
|
||||
|
||||
if ('enableBackup' in data) values.enableBackup = data.enableBackup;
|
||||
|
||||
values.oldConfig = getAppConfig(app);
|
||||
|
||||
debug('Will configure app with id:%s values:%j', appId, values);
|
||||
@@ -620,7 +646,7 @@ function update(appId, data, auditSource, callback) {
|
||||
downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var values = { };
|
||||
var updateConfig = { };
|
||||
|
||||
error = manifestFormat.parse(manifest);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest error:' + error.message));
|
||||
@@ -628,13 +654,7 @@ function update(appId, data, auditSource, callback) {
|
||||
error = checkManifestConstraints(manifest);
|
||||
if (error) return callback(error);
|
||||
|
||||
values.manifest = manifest;
|
||||
|
||||
if ('portBindings' in data) {
|
||||
values.portBindings = data.portBindings;
|
||||
error = validatePortBindings(data.portBindings, values.manifest.tcpPorts);
|
||||
if (error) return callback(error);
|
||||
}
|
||||
updateConfig.manifest = manifest;
|
||||
|
||||
if ('icon' in data) {
|
||||
if (data.icon) {
|
||||
@@ -654,26 +674,23 @@ function update(appId, data, auditSource, callback) {
|
||||
|
||||
// prevent user from installing a app with different manifest id over an existing app
|
||||
// this allows cloudron install -f --app <appid> for an app installed from the appStore
|
||||
if (app.manifest.id !== values.manifest.id) {
|
||||
if (app.manifest.id !== updateConfig.manifest.id) {
|
||||
if (!data.force) return callback(new AppsError(AppsError.BAD_FIELD, 'manifest id does not match. force to override'));
|
||||
// clear appStoreId so that this app does not get updates anymore
|
||||
values.appStoreId = '';
|
||||
updateConfig.appStoreId = '';
|
||||
}
|
||||
|
||||
// do not update apps in debug mode
|
||||
if (app.debugMode && !data.force) return callback(new AppsError(AppsError.BAD_STATE, 'debug mode enabled. force to override'));
|
||||
|
||||
// Ensure we update the memory limit in case the new app requires more memory as a minimum
|
||||
// 0 and -1 are special values for memory limit indicating unset and unlimited
|
||||
if (app.memoryLimit > 0 && values.manifest.memoryLimit && app.memoryLimit < values.manifest.memoryLimit) {
|
||||
values.memoryLimit = values.manifest.memoryLimit;
|
||||
// 0 and -1 are special updateConfig for memory limit indicating unset and unlimited
|
||||
if (app.memoryLimit > 0 && updateConfig.manifest.memoryLimit && app.memoryLimit < updateConfig.manifest.memoryLimit) {
|
||||
updateConfig.memoryLimit = updateConfig.manifest.memoryLimit;
|
||||
}
|
||||
|
||||
values.oldConfig = getAppConfig(app);
|
||||
|
||||
appdb.setInstallationCommand(appId, data.force ? appdb.ISTATE_PENDING_FORCE_UPDATE : appdb.ISTATE_PENDING_UPDATE, values, function (error) {
|
||||
appdb.setInstallationCommand(appId, data.force ? appdb.ISTATE_PENDING_FORCE_UPDATE : appdb.ISTATE_PENDING_UPDATE, { updateConfig: updateConfig }, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails('' /* location cannot conflict */, values.portBindings, error));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
taskmanager.restartAppTask(appId);
|
||||
@@ -754,22 +771,22 @@ function restore(appId, data, auditSource, callback) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
// for empty or null backupId, use existing manifest to mimic a reinstall
|
||||
var func = data.backupId ? backups.getRestoreConfig.bind(null, data.backupId) : function (next) { return next(null, { manifest: app.manifest }); };
|
||||
var func = data.backupId ? backups.get.bind(null, data.backupId) : function (next) { return next(null, { manifest: app.manifest }); };
|
||||
|
||||
func(function (error, restoreConfig) {
|
||||
func(function (error, backupInfo) {
|
||||
if (error && error.reason === BackupsError.NOT_FOUND) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
|
||||
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (!restoreConfig) callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Could not get restore config'));
|
||||
if (!backupInfo.manifest) callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Could not get restore manifest'));
|
||||
|
||||
// re-validate because this new box version may not accept old configs
|
||||
error = checkManifestConstraints(restoreConfig.manifest);
|
||||
error = checkManifestConstraints(backupInfo.manifest);
|
||||
if (error) return callback(error);
|
||||
|
||||
var values = {
|
||||
lastBackupId: data.backupId || null, // when null, apptask simply reinstalls
|
||||
manifest: restoreConfig.manifest,
|
||||
restoreConfig: data.backupId ? { backupId: data.backupId, backupFormat: backupInfo.format } : null, // when null, apptask simply reinstalls
|
||||
manifest: backupInfo.manifest,
|
||||
|
||||
oldConfig: getAppConfig(app)
|
||||
};
|
||||
@@ -808,24 +825,24 @@ function clone(appId, data, auditSource, callback) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
backups.getRestoreConfig(backupId, function (error, restoreConfig) {
|
||||
backups.get(backupId, function (error, backupInfo) {
|
||||
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
|
||||
if (error && error.reason === BackupsError.NOT_FOUND) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (!restoreConfig) callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Could not get restore config'));
|
||||
if (!backupInfo.manifest) callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Could not get restore config'));
|
||||
|
||||
// re-validate because this new box version may not accept old configs
|
||||
error = checkManifestConstraints(restoreConfig.manifest);
|
||||
error = checkManifestConstraints(backupInfo.manifest);
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validateHostname(location, config.fqdn());
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validatePortBindings(portBindings, restoreConfig.manifest.tcpPorts);
|
||||
error = validatePortBindings(portBindings, backupInfo.manifest.tcpPorts);
|
||||
if (error) return callback(error);
|
||||
|
||||
var newAppId = uuid.v4(), appStoreId = app.appStoreId, manifest = restoreConfig.manifest;
|
||||
var newAppId = uuid.v4(), appStoreId = app.appStoreId, manifest = backupInfo.manifest;
|
||||
|
||||
appstore.purchase(newAppId, appStoreId, function (error) {
|
||||
if (error && error.reason === AppstoreError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
|
||||
@@ -838,7 +855,7 @@ function clone(appId, data, auditSource, callback) {
|
||||
memoryLimit: app.memoryLimit,
|
||||
accessRestriction: app.accessRestriction,
|
||||
xFrameOptions: app.xFrameOptions,
|
||||
lastBackupId: backupId,
|
||||
restoreConfig: { backupId: backupId, backupFormat: backupInfo.format },
|
||||
sso: !!app.sso,
|
||||
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app'
|
||||
};
|
||||
@@ -995,23 +1012,18 @@ function exec(appId, options, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function updateApps(updateInfo, auditSource, callback) { // updateInfo is { appId -> { manifest } }
|
||||
function autoupdateApps(updateInfo, auditSource, callback) { // updateInfo is { appId -> { manifest } }
|
||||
assert.strictEqual(typeof updateInfo, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
function canAutoupdateApp(app, newManifest) {
|
||||
if ((semver.major(app.manifest.version) !== 0) && (semver.major(app.manifest.version) !== semver.major(newManifest.version))) return false; // major changes are blocking
|
||||
if ((semver.major(app.manifest.version) !== 0) && (semver.major(app.manifest.version) !== semver.major(newManifest.version))) return new Error('Major version change'); // major changes are blocking
|
||||
|
||||
var newTcpPorts = newManifest.tcpPorts || { };
|
||||
var oldTcpPorts = app.manifest.tcpPorts || { };
|
||||
var portBindings = app.portBindings; // this is never null
|
||||
|
||||
for (var env in newTcpPorts) {
|
||||
if (!(env in oldTcpPorts)) return new Error(env + ' is required from user');
|
||||
}
|
||||
|
||||
for (env in portBindings) {
|
||||
for (var env in portBindings) {
|
||||
if (!(env in newTcpPorts)) return new Error(env + ' was in use but new update removes it');
|
||||
}
|
||||
|
||||
@@ -1026,7 +1038,7 @@ function updateApps(updateInfo, auditSource, callback) { // updateInfo is { appI
|
||||
if (error) {
|
||||
debug('Cannot autoupdate app %s : %s', appId, error.message);
|
||||
return iteratorDone();
|
||||
}
|
||||
}
|
||||
|
||||
error = canAutoupdateApp(app, updateInfo[appId].manifest);
|
||||
if (error) {
|
||||
@@ -1096,10 +1108,14 @@ function restoreInstalledApps(callback) {
|
||||
async.map(apps, function (app, iteratorDone) {
|
||||
debug('marking %s for restore', app.location || app.id);
|
||||
|
||||
appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_RESTORE, { oldConfig: null }, function (error) {
|
||||
if (error) debug('did not mark %s for restore', app.location || app.id, error);
|
||||
backups.getByAppIdPaged(1, 1, app.id, function (error, results) {
|
||||
var restoreConfig = !error && results.length ? { backupId: results[0].id, backupFormat: results[0].format } : null;
|
||||
|
||||
iteratorDone(); // always succeed
|
||||
appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_RESTORE, { restoreConfig: restoreConfig, oldConfig: null }, function (error) {
|
||||
if (error) debug('did not mark %s for restore', app.location || app.id, error);
|
||||
|
||||
iteratorDone(); // always succeed
|
||||
});
|
||||
});
|
||||
}, callback);
|
||||
});
|
||||
@@ -1122,3 +1138,85 @@ function configureInstalledApps(callback) {
|
||||
}, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function downloadFile(appId, filePath, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof filePath, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
exec(appId, { cmd: [ 'stat', '--printf=%F-%s', filePath ], tty: true }, function (error, stream) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var data = '';
|
||||
stream.setEncoding('utf8');
|
||||
stream.on('data', function (d) { data += d; });
|
||||
stream.on('end', function () {
|
||||
var parts = data.split('-');
|
||||
if (parts.length !== 2) return callback(new AppsError(AppsError.NOT_FOUND, 'file does not exist'));
|
||||
|
||||
var type = parts[0], filename, cmd, size;
|
||||
|
||||
if (type === 'regular file') {
|
||||
cmd = [ 'cat', filePath ];
|
||||
size = parseInt(parts[1], 10);
|
||||
filename = path.basename(filePath);
|
||||
if (isNaN(size)) return callback(new AppsError(AppsError.NOT_FOUND, 'file does not exist'));
|
||||
} else if (type === 'directory') {
|
||||
cmd = ['tar', 'zcf', '-', '-C', filePath, '.'];
|
||||
filename = path.basename(filePath) + '.tar.gz';
|
||||
size = 0; // unknown
|
||||
} else {
|
||||
return callback(new AppsError(AppsError.NOT_FOUND, 'only files or dirs can be downloaded'));
|
||||
}
|
||||
|
||||
exec(appId, { cmd: cmd , tty: false }, function (error, stream) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var stdoutStream = new TransformStream({
|
||||
transform: function (chunk, ignoredEncoding, callback) {
|
||||
this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk;
|
||||
|
||||
while (true) {
|
||||
if (this._buffer.length < 8) break; // header is 8 bytes
|
||||
|
||||
var type = this._buffer.readUInt8(0);
|
||||
var len = this._buffer.readUInt32BE(4);
|
||||
|
||||
if (this._buffer.length < (8 + len)) break; // not enough
|
||||
|
||||
var payload = this._buffer.slice(8, 8 + len);
|
||||
|
||||
this._buffer = this._buffer.slice(8+len); // consumed
|
||||
|
||||
if (type === 1) this.push(payload);
|
||||
}
|
||||
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
stream.pipe(stdoutStream);
|
||||
|
||||
return callback(null, stdoutStream, { filename: filename, size: size });
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function uploadFile(appId, sourceFilePath, destFilePath, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof sourceFilePath, 'string');
|
||||
assert.strictEqual(typeof destFilePath, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
exec(appId, { cmd: [ 'bash', '-c', 'cat - > ' + destFilePath ], tty: false }, function (error, stream) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var readFile = fs.createReadStream(sourceFilePath);
|
||||
readFile.on('error', callback);
|
||||
|
||||
readFile.pipe(stream);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
+54
-7
@@ -11,6 +11,10 @@ exports = module.exports = {
|
||||
getAppUpdate: getAppUpdate,
|
||||
getBoxUpdate: getBoxUpdate,
|
||||
|
||||
getAccount: getAccount,
|
||||
|
||||
sendFeedback: sendFeedback,
|
||||
|
||||
AppstoreError: AppstoreError
|
||||
};
|
||||
|
||||
@@ -162,16 +166,17 @@ function sendAliveStatus(data, callback) {
|
||||
provider: result[settings.TLS_CONFIG_KEY].provider
|
||||
},
|
||||
backupConfig: {
|
||||
provider: result[settings.BACKUP_CONFIG_KEY].provider
|
||||
provider: result[settings.BACKUP_CONFIG_KEY].provider,
|
||||
hardlinks: !result[settings.BACKUP_CONFIG_KEY].noHardlinks
|
||||
},
|
||||
mailConfig: {
|
||||
enabled: result[settings.MAIL_CONFIG_KEY].enabled
|
||||
},
|
||||
mailRelay: {
|
||||
provider: result[settings.MAIL_RELAY_KEY].provider
|
||||
},
|
||||
mailCatchAll: {
|
||||
count: result[settings.CATCH_ALL_ADDRESS_KEY].length
|
||||
},
|
||||
mailRelay: {
|
||||
provider: result[settings.MAIL_RELAY_KEY].provider
|
||||
},
|
||||
mailCatchAll: {
|
||||
count: result[settings.CATCH_ALL_ADDRESS_KEY].length
|
||||
},
|
||||
autoupdatePattern: result[settings.AUTOUPDATE_PATTERN_KEY],
|
||||
timeZone: result[settings.TIME_ZONE_KEY],
|
||||
@@ -180,6 +185,7 @@ function sendAliveStatus(data, callback) {
|
||||
var data = {
|
||||
domain: config.fqdn(),
|
||||
version: config.version(),
|
||||
adminFqdn: config.adminFqdn(),
|
||||
provider: config.provider(),
|
||||
backendSettings: backendSettings,
|
||||
machine: {
|
||||
@@ -245,3 +251,44 @@ function getAppUpdate(app, callback) {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getAccount(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getAppstoreConfig(function (error, appstoreConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId;
|
||||
|
||||
superagent.get(url).query({ accessToken: appstoreConfig.token }).timeout(10 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
|
||||
|
||||
// { profile: { id, email, groupId, billing, firstName, lastName, company, street, city, zip, state, country } }
|
||||
callback(null, result.body.profile);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function sendFeedback(info, callback) {
|
||||
assert.strictEqual(typeof info, 'object');
|
||||
assert.strictEqual(typeof info.email, 'string');
|
||||
assert.strictEqual(typeof info.displayName, 'string');
|
||||
assert.strictEqual(typeof info.type, 'string');
|
||||
assert.strictEqual(typeof info.subject, 'string');
|
||||
assert.strictEqual(typeof info.description, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getAppstoreConfig(function (error, appstoreConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/feedback';
|
||||
|
||||
superagent.post(url).query({ accessToken: appstoreConfig.token }).send(info).timeout(10 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+127
-50
@@ -35,6 +35,7 @@ var addons = require('./addons.js'),
|
||||
certificates = require('./certificates.js'),
|
||||
config = require('./config.js'),
|
||||
database = require('./database.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:apptask'),
|
||||
docker = require('./docker.js'),
|
||||
ejs = require('ejs'),
|
||||
@@ -42,6 +43,7 @@ var addons = require('./addons.js'),
|
||||
manifestFormat = require('cloudron-manifestformat'),
|
||||
net = require('net'),
|
||||
nginx = require('./nginx.js'),
|
||||
os = require('os'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
@@ -55,7 +57,9 @@ var addons = require('./addons.js'),
|
||||
_ = require('underscore');
|
||||
|
||||
var COLLECTD_CONFIG_EJS = fs.readFileSync(__dirname + '/collectd.config.ejs', { encoding: 'utf8' }),
|
||||
RELOAD_COLLECTD_CMD = path.join(__dirname, 'scripts/reloadcollectd.sh'),
|
||||
CONFIGURE_COLLECTD_CMD = path.join(__dirname, 'scripts/configurecollectd.sh'),
|
||||
LOGROTATE_CONFIG_EJS = fs.readFileSync(__dirname + '/logrotate.ejs', { encoding: 'utf8' }),
|
||||
CONFIGURE_LOGROTATE_CMD = path.join(__dirname, 'scripts/configurelogrotate.sh'),
|
||||
RMAPPDIR_CMD = path.join(__dirname, 'scripts/rmappdir.sh'),
|
||||
CREATEAPPDIR_CMD = path.join(__dirname, 'scripts/createappdir.sh');
|
||||
|
||||
@@ -127,7 +131,7 @@ function deleteContainers(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'deleting containers');
|
||||
debugApp(app, 'deleting app containers (app, scheduler)');
|
||||
|
||||
docker.deleteContainers(app.id, function (error) {
|
||||
if (error) return callback(new Error('Error deleting container: ' + error));
|
||||
@@ -143,11 +147,12 @@ function createVolume(app, callback) {
|
||||
shell.sudo('createVolume', [ CREATEAPPDIR_CMD, app.id ], callback);
|
||||
}
|
||||
|
||||
function deleteVolume(app, callback) {
|
||||
function deleteVolume(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
shell.sudo('deleteVolume', [ RMAPPDIR_CMD, app.id ], callback);
|
||||
shell.sudo('deleteVolume', [ RMAPPDIR_CMD, app.id, !!options.removeDirectory ], callback);
|
||||
}
|
||||
|
||||
function addCollectdProfile(app, callback) {
|
||||
@@ -157,7 +162,7 @@ function addCollectdProfile(app, callback) {
|
||||
var collectdConf = ejs.render(COLLECTD_CONFIG_EJS, { appId: app.id, containerId: app.containerId });
|
||||
fs.writeFile(path.join(paths.COLLECTD_APPCONFIG_DIR, app.id + '.conf'), collectdConf, function (error) {
|
||||
if (error) return callback(error);
|
||||
shell.sudo('addCollectdProfile', [ RELOAD_COLLECTD_CMD ], callback);
|
||||
shell.sudo('addCollectdProfile', [ CONFIGURE_COLLECTD_CMD, 'add', app.id ], callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -167,17 +172,41 @@ function removeCollectdProfile(app, callback) {
|
||||
|
||||
fs.unlink(path.join(paths.COLLECTD_APPCONFIG_DIR, app.id + '.conf'), function (error) {
|
||||
if (error && error.code !== 'ENOENT') debugApp(app, 'Error removing collectd profile', error);
|
||||
shell.sudo('removeCollectdProfile', [ RELOAD_COLLECTD_CMD ], callback);
|
||||
shell.sudo('removeCollectdProfile', [ CONFIGURE_COLLECTD_CMD, 'remove', app.id ], callback);
|
||||
});
|
||||
}
|
||||
|
||||
function verifyManifest(app, callback) {
|
||||
function addLogrotateConfig(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'Verifying manifest');
|
||||
docker.inspect(app.containerId, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var runVolume = result.Mounts.find(function (mount) { return mount.Destination === '/run'; });
|
||||
if (!runVolume) return callback(new Error('App does not have /run mounted'));
|
||||
|
||||
// logrotate configs can have arbitrary commands, so the config files must be owned by root
|
||||
var logrotateConf = ejs.render(LOGROTATE_CONFIG_EJS, { volumePath: runVolume.Source });
|
||||
var tmpFilePath = path.join(os.tmpdir(), app.id + '.logrotate');
|
||||
fs.writeFile(tmpFilePath, logrotateConf, function (error) {
|
||||
if (error) return callback(error);
|
||||
shell.sudo('addLogrotateConfig', [ CONFIGURE_LOGROTATE_CMD, 'add', app.id, tmpFilePath ], callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function removeLogrotateConfig(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
shell.sudo('removeLogrotateConfig', [ CONFIGURE_LOGROTATE_CMD, 'remove', app.id ], callback);
|
||||
}
|
||||
|
||||
function verifyManifest(manifest, callback) {
|
||||
assert.strictEqual(typeof manifest, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var manifest = app.manifest;
|
||||
var error = manifestFormat.parse(manifest);
|
||||
if (error) return callback(new Error(util.format('Manifest error: %s', error.message)));
|
||||
|
||||
@@ -210,7 +239,7 @@ function downloadIcon(app, callback) {
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, app.id + '.png'), res.body)) return retryCallback(new Error('Error saving icon:' + safe.error.message));
|
||||
|
||||
retryCallback(null);
|
||||
});
|
||||
});
|
||||
}, callback);
|
||||
}
|
||||
|
||||
@@ -242,6 +271,7 @@ function registerSubdomain(app, overwrite, callback) {
|
||||
}, function (error, result) {
|
||||
if (error || result instanceof Error) return callback(error || result);
|
||||
|
||||
// dnsRecordId tracks whether we created this DNS record so that we can unregister later
|
||||
updateApp(app, { dnsRecordId: result }, callback);
|
||||
});
|
||||
});
|
||||
@@ -258,6 +288,11 @@ function unregisterSubdomain(app, location, callback) {
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
if (!app.dnsRecordId) {
|
||||
debugApp(app, 'Skip unregister of record not created by cloudron');
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(error);
|
||||
|
||||
@@ -349,30 +384,34 @@ function updateApp(app, values, callback) {
|
||||
// - setup addons (requires the above volume)
|
||||
// - setup the container (requires image, volumes, addons)
|
||||
// - setup collectd (requires container id)
|
||||
// restore is also handled here since restore is just an install with some oldConfig to clean up
|
||||
function install(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const backupId = app.lastBackupId, isRestoring = app.installationState === appdb.ISTATE_PENDING_RESTORE;
|
||||
const restoreConfig = app.restoreConfig, isRestoring = app.installationState === appdb.ISTATE_PENDING_RESTORE;
|
||||
|
||||
async.series([
|
||||
verifyManifest.bind(null, app),
|
||||
// this protects against the theoretical possibility of an app being marked for install/restore from
|
||||
// a previous version of box code
|
||||
verifyManifest.bind(null, app.manifest),
|
||||
|
||||
// teardown for re-installs
|
||||
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
|
||||
unconfigureNginx.bind(null, app),
|
||||
removeCollectdProfile.bind(null, app),
|
||||
removeLogrotateConfig.bind(null, app),
|
||||
stopApp.bind(null, app),
|
||||
deleteContainers.bind(null, app),
|
||||
// oldConfig can be null during upgrades
|
||||
addons.teardownAddons.bind(null, app, app.oldConfig ? app.oldConfig.manifest.addons : app.manifest.addons),
|
||||
deleteVolume.bind(null, app),
|
||||
deleteVolume.bind(null, app, { removeDirectory: false }), // do not remove any symlinked volume
|
||||
|
||||
// for restore case
|
||||
function deleteImageIfChanged(done) {
|
||||
if (!app.oldConfig || (app.oldConfig.manifest.dockerImage === app.manifest.dockerImage)) return done();
|
||||
if (!app.oldConfig || (app.oldConfig.manifest.dockerImage === app.manifest.dockerImage)) return done();
|
||||
|
||||
docker.deleteImage(app.oldConfig.manifest, done);
|
||||
docker.deleteImage(app.oldConfig.manifest, done);
|
||||
},
|
||||
|
||||
reserveHttpPort.bind(null, app),
|
||||
@@ -390,7 +429,7 @@ function install(app, callback) {
|
||||
createVolume.bind(null, app),
|
||||
|
||||
function restoreFromBackup(next) {
|
||||
if (!backupId) {
|
||||
if (!restoreConfig) {
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '60, Setting up addons' }),
|
||||
addons.setupAddons.bind(null, app, app.manifest.addons),
|
||||
@@ -398,7 +437,7 @@ function install(app, callback) {
|
||||
} else {
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '60, Download backup and restoring addons' }),
|
||||
backups.restoreApp.bind(null, app, app.manifest.addons, backupId),
|
||||
backups.restoreApp.bind(null, app, app.manifest.addons, restoreConfig),
|
||||
], next);
|
||||
}
|
||||
},
|
||||
@@ -406,6 +445,9 @@ function install(app, callback) {
|
||||
updateApp.bind(null, app, { installationProgress: '70, Creating container' }),
|
||||
createContainer.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '75, Setting up logrotate config' }),
|
||||
addLogrotateConfig.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '80, Setting up collectd profile' }),
|
||||
addCollectdProfile.bind(null, app),
|
||||
|
||||
@@ -415,7 +457,7 @@ function install(app, callback) {
|
||||
exports._waitForDnsPropagation.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '90, Waiting for External Domain setup' }),
|
||||
exports._waitForAltDomainDnsPropagation.bind(null, app), // required when restoring and !lastBackupId
|
||||
exports._waitForAltDomainDnsPropagation.bind(null, app), // required when restoring and !restoreConfig
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '95, Configure nginx' }),
|
||||
configureNginx.bind(null, app),
|
||||
@@ -438,11 +480,9 @@ function backup(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var prefix = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,'');
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '10, Backing up' }),
|
||||
backups.backupApp.bind(null, app, app.manifest, prefix),
|
||||
backups.backupApp.bind(null, app, app.manifest),
|
||||
|
||||
// done!
|
||||
function (callback) {
|
||||
@@ -463,15 +503,18 @@ function configure(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// oldConfig can be null during an infra update
|
||||
var locationChanged = app.oldConfig && app.oldConfig.location !== app.location;
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
|
||||
unconfigureNginx.bind(null, app),
|
||||
removeCollectdProfile.bind(null, app),
|
||||
removeLogrotateConfig.bind(null, app),
|
||||
stopApp.bind(null, app),
|
||||
deleteContainers.bind(null, app),
|
||||
function (next) {
|
||||
// oldConfig can be null during an infra update
|
||||
if (!app.oldConfig || app.oldConfig.location === app.location) return next();
|
||||
if (!locationChanged) return next();
|
||||
unregisterSubdomain(app, app.oldConfig.location, next);
|
||||
},
|
||||
|
||||
@@ -481,7 +524,7 @@ function configure(app, callback) {
|
||||
downloadIcon.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '35, Registering subdomain' }),
|
||||
registerSubdomain.bind(null, app, true /* overwrite */),
|
||||
registerSubdomain.bind(null, app, !locationChanged /* overwrite */), // if location changed, do not overwrite to detect conflicts
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '40, Downloading image' }),
|
||||
docker.downloadImage.bind(null, app.manifest),
|
||||
@@ -496,6 +539,9 @@ function configure(app, callback) {
|
||||
updateApp.bind(null, app, { installationProgress: '60, Creating container' }),
|
||||
createContainer.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '65, Setting up logrotate config' }),
|
||||
addLogrotateConfig.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '70, Add collectd profile' }),
|
||||
addCollectdProfile.bind(null, app),
|
||||
|
||||
@@ -529,47 +575,72 @@ function update(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'Updating to %s', safe.query(app, 'manifest.version'));
|
||||
debugApp(app, `Updating to ${app.updateConfig.manifest.version}`);
|
||||
|
||||
// app does not want these addons anymore
|
||||
// FIXME: this does not handle option changes (like multipleDatabases)
|
||||
var unusedAddons = _.omit(app.oldConfig.manifest.addons, Object.keys(app.manifest.addons));
|
||||
var unusedAddons = _.omit(app.manifest.addons, Object.keys(app.updateConfig.manifest.addons));
|
||||
|
||||
async.series([
|
||||
// this protects against the theoretical possibility of an app being marked for update from
|
||||
// a previous version of box code
|
||||
updateApp.bind(null, app, { installationProgress: '0, Verify manifest' }),
|
||||
verifyManifest.bind(null, app),
|
||||
|
||||
// download new image before app is stopped. this is so we can reduce downtime
|
||||
// and also not remove the 'common' layers when the old image is deleted
|
||||
updateApp.bind(null, app, { installationProgress: '15, Downloading image' }),
|
||||
docker.downloadImage.bind(null, app.manifest),
|
||||
|
||||
// note: we cleanup first and then backup. this is done so that the app is not running should backup fail
|
||||
// we cannot easily 'recover' from backup failures because we have to revert manfest and portBindings
|
||||
updateApp.bind(null, app, { installationProgress: '25, Cleaning up old install' }),
|
||||
removeCollectdProfile.bind(null, app),
|
||||
stopApp.bind(null, app),
|
||||
deleteContainers.bind(null, app),
|
||||
function deleteImageIfChanged(done) {
|
||||
if (app.oldConfig.manifest.dockerImage === app.manifest.dockerImage) return done();
|
||||
|
||||
docker.deleteImage(app.oldConfig.manifest, done);
|
||||
},
|
||||
verifyManifest.bind(null, app.updateConfig.manifest),
|
||||
|
||||
function (next) {
|
||||
if (app.installationState === appdb.ISTATE_PENDING_FORCE_UPDATE) return next(null);
|
||||
|
||||
var prefix = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,'');
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '30, Backing up app' }),
|
||||
backups.backupApp.bind(null, app, app.oldConfig.manifest, prefix)
|
||||
updateApp.bind(null, app, { installationProgress: '15, Backing up app' }),
|
||||
backups.backupApp.bind(null, app, app.manifest)
|
||||
], next);
|
||||
},
|
||||
|
||||
// download new image before app is stopped. this is so we can reduce downtime
|
||||
// and also not remove the 'common' layers when the old image is deleted
|
||||
updateApp.bind(null, app, { installationProgress: '25, Downloading image' }),
|
||||
docker.downloadImage.bind(null, app.updateConfig.manifest),
|
||||
|
||||
// note: we cleanup first and then backup. this is done so that the app is not running should backup fail
|
||||
// we cannot easily 'recover' from backup failures because we have to revert manfest and portBindings
|
||||
updateApp.bind(null, app, { installationProgress: '35, Cleaning up old install' }),
|
||||
removeCollectdProfile.bind(null, app),
|
||||
removeLogrotateConfig.bind(null, app),
|
||||
stopApp.bind(null, app),
|
||||
deleteContainers.bind(null, app),
|
||||
function deleteImageIfChanged(done) {
|
||||
if (app.manifest.dockerImage === app.updateConfig.manifest.dockerImage) return done();
|
||||
|
||||
docker.deleteImage(app.manifest, done);
|
||||
},
|
||||
|
||||
// only delete unused addons after backup
|
||||
addons.teardownAddons.bind(null, app, unusedAddons),
|
||||
|
||||
// free unused ports
|
||||
function (next) {
|
||||
// make sure we always have objects
|
||||
var currentPorts = app.portBindings || {};
|
||||
var newPorts = app.updateConfig.manifest.tcpPorts || {};
|
||||
|
||||
async.each(Object.keys(currentPorts), function (portName, callback) {
|
||||
if (newPorts[portName]) return callback(); // port still in use
|
||||
|
||||
appdb.delPortBinding(currentPorts[portName], function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) console.error('Portbinding does not exist in database.');
|
||||
else if (error) return next(error);
|
||||
|
||||
// also delete from app object for further processing (the db is updated in the next step)
|
||||
delete app.portBindings[portName];
|
||||
|
||||
callback();
|
||||
});
|
||||
}, next);
|
||||
},
|
||||
|
||||
// switch over to the new config. manifest, memoryLimit, portBindings, appstoreId are updated here
|
||||
updateApp.bind(null, app, app.updateConfig),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '45, Downloading icon' }),
|
||||
downloadIcon.bind(null, app),
|
||||
|
||||
@@ -579,6 +650,9 @@ function update(app, callback) {
|
||||
updateApp.bind(null, app, { installationProgress: '80, Creating container' }),
|
||||
createContainer.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '85, Setting up logrotate config' }),
|
||||
addLogrotateConfig.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '90, Add collectd profile' }),
|
||||
addCollectdProfile.bind(null, app),
|
||||
|
||||
@@ -587,7 +661,7 @@ function update(app, callback) {
|
||||
// done!
|
||||
function (callback) {
|
||||
debugApp(app, 'updated');
|
||||
updateApp(app, { installationState: appdb.ISTATE_INSTALLED, installationProgress: '', health: null }, callback);
|
||||
updateApp(app, { installationState: appdb.ISTATE_INSTALLED, installationProgress: '', health: null, updateConfig: null }, callback);
|
||||
}
|
||||
], function seriesDone(error) {
|
||||
if (error) {
|
||||
@@ -608,6 +682,9 @@ function uninstall(app, callback) {
|
||||
updateApp.bind(null, app, { installationProgress: '0, Remove collectd profile' }),
|
||||
removeCollectdProfile.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '5, Remove logrotate config' }),
|
||||
removeLogrotateConfig.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '10, Stopping app' }),
|
||||
stopApp.bind(null, app),
|
||||
|
||||
@@ -618,7 +695,7 @@ function uninstall(app, callback) {
|
||||
addons.teardownAddons.bind(null, app, app.manifest.addons),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '40, Deleting volume' }),
|
||||
deleteVolume.bind(null, app),
|
||||
deleteVolume.bind(null, app, { removeDirectory: true }),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '50, Deleting image' }),
|
||||
docker.deleteImage.bind(null, app.manifest),
|
||||
|
||||
+35
-28
@@ -2,7 +2,9 @@
|
||||
|
||||
exports = module.exports = {
|
||||
initialize: initialize,
|
||||
uninitialize: uninitialize
|
||||
uninitialize: uninitialize,
|
||||
|
||||
accessTokenAuth: accessTokenAuth
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
@@ -23,22 +25,22 @@ var assert = require('assert'),
|
||||
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
|
||||
passport.serializeUser(function (user, callback) {
|
||||
callback(null, user.id);
|
||||
});
|
||||
|
||||
|
||||
passport.deserializeUser(function(userId, callback) {
|
||||
user.get(userId, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
|
||||
var md5 = crypto.createHash('md5').update(result.alternateEmail || result.email).digest('hex');
|
||||
result.gravatar = 'https://www.gravatar.com/avatar/' + md5 + '.jpg?s=24&d=mm';
|
||||
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
passport.use(new LocalStrategy(function (username, password, callback) {
|
||||
if (username.indexOf('@') === -1) {
|
||||
user.verifyWithUsername(username, password, function (error, result) {
|
||||
@@ -58,7 +60,7 @@ function initialize(callback) {
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
passport.use(new BasicStrategy(function (username, password, callback) {
|
||||
if (username.indexOf('cid-') === 0) {
|
||||
debug('BasicStrategy: detected client id %s instead of username:password', username);
|
||||
@@ -80,7 +82,7 @@ function initialize(callback) {
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
passport.use(new ClientPasswordStrategy(function (clientId, clientSecret, callback) {
|
||||
clients.get(clientId, function(error, client) {
|
||||
if (error && error.reason === ClientsError.NOT_FOUND) return callback(null, false);
|
||||
@@ -89,30 +91,35 @@ function initialize(callback) {
|
||||
return callback(null, client);
|
||||
});
|
||||
}));
|
||||
|
||||
passport.use(new BearerStrategy(function (accessToken, callback) {
|
||||
tokendb.get(accessToken, function (error, token) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
|
||||
if (error) return callback(error);
|
||||
|
||||
// scopes here can define what capabilities that token carries
|
||||
// passport put the 'info' object into req.authInfo, where we can further validate the scopes
|
||||
var info = { scope: token.scope };
|
||||
|
||||
user.get(token.identifier, function (error, user) {
|
||||
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, user, info);
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
passport.use(new BearerStrategy(accessTokenAuth));
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function uninitialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function accessTokenAuth(accessToken, callback) {
|
||||
assert.strictEqual(typeof accessToken, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
tokendb.get(accessToken, function (error, token) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
|
||||
if (error) return callback(error);
|
||||
|
||||
// scopes here can define what capabilities that token carries
|
||||
// passport put the 'info' object into req.authInfo, where we can further validate the scopes
|
||||
var info = { scope: token.scope };
|
||||
|
||||
user.get(token.identifier, function (error, user) {
|
||||
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, user, info);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+32
-42
@@ -6,7 +6,7 @@ var assert = require('assert'),
|
||||
safe = require('safetydance'),
|
||||
util = require('util');
|
||||
|
||||
var BACKUPS_FIELDS = [ 'id', 'creationTime', 'version', 'type', 'dependsOn', 'state', 'restoreConfigJson' ];
|
||||
var BACKUPS_FIELDS = [ 'id', 'creationTime', 'version', 'type', 'dependsOn', 'state', 'manifestJson', 'format' ];
|
||||
|
||||
exports = module.exports = {
|
||||
add: add,
|
||||
@@ -34,8 +34,8 @@ function postProcess(result) {
|
||||
|
||||
result.dependsOn = result.dependsOn ? result.dependsOn.split(',') : [ ];
|
||||
|
||||
result.restoreConfig = result.restoreConfigJson ? safe.JSON.parse(result.restoreConfigJson) : null;
|
||||
delete result.restoreConfigJson;
|
||||
result.manifest = result.manifestJson ? safe.JSON.parse(result.manifestJson) : null;
|
||||
delete result.manifestJson;
|
||||
}
|
||||
|
||||
function getByTypeAndStatePaged(type, state, page, perPage, callback) {
|
||||
@@ -47,12 +47,12 @@ function getByTypeAndStatePaged(type, state, page, perPage, callback) {
|
||||
|
||||
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE type = ? AND state = ? ORDER BY creationTime DESC LIMIT ?,?',
|
||||
[ type, state, (page-1)*perPage, perPage ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
results.forEach(function (result) { postProcess(result); });
|
||||
results.forEach(function (result) { postProcess(result); });
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
callback(null, results);
|
||||
});
|
||||
}
|
||||
|
||||
function getByTypePaged(type, page, perPage, callback) {
|
||||
@@ -63,12 +63,12 @@ function getByTypePaged(type, page, perPage, callback) {
|
||||
|
||||
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE type = ? ORDER BY creationTime DESC LIMIT ?,?',
|
||||
[ type, (page-1)*perPage, perPage ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
results.forEach(function (result) { postProcess(result); });
|
||||
results.forEach(function (result) { postProcess(result); });
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
callback(null, results);
|
||||
});
|
||||
}
|
||||
|
||||
function getByAppIdPaged(page, perPage, appId, callback) {
|
||||
@@ -80,12 +80,12 @@ function getByAppIdPaged(page, perPage, appId, callback) {
|
||||
// box versions (0.93.x and below) used to use appbackup_ prefix
|
||||
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE type = ? AND state = ? AND id LIKE ? ORDER BY creationTime DESC LIMIT ?,?',
|
||||
[ exports.BACKUP_TYPE_APP, exports.BACKUP_STATE_NORMAL, '%app%\\_' + appId + '\\_%', (page-1)*perPage, perPage ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
results.forEach(function (result) { postProcess(result); });
|
||||
results.forEach(function (result) { postProcess(result); });
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
callback(null, results);
|
||||
});
|
||||
}
|
||||
|
||||
function get(id, callback) {
|
||||
@@ -94,13 +94,13 @@ function get(id, callback) {
|
||||
|
||||
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE id = ? ORDER BY creationTime DESC',
|
||||
[ id ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
postProcess(result[0]);
|
||||
postProcess(result[0]);
|
||||
|
||||
callback(null, result[0]);
|
||||
});
|
||||
callback(null, result[0]);
|
||||
});
|
||||
}
|
||||
|
||||
function add(backup, callback) {
|
||||
@@ -109,20 +109,21 @@ function add(backup, callback) {
|
||||
assert.strictEqual(typeof backup.version, 'string');
|
||||
assert(backup.type === exports.BACKUP_TYPE_APP || backup.type === exports.BACKUP_TYPE_BOX);
|
||||
assert(util.isArray(backup.dependsOn));
|
||||
assert.strictEqual(typeof backup.restoreConfig, 'object');
|
||||
assert.strictEqual(typeof backup.manifest, 'object');
|
||||
assert.strictEqual(typeof backup.format, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var creationTime = backup.creationTime || new Date(); // allow tests to set the time
|
||||
var restoreConfig = backup.restoreConfig ? JSON.stringify(backup.restoreConfig) : '';
|
||||
var manifestJson = JSON.stringify(backup.manifest);
|
||||
|
||||
database.query('INSERT INTO backups (id, version, type, creationTime, state, dependsOn, restoreConfigJson) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||||
[ backup.id, backup.version, backup.type, creationTime, exports.BACKUP_STATE_NORMAL, backup.dependsOn.join(','), restoreConfig ],
|
||||
database.query('INSERT INTO backups (id, version, type, creationTime, state, dependsOn, manifestJson, format) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[ backup.id, backup.version, backup.type, creationTime, exports.BACKUP_STATE_NORMAL, backup.dependsOn.join(','), manifestJson, backup.format ],
|
||||
function (error) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function update(id, backup, callback) {
|
||||
@@ -158,19 +159,8 @@ function del(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
get(id, function (error, result) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback();
|
||||
if (error) return callback(error);
|
||||
|
||||
var whereClause = [ 'id=?' ], whereArgs = [ result.id ];
|
||||
result.dependsOn.forEach(function (id) {
|
||||
whereClause.push('id=?');
|
||||
whereArgs.push(id);
|
||||
});
|
||||
|
||||
database.query('DELETE FROM backups WHERE ' + whereClause.join(' OR '), whereArgs, function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
callback(null);
|
||||
});
|
||||
database.query('DELETE FROM backups WHERE id=?', [ id ], function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
+617
-192
File diff suppressed because it is too large
Load Diff
+15
-69
@@ -1,7 +1,12 @@
|
||||
#!/usr/bin/env node
|
||||
#!/bin/bash
|
||||
':' //# comment; exec /usr/bin/env node --max_old_space_size=300 "$0" "$@"
|
||||
|
||||
// to understand the above hack read http://sambal.org/2014/02/passing-options-node-shebang-line/
|
||||
|
||||
'use strict';
|
||||
|
||||
if (process.argv[2] === '--check') return console.log('OK');
|
||||
|
||||
require('supererror')({ splatchError: true });
|
||||
|
||||
// remove timestamp from debug() based output
|
||||
@@ -10,29 +15,11 @@ require('debug').formatArgs = function formatArgs(args) {
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
BackupsError = require('./backups.js').BackupsError,
|
||||
caas = require('./storage/caas.js'),
|
||||
backups = require('./backups.js'),
|
||||
database = require('./database.js'),
|
||||
debug = require('debug')('box:backuptask'),
|
||||
filesystem = require('./storage/filesystem.js'),
|
||||
noop = require('./storage/noop.js'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
s3 = require('./storage/s3.js'),
|
||||
safe = require('safetydance'),
|
||||
settings = require('./settings.js');
|
||||
|
||||
function api(provider) {
|
||||
switch (provider) {
|
||||
case 'caas': return caas;
|
||||
case 's3': return s3;
|
||||
case 'filesystem': return filesystem;
|
||||
case 'minio': return s3;
|
||||
case 'exoscale-sos': return s3;
|
||||
case 'noop': return noop;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
safe = require('safetydance');
|
||||
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -40,52 +27,12 @@ function initialize(callback) {
|
||||
database.initialize(callback);
|
||||
}
|
||||
|
||||
function backupApp(backupId, appId, callback) {
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('Start app backup with id %s for %s', backupId, appId);
|
||||
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
var backupMapping = [{
|
||||
source: path.join(paths.APPS_DATA_DIR, appId),
|
||||
destination: '.'
|
||||
}];
|
||||
|
||||
api(backupConfig.provider).backup(backupConfig, backupId, backupMapping, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function backupBox(backupId, callback) {
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('Start box backup with id %s', backupId);
|
||||
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
var backupMapping = [{
|
||||
source: paths.BOX_DATA_DIR,
|
||||
destination: 'box'
|
||||
}, {
|
||||
source: path.join(paths.PLATFORM_DATA_DIR, 'mail'),
|
||||
destination: 'mail'
|
||||
}];
|
||||
|
||||
api(backupConfig.provider).backup(backupConfig, backupId, backupMapping, callback);
|
||||
});
|
||||
}
|
||||
|
||||
// Main process starts here
|
||||
var backupId = process.argv[2];
|
||||
var appId = process.argv[3];
|
||||
var format = process.argv[3];
|
||||
var dataDir = process.argv[4];
|
||||
|
||||
if (appId) debug('Backuptask for the app %s with id %s', appId, backupId);
|
||||
else debug('Backuptask for the whole Cloudron with id %s', backupId);
|
||||
debug(`Backing up ${dataDir} to ${backupId}`);
|
||||
|
||||
process.on('SIGTERM', function () {
|
||||
process.exit(0);
|
||||
@@ -94,7 +41,9 @@ process.on('SIGTERM', function () {
|
||||
initialize(function (error) {
|
||||
if (error) throw error;
|
||||
|
||||
function resultHandler(error) {
|
||||
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, '');
|
||||
|
||||
backups.upload(backupId, format, dataDir, function resultHandler(error) {
|
||||
if (error) debug('completed with error', error);
|
||||
|
||||
debug('completed');
|
||||
@@ -104,8 +53,5 @@ initialize(function (error) {
|
||||
// https://nodejs.org/api/process.html are exit codes used by node. apps.js uses the value below
|
||||
// to check apptask crashes
|
||||
process.exit(error ? 50 : 0);
|
||||
}
|
||||
|
||||
if (appId) backupApp(backupId, appId, resultHandler);
|
||||
else backupBox(backupId, resultHandler);
|
||||
});
|
||||
});
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ var assert = require('assert'),
|
||||
|
||||
var CA_PROD = 'https://acme-v01.api.letsencrypt.org',
|
||||
CA_STAGING = 'https://acme-staging.api.letsencrypt.org',
|
||||
LE_AGREEMENT = 'https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf';
|
||||
LE_AGREEMENT = 'https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf';
|
||||
|
||||
exports = module.exports = {
|
||||
getCertificate: getCertificate,
|
||||
|
||||
+2
-2
@@ -177,7 +177,7 @@ function renewAll(auditSource, callback) {
|
||||
apps.getAll(function (error, allApps) {
|
||||
if (error) return callback(error);
|
||||
|
||||
allApps.push({ location: constants.ADMIN_LOCATION }); // inject fake webadmin app
|
||||
allApps.push({ location: config.adminLocation() }); // inject fake webadmin app
|
||||
|
||||
var expiringApps = [ ];
|
||||
for (var i = 0; i < allApps.length; i++) {
|
||||
@@ -239,7 +239,7 @@ function renewAll(auditSource, callback) {
|
||||
}
|
||||
|
||||
// reconfigure and reload nginx. this is required for the case where we got a renewed cert after fallback
|
||||
var configureFunc = app.location === constants.ADMIN_LOCATION ?
|
||||
var configureFunc = app.location === config.adminLocation() ?
|
||||
nginx.configureAdmin.bind(null, certFilePath, keyFilePath, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn())
|
||||
: nginx.configureApp.bind(null, app, certFilePath, keyFilePath);
|
||||
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ var appdb = require('./appdb.js'),
|
||||
hat = require('hat'),
|
||||
tokendb = require('./tokendb.js'),
|
||||
util = require('util'),
|
||||
uuid = require('node-uuid');
|
||||
uuid = require('uuid');
|
||||
|
||||
function ClientsError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
|
||||
+66
-78
@@ -12,7 +12,7 @@ exports = module.exports = {
|
||||
dnsSetup: dnsSetup,
|
||||
getLogs: getLogs,
|
||||
|
||||
sendHeartbeat: sendHeartbeat,
|
||||
sendCaasHeartbeat: sendCaasHeartbeat,
|
||||
|
||||
updateToLatest: updateToLatest,
|
||||
reboot: reboot,
|
||||
@@ -52,6 +52,7 @@ var appdb = require('./appdb.js'),
|
||||
safe = require('safetydance'),
|
||||
settings = require('./settings.js'),
|
||||
SettingsError = settings.SettingsError,
|
||||
settingsdb = require('./settingsdb.js'),
|
||||
shell = require('./shell.js'),
|
||||
spawn = require('child_process').spawn,
|
||||
split = require('split'),
|
||||
@@ -63,7 +64,6 @@ var appdb = require('./appdb.js'),
|
||||
updateChecker = require('./updatechecker.js'),
|
||||
user = require('./user.js'),
|
||||
UserError = user.UserError,
|
||||
user = require('./user.js'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
@@ -127,7 +127,6 @@ function initialize(callback) {
|
||||
async.series([
|
||||
certificates.initialize,
|
||||
settings.initialize,
|
||||
installAppBundle,
|
||||
configureDefaultServer,
|
||||
onDomainConfigured
|
||||
], function (error) {
|
||||
@@ -185,12 +184,18 @@ function dnsSetup(dnsConfig, domain, zoneName, callback) {
|
||||
config.setFqdn(domain); // set fqdn only after dns config is valid, otherwise cannot re-setup if we failed
|
||||
config.setZoneName(zoneName);
|
||||
|
||||
async.series([ // do not block
|
||||
onDomainConfigured,
|
||||
configureWebadmin
|
||||
], NOOP_CALLBACK);
|
||||
// upsert the temporary domain record in settings db
|
||||
// This can be removed after this release
|
||||
settingsdb.set('domain', JSON.stringify({ fqdn: domain, zoneName: zoneName }), function (error) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
callback();
|
||||
async.series([ // do not block
|
||||
onDomainConfigured,
|
||||
configureWebadmin
|
||||
], NOOP_CALLBACK);
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -236,24 +241,32 @@ function configureWebadmin(callback) {
|
||||
callback(error);
|
||||
}
|
||||
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return done(error);
|
||||
function configureNginx(error) {
|
||||
debug('configureNginx: dns update:%j', error);
|
||||
|
||||
addDnsRecords(ip, function (error) {
|
||||
certificates.ensureCertificate({ location: config.adminLocation() }, function (error, certFilePath, keyFilePath) {
|
||||
if (error) return done(error);
|
||||
|
||||
gWebadminStatus.tls = true;
|
||||
|
||||
nginx.configureAdmin(certFilePath, keyFilePath, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn(), done);
|
||||
});
|
||||
}
|
||||
|
||||
// update the DNS. configure nginx regardless of whether it succeeded so that
|
||||
// box is accessible even if dns creds are invalid
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return configureNginx(error);
|
||||
|
||||
addDnsRecords(ip, function (error) {
|
||||
if (error) return configureNginx(error);
|
||||
|
||||
subdomains.waitForDns(config.adminFqdn(), ip, 'A', { interval: 30000, times: 50000 }, function (error) {
|
||||
if (error) return done(error);
|
||||
if (error) return configureNginx(error);
|
||||
|
||||
gWebadminStatus.dns = true;
|
||||
|
||||
certificates.ensureCertificate({ location: constants.ADMIN_LOCATION }, function (error, certFilePath, keyFilePath) {
|
||||
if (error) return done(error);
|
||||
|
||||
gWebadminStatus.tls = true;
|
||||
|
||||
nginx.configureAdmin(certFilePath, keyFilePath, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn(), done);
|
||||
});
|
||||
configureNginx();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -410,9 +423,12 @@ function getConfig(callback) {
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
webServerOrigin: config.webServerOrigin(),
|
||||
fqdn: config.fqdn(),
|
||||
adminLocation: config.adminLocation(),
|
||||
adminFqdn: config.adminFqdn(),
|
||||
mailFqdn: config.mailFqdn(),
|
||||
version: config.version(),
|
||||
update: updateChecker.getUpdateInfo(),
|
||||
progress: progress.get(),
|
||||
progress: progress.getAll(),
|
||||
isCustomDomain: config.isCustomDomain(),
|
||||
isDemo: config.isDemo(),
|
||||
developerMode: developerMode,
|
||||
@@ -430,8 +446,8 @@ function getConfig(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function sendHeartbeat() {
|
||||
if (config.provider() !== 'caas') return;
|
||||
function sendCaasHeartbeat() {
|
||||
assert(config.provider() === 'caas', 'Heartbeat is only sent for managed cloudrons');
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/heartbeat';
|
||||
superagent.post(url).query({ token: config.token(), version: config.version() }).timeout(30 * 1000).end(function (error, result) {
|
||||
@@ -531,9 +547,9 @@ function addDnsRecords(ip, callback) {
|
||||
var dkimKey = readDkimPublicKeySync();
|
||||
if (!dkimKey) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, new Error('Failed to read dkim public key')));
|
||||
|
||||
var webadminRecord = { subdomain: constants.ADMIN_LOCATION, type: 'A', values: [ ip ] };
|
||||
var webadminRecord = { subdomain: config.adminLocation(), type: 'A', values: [ ip ] };
|
||||
// t=s limits the domainkey to this domain and not it's subdomains
|
||||
var dkimRecord = { subdomain: constants.DKIM_SELECTOR + '._domainkey', type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] };
|
||||
var dkimRecord = { subdomain: config.dkimSelector() + '._domainkey', type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] };
|
||||
|
||||
var records = [ ];
|
||||
if (config.isCustomDomain()) {
|
||||
@@ -568,7 +584,8 @@ function addDnsRecords(ip, callback) {
|
||||
});
|
||||
});
|
||||
}, function (error) {
|
||||
debug('addDnsRecords: done updating records with error:', error);
|
||||
if (error) debug('addDnsRecords: done updating records with error:', error);
|
||||
else debug('addDnsRecords: done');
|
||||
|
||||
callback(error);
|
||||
});
|
||||
@@ -662,19 +679,19 @@ function doUpgrade(boxUpdateInfo, callback) {
|
||||
if (error) return upgradeError(error);
|
||||
|
||||
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/upgrade')
|
||||
.query({ token: config.token() })
|
||||
.send({ version: boxUpdateInfo.version })
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return upgradeError(new Error('Network error making upgrade request: ' + error));
|
||||
if (result.statusCode !== 202) return upgradeError(new Error(util.format('Server not ready to upgrade. statusCode: %s body: %j', result.status, result.body)));
|
||||
.query({ token: config.token() })
|
||||
.send({ version: boxUpdateInfo.version })
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return upgradeError(new Error('Network error making upgrade request: ' + error));
|
||||
if (result.statusCode !== 202) return upgradeError(new Error(util.format('Server not ready to upgrade. statusCode: %s body: %j', result.status, result.body)));
|
||||
|
||||
progress.set(progress.UPDATE, 10, 'Updating base system');
|
||||
progress.set(progress.UPDATE, 10, 'Updating base system');
|
||||
|
||||
// no need to unlock since this is the last thing we ever do on this box
|
||||
callback();
|
||||
retire('upgrade');
|
||||
});
|
||||
// no need to unlock since this is the last thing we ever do on this box
|
||||
callback();
|
||||
retire('upgrade');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -698,6 +715,7 @@ function doUpdate(boxUpdateInfo, callback) {
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
webServerOrigin: config.webServerOrigin(),
|
||||
fqdn: config.fqdn(),
|
||||
adminLocation: config.adminLocation(),
|
||||
tlsCert: config.tlsCert(),
|
||||
tlsKey: config.tlsKey(),
|
||||
isCustomDomain: config.isCustomDomain(),
|
||||
@@ -729,36 +747,6 @@ function doUpdate(boxUpdateInfo, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function installAppBundle(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (fs.existsSync(paths.FIRST_RUN_FILE)) return callback();
|
||||
|
||||
var bundle = config.get('appBundle');
|
||||
debug('initialize: installing app bundle on first run: %j', bundle);
|
||||
|
||||
if (!bundle || bundle.length === 0) return callback();
|
||||
|
||||
async.eachSeries(bundle, function (appInfo, iteratorCallback) {
|
||||
debug('autoInstall: installing %s at %s', appInfo.appstoreId, appInfo.location);
|
||||
|
||||
var data = {
|
||||
appStoreId: appInfo.appstoreId,
|
||||
location: appInfo.location,
|
||||
portBindings: appInfo.portBindings || null,
|
||||
accessRestriction: appInfo.accessRestriction || null,
|
||||
};
|
||||
|
||||
apps.install(data, { userId: null, username: 'autoinstaller' }, iteratorCallback);
|
||||
}, function (error) {
|
||||
if (error) debug('autoInstallApps: ', error);
|
||||
|
||||
fs.writeFileSync(paths.FIRST_RUN_FILE, 'been there, done that', 'utf8');
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function checkDiskSpace(callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
@@ -836,20 +824,20 @@ function doMigrate(options, callback) {
|
||||
debug('migrate: domain: %s size %s region %s', options.domain, options.size, options.region);
|
||||
|
||||
superagent
|
||||
.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/migrate')
|
||||
.query({ token: config.token() })
|
||||
.send(options)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return unlock(error); // network error
|
||||
if (result.statusCode === 409) return unlock(new CloudronError(CloudronError.BAD_STATE));
|
||||
if (result.statusCode === 404) return unlock(new CloudronError(CloudronError.NOT_FOUND));
|
||||
if (result.statusCode !== 202) return unlock(new CloudronError(CloudronError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
|
||||
.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/migrate')
|
||||
.query({ token: config.token() })
|
||||
.send(options)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return unlock(error); // network error
|
||||
if (result.statusCode === 409) return unlock(new CloudronError(CloudronError.BAD_STATE));
|
||||
if (result.statusCode === 404) return unlock(new CloudronError(CloudronError.NOT_FOUND));
|
||||
if (result.statusCode !== 202) return unlock(new CloudronError(CloudronError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
|
||||
|
||||
progress.set(progress.MIGRATE, 10, 'Migrating');
|
||||
progress.set(progress.MIGRATE, 10, 'Migrating');
|
||||
|
||||
retire('migrate', _.pick(options, 'domain', 'size', 'region'));
|
||||
});
|
||||
retire('migrate', _.pick(options, 'domain', 'size', 'region'));
|
||||
});
|
||||
});
|
||||
|
||||
callback(null);
|
||||
|
||||
+74
-34
@@ -17,6 +17,7 @@ exports = module.exports = {
|
||||
apiServerOrigin: apiServerOrigin,
|
||||
webServerOrigin: webServerOrigin,
|
||||
fqdn: fqdn,
|
||||
zoneName: zoneName,
|
||||
setFqdn: setFqdn,
|
||||
token: token,
|
||||
version: version,
|
||||
@@ -28,11 +29,14 @@ exports = module.exports = {
|
||||
adminOrigin: adminOrigin,
|
||||
internalAdminOrigin: internalAdminOrigin,
|
||||
sysadminOrigin: sysadminOrigin, // caas routes
|
||||
adminLocation: adminLocation,
|
||||
adminFqdn: adminFqdn,
|
||||
mailLocation: mailLocation,
|
||||
mailFqdn: mailFqdn,
|
||||
appFqdn: appFqdn,
|
||||
zoneName: zoneName,
|
||||
setZoneName: setZoneName,
|
||||
hasIPv6: hasIPv6,
|
||||
dkimSelector: dkimSelector,
|
||||
|
||||
isDemo: isDemo,
|
||||
|
||||
@@ -44,13 +48,17 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
constants = require('./constants.js'),
|
||||
debug = require('debug')('box:config.js'),
|
||||
fs = require('fs'),
|
||||
path = require('path'),
|
||||
safe = require('safetydance'),
|
||||
tld = require('tldjs'),
|
||||
_ = require('underscore');
|
||||
|
||||
|
||||
// assert on unknown environment can't proceed
|
||||
assert(exports.CLOUDRON || exports.TEST, 'Unknown environment. This should not happen!');
|
||||
|
||||
var homeDir = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
|
||||
|
||||
var data = { };
|
||||
@@ -62,8 +70,25 @@ function baseDir() {
|
||||
|
||||
var cloudronConfigFileName = path.join(baseDir(), 'configs/cloudron.conf');
|
||||
|
||||
// only tests can run without a config file on disk, they use the defaults with runtime overrides
|
||||
if (exports.CLOUDRON) assert(fs.existsSync(cloudronConfigFileName), 'No cloudron.conf found, cannot proceed');
|
||||
|
||||
function saveSync() {
|
||||
fs.writeFileSync(cloudronConfigFileName, JSON.stringify(data, null, 4)); // functions are ignored by JSON.stringify
|
||||
// only save values we want to have in the cloudron.conf, see start.sh
|
||||
var conf = {
|
||||
version: data.version,
|
||||
token: data.token,
|
||||
apiServerOrigin: data.apiServerOrigin,
|
||||
webServerOrigin: data.webServerOrigin,
|
||||
fqdn: data.fqdn,
|
||||
zoneName: data.zoneName,
|
||||
adminLocation: data.adminLocation,
|
||||
isCustomDomain: data.isCustomDomain,
|
||||
provider: data.provider,
|
||||
isDemo: data.isDemo
|
||||
};
|
||||
|
||||
fs.writeFileSync(cloudronConfigFileName, JSON.stringify(conf, null, 4)); // functions are ignored by JSON.stringify
|
||||
}
|
||||
|
||||
function _reset(callback) {
|
||||
@@ -76,45 +101,41 @@ function _reset(callback) {
|
||||
|
||||
function initConfig() {
|
||||
// setup defaults
|
||||
data.fqdn = 'localhost';
|
||||
data.fqdn = '';
|
||||
data.zoneName = '';
|
||||
|
||||
data.adminLocation = 'my';
|
||||
data.port = 3000;
|
||||
data.token = null;
|
||||
data.version = null;
|
||||
data.isCustomDomain = true;
|
||||
data.apiServerOrigin = null;
|
||||
data.webServerOrigin = null;
|
||||
data.smtpPort = 2525; // // this value comes from mail container
|
||||
data.provider = 'caas';
|
||||
data.smtpPort = 2525; // this value comes from mail container
|
||||
data.sysadminPort = 3001;
|
||||
data.ldapPort = 3002;
|
||||
data.provider = 'caas';
|
||||
data.appBundle = [ ];
|
||||
|
||||
if (exports.CLOUDRON) {
|
||||
data.port = 3000;
|
||||
data.apiServerOrigin = null;
|
||||
data.database = null;
|
||||
} else if (exports.TEST) {
|
||||
// keep in sync with start.sh
|
||||
data.database = {
|
||||
hostname: '127.0.0.1',
|
||||
username: 'root',
|
||||
password: 'password',
|
||||
port: 3306,
|
||||
name: 'box'
|
||||
};
|
||||
|
||||
// overrides for local testings
|
||||
if (exports.TEST) {
|
||||
data.port = 5454;
|
||||
data.apiServerOrigin = 'http://localhost:6060'; // hock doesn't support https
|
||||
data.database = {
|
||||
hostname: 'localhost',
|
||||
username: 'root',
|
||||
password: '',
|
||||
port: 3306,
|
||||
name: 'boxtest'
|
||||
};
|
||||
data.token = 'APPSTORE_TOKEN';
|
||||
} else {
|
||||
assert(false, 'Unknown environment. This should not happen!');
|
||||
data.apiServerOrigin = 'http://localhost:6060'; // hock doesn't support https
|
||||
data.database.password = '';
|
||||
data.database.name = 'boxtest';
|
||||
}
|
||||
|
||||
if (safe.fs.existsSync(cloudronConfigFileName)) {
|
||||
var existingData = safe.JSON.parse(safe.fs.readFileSync(cloudronConfigFileName, 'utf8'));
|
||||
_.extend(data, existingData); // overwrite defaults with saved config
|
||||
return;
|
||||
}
|
||||
|
||||
saveSync();
|
||||
// overwrite defaults with saved config
|
||||
var existingData = safe.JSON.parse(safe.fs.readFileSync(cloudronConfigFileName, 'utf8'));
|
||||
_.extend(data, existingData);
|
||||
}
|
||||
|
||||
initConfig();
|
||||
@@ -175,16 +196,24 @@ function appFqdn(location) {
|
||||
return isCustomDomain() ? location + '.' + fqdn() : location + '-' + fqdn();
|
||||
}
|
||||
|
||||
function adminFqdn() {
|
||||
return appFqdn(constants.ADMIN_LOCATION);
|
||||
function mailLocation() {
|
||||
return get('adminLocation'); // not a typo! should be same as admin location until we figure out certificates
|
||||
}
|
||||
|
||||
function mailFqdn() {
|
||||
return appFqdn(constants.MAIL_LOCATION);
|
||||
return appFqdn(mailLocation());
|
||||
}
|
||||
|
||||
function adminLocation() {
|
||||
return get('adminLocation');
|
||||
}
|
||||
|
||||
function adminFqdn() {
|
||||
return appFqdn(adminLocation());
|
||||
}
|
||||
|
||||
function adminOrigin() {
|
||||
return 'https://' + appFqdn(constants.ADMIN_LOCATION);
|
||||
return 'https://' + appFqdn(adminLocation());
|
||||
}
|
||||
|
||||
function internalAdminOrigin() {
|
||||
@@ -232,3 +261,14 @@ function tlsKey() {
|
||||
var keyFile = path.join(baseDir(), 'configs/host.key');
|
||||
return safe.fs.readFileSync(keyFile, 'utf8');
|
||||
}
|
||||
|
||||
function hasIPv6() {
|
||||
const IPV6_PROC_FILE = '/proc/net/if_inet6';
|
||||
return fs.existsSync(IPV6_PROC_FILE);
|
||||
}
|
||||
|
||||
function dkimSelector() {
|
||||
var loc = adminLocation();
|
||||
return loc === 'my' ? 'cloudron' : `cloudron-${loc.replace(/\./g, '')}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
// default admin installation location. keep in sync with ADMIN_LOCATION in setup/start.sh and BOX_ADMIN_LOCATION in appstore constants.js
|
||||
exports = module.exports = {
|
||||
ADMIN_LOCATION: 'my',
|
||||
API_LOCATION: 'api', // this is unused but reserved for future use (#403)
|
||||
SMTP_LOCATION: 'smtp',
|
||||
IMAP_LOCATION: 'imap',
|
||||
MAIL_LOCATION: 'my', // not a typo! should be same as admin location until we figure out certificates
|
||||
POSTMAN_LOCATION: 'postman', // used in dovecot bounces
|
||||
|
||||
// These are combined into one array because users and groups become mailboxes
|
||||
@@ -36,8 +33,6 @@ exports = module.exports = {
|
||||
|
||||
DEMO_USERNAME: 'cloudron',
|
||||
|
||||
DKIM_SELECTOR: 'cloudron',
|
||||
|
||||
AUTOUPDATE_PATTERN_NEVER: 'never'
|
||||
};
|
||||
|
||||
|
||||
+19
-17
@@ -35,7 +35,7 @@ var gAliveJob = null, // send periodic stats
|
||||
gCleanupTokensJob = null,
|
||||
gDockerVolumeCleanerJob = null,
|
||||
gDynamicDNSJob = null,
|
||||
gHeartbeatJob = null, // for CaaS health check
|
||||
gCaasHeartbeatJob = null, // for CaaS health check
|
||||
gSchedulerSyncJob = null,
|
||||
gDigestEmailJob = null;
|
||||
|
||||
@@ -53,18 +53,20 @@ var AUDIT_SOURCE = { userId: null, username: 'cron' };
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
gHeartbeatJob = new CronJob({
|
||||
cronTime: '00 */1 * * * *', // every minute
|
||||
onTick: cloudron.sendHeartbeat,
|
||||
start: false
|
||||
});
|
||||
// hack: send the first heartbeat only after we are running for 60 seconds
|
||||
// required as we end up sending a heartbeat and then cloudron-setup reboots the server
|
||||
setTimeout(function () {
|
||||
if (!gHeartbeatJob) return; // already uninitalized
|
||||
gHeartbeatJob.start();
|
||||
cloudron.sendHeartbeat();
|
||||
}, 1000 * 60);
|
||||
if (config.provider() === 'caas') {
|
||||
gCaasHeartbeatJob = new CronJob({
|
||||
cronTime: '00 */1 * * * *', // every minute
|
||||
onTick: cloudron.sendCaasHeartbeat,
|
||||
start: false
|
||||
});
|
||||
// hack: send the first heartbeat only after we are running for 60 seconds
|
||||
// required as we end up sending a heartbeat and then cloudron-setup reboots the server
|
||||
setTimeout(function () {
|
||||
if (!gCaasHeartbeatJob) return; // already uninitalized
|
||||
gCaasHeartbeatJob.start();
|
||||
cloudron.sendCaasHeartbeat();
|
||||
}, 1000 * 60);
|
||||
}
|
||||
|
||||
var randomHourMinute = Math.floor(60*Math.random());
|
||||
gAliveJob = new CronJob({
|
||||
@@ -139,7 +141,7 @@ function recreateJobs(tz) {
|
||||
if (gCleanupBackupsJob) gCleanupBackupsJob.stop();
|
||||
gCleanupBackupsJob = new CronJob({
|
||||
cronTime: '00 45 */6 * * *', // every 6 hours. try not to overlap with ensureBackup job
|
||||
onTick: backups.cleanup,
|
||||
onTick: backups.cleanup.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
|
||||
start: true,
|
||||
timeZone: tz
|
||||
});
|
||||
@@ -208,7 +210,7 @@ function autoupdatePatternChanged(pattern) {
|
||||
}
|
||||
} else if (updateInfo.apps) {
|
||||
debug('Starting app update to %j', updateInfo.apps);
|
||||
apps.updateApps(updateInfo.apps, AUDIT_SOURCE, NOOP_CALLBACK);
|
||||
apps.autoupdateApps(updateInfo.apps, AUDIT_SOURCE, NOOP_CALLBACK);
|
||||
} else {
|
||||
debug('No auto updates available');
|
||||
}
|
||||
@@ -252,8 +254,8 @@ function uninitialize(callback) {
|
||||
if (gAppUpdateCheckerJob) gAppUpdateCheckerJob.stop();
|
||||
gAppUpdateCheckerJob = null;
|
||||
|
||||
if (gHeartbeatJob) gHeartbeatJob.stop();
|
||||
gHeartbeatJob = null;
|
||||
if (gCaasHeartbeatJob) gCaasHeartbeatJob.stop();
|
||||
gCaasHeartbeatJob = null;
|
||||
|
||||
if (gAliveJob) gAliveJob.stop();
|
||||
gAliveJob = null;
|
||||
|
||||
+1
-21
@@ -7,19 +7,15 @@ exports = module.exports = {
|
||||
|
||||
isEnabled: isEnabled,
|
||||
setEnabled: setEnabled,
|
||||
issueDeveloperToken: issueDeveloperToken,
|
||||
getNonApprovedApps: getNonApprovedApps
|
||||
issueDeveloperToken: issueDeveloperToken
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
clients = require('./clients.js'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
debug = require('debug')('box:developer'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
tokendb = require('./tokendb.js'),
|
||||
settings = require('./settings.js'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util');
|
||||
|
||||
function DeveloperError(reason, errorOrMessage) {
|
||||
@@ -84,19 +80,3 @@ function issueDeveloperToken(user, auditSource, callback) {
|
||||
callback(null, { token: token, expiresAt: new Date(expiresAt).toISOString() });
|
||||
});
|
||||
}
|
||||
|
||||
function getNonApprovedApps(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/apps';
|
||||
superagent.get(url).query({ token: config.token(), boxVersion: config.version() }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new DeveloperError(DeveloperError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode === 401 || result.statusCode === 403) {
|
||||
debug('Failed to list apps in development. Appstore token invalid or missing. Returning empty list.', result.body);
|
||||
return callback(null, []);
|
||||
}
|
||||
if (result.statusCode !== 200) return callback(new DeveloperError(DeveloperError.EXTERNAL_ERROR, util.format('App listing failed. %s %j', result.status, result.body)));
|
||||
|
||||
callback(null, result.body.apps || []);
|
||||
});
|
||||
}
|
||||
|
||||
+25
-18
@@ -33,31 +33,38 @@ function maybeSend(callback) {
|
||||
|
||||
var hasSubscription = result && result.plan.id !== 'free' && result.plan.id !== 'undecided';
|
||||
|
||||
eventlog.getByActionLastWeek(eventlog.ACTION_APP_UPDATE, function (error, appUpdates) {
|
||||
eventlog.getByCreationTime(new Date(new Date() - 7*86400000), function (error, events) {
|
||||
if (error) return callback(error);
|
||||
|
||||
eventlog.getByActionLastWeek(eventlog.ACTION_UPDATE, function (error, boxUpdates) {
|
||||
if (error) return callback(error);
|
||||
var appUpdates = events.filter(function (e) { return e.action === eventlog.ACTION_APP_UPDATE; }).map(function (e) { return e.data; });
|
||||
var boxUpdates = events.filter(function (e) { return e.action === eventlog.ACTION_UPDATE; }).map(function (e) { return e.data; });
|
||||
var certRenewals = events.filter(function (e) { return e.action === eventlog.ACTION_CERTIFICATE_RENEWAL; }).map(function (e) { return e.data; });
|
||||
var usersAdded = events.filter(function (e) { return e.action === eventlog.ACTION_USER_ADD; }).map(function (e) { return e.data; });
|
||||
var usersRemoved = events.filter(function (e) { return e.action === eventlog.ACTION_USER_REMOVE; }).map(function (e) { return e.data; });
|
||||
var finishedBackups = events.filter(function (e) { return e.action === eventlog.ACTION_BACKUP_FINISH && !e.errorMessage; }).map(function (e) { return e.data; });
|
||||
|
||||
var info = {
|
||||
hasSubscription: hasSubscription,
|
||||
if (error) return callback(error);
|
||||
|
||||
pendingAppUpdates: pendingAppUpdates,
|
||||
pendingBoxUpdate: updateInfo.box || null,
|
||||
var info = {
|
||||
hasSubscription: hasSubscription,
|
||||
|
||||
finishedAppUpdates: (appUpdates || []).map(function (e) { return e.data; }),
|
||||
finishedBoxUpdates: (boxUpdates || []).map(function (e) { return e.data; })
|
||||
};
|
||||
pendingAppUpdates: pendingAppUpdates,
|
||||
pendingBoxUpdate: updateInfo.box || null,
|
||||
|
||||
if (info.pendingAppUpdates.length || info.pendingBoxUpdate || info.finishedAppUpdates.length || info.finishedBoxUpdates.length) {
|
||||
debug('maybeSend: sending digest email', info);
|
||||
mailer.sendDigest(info);
|
||||
} else {
|
||||
debug('maybeSend: nothing happened, NOT sending digest email');
|
||||
}
|
||||
finishedAppUpdates: appUpdates,
|
||||
finishedBoxUpdates: boxUpdates,
|
||||
|
||||
callback();
|
||||
});
|
||||
certRenewals: certRenewals,
|
||||
finishedBackups: finishedBackups, // only the successful backups
|
||||
usersAdded: usersAdded,
|
||||
usersRemoved: usersRemoved // unused because we don't have username to work with
|
||||
};
|
||||
|
||||
// always send digest for backup failure notification
|
||||
debug('maybeSend: sending digest email', info);
|
||||
mailer.sendDigest(info);
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+3
-3
@@ -38,7 +38,7 @@ function add(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
.send(data)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (error && !error.response) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode === 400) return callback(new SubdomainError(SubdomainError.BAD_FIELD, result.body.message));
|
||||
if (result.statusCode === 420) return callback(new SubdomainError(SubdomainError.STILL_BUSY));
|
||||
if (result.statusCode !== 201) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
|
||||
@@ -63,7 +63,7 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
.query({ token: dnsConfig.token, type: type })
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (error && !error.response) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode !== 200) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
|
||||
|
||||
return callback(null, result.body.values);
|
||||
@@ -102,7 +102,7 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
.send(data)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (error && !error.response) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode === 400) return callback(new SubdomainError(SubdomainError.BAD_FIELD, result.body.message));
|
||||
if (result.statusCode === 420) return callback(new SubdomainError(SubdomainError.STILL_BUSY));
|
||||
if (result.statusCode === 404) return callback(new SubdomainError(SubdomainError.NOT_FOUND));
|
||||
|
||||
+12
-4
@@ -66,18 +66,18 @@ function getDNSRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, cal
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var fqdn = subdomain === '' ? zoneName : subdomain + '.' + zoneName;
|
||||
|
||||
superagent.get(CLOUDFLARE_ENDPOINT + '/zones/' + zoneId + '/dns_records')
|
||||
.set('X-Auth-Key',dnsConfig.token)
|
||||
.set('X-Auth-Email',dnsConfig.email)
|
||||
.query({ type: type, name: fqdn })
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
|
||||
|
||||
var fqdn = subdomain === '' ? zoneName : subdomain + '.' + zoneName;
|
||||
var tmp = result.body.result.filter(function (record) {
|
||||
return (record.type === type && record.name === fqdn);
|
||||
});
|
||||
var tmp = result.body.result;
|
||||
|
||||
return callback(null, tmp);
|
||||
});
|
||||
@@ -109,10 +109,18 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
var i = 0;
|
||||
|
||||
async.eachSeries(values, function (value, callback) {
|
||||
var priority = null;
|
||||
|
||||
if (type === 'MX') {
|
||||
priority = value.split(' ')[0];
|
||||
value = value.split(' ')[1];
|
||||
}
|
||||
|
||||
var data = {
|
||||
type: type,
|
||||
name: fqdn,
|
||||
content: value,
|
||||
priority: priority,
|
||||
ttl: 120 // 1 means "automatic" (meaning 300ms) and 120 is the lowest supported
|
||||
};
|
||||
|
||||
|
||||
+66
-49
@@ -10,9 +10,10 @@ exports = module.exports = {
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
constants = require('../constants.js'),
|
||||
config = require('../config.js'),
|
||||
debug = require('debug')('box:dns/digitalocean'),
|
||||
dns = require('dns'),
|
||||
safe = require('safetydance'),
|
||||
SubdomainError = require('../subdomains.js').SubdomainError,
|
||||
superagent = require('superagent'),
|
||||
util = require('util');
|
||||
@@ -30,22 +31,34 @@ function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
superagent.get(DIGITALOCEAN_ENDPOINT + '/v2/domains/' + zoneName + '/records')
|
||||
.set('Authorization', 'Bearer ' + dnsConfig.token)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (result.statusCode === 404) return callback(new SubdomainError(SubdomainError.NOT_FOUND, formatError(result)));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode !== 200) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, formatError(result)));
|
||||
var nextPage = null, matchingRecords = [];
|
||||
|
||||
var tmp = result.body.domain_records.filter(function (record) {
|
||||
return (record.type === type && record.name === subdomain);
|
||||
});
|
||||
async.doWhilst(function (iteratorDone) {
|
||||
var url = nextPage ? nextPage : DIGITALOCEAN_ENDPOINT + '/v2/domains/' + zoneName + '/records';
|
||||
|
||||
debug('getInternal: %j', tmp);
|
||||
superagent.get(url)
|
||||
.set('Authorization', 'Bearer ' + dnsConfig.token)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode === 404) return callback(new SubdomainError(SubdomainError.NOT_FOUND, formatError(result)));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode !== 200) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
return callback(null, tmp);
|
||||
matchingRecords = matchingRecords.concat(result.body.domain_records.filter(function (record) {
|
||||
return (record.type === type && record.name === subdomain);
|
||||
}));
|
||||
|
||||
nextPage = (result.body.links && result.body.links.pages) ? result.body.links.pages.next : null;
|
||||
|
||||
iteratorDone();
|
||||
});
|
||||
}, function () { return !!nextPage; }, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('getInternal: %j', matchingRecords);
|
||||
|
||||
return callback(null, matchingRecords);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -65,7 +78,7 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
// used to track available records to update instead of create
|
||||
var i = 0;
|
||||
var i = 0, recordIds = [];
|
||||
|
||||
async.eachSeries(values, function (value, callback) {
|
||||
var priority = null;
|
||||
@@ -85,38 +98,42 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
|
||||
if (i >= result.length) {
|
||||
superagent.post(DIGITALOCEAN_ENDPOINT + '/v2/domains/' + zoneName + '/records')
|
||||
.set('Authorization', 'Bearer ' + dnsConfig.token)
|
||||
.send(data)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode === 422) return callback(new SubdomainError(SubdomainError.BAD_FIELD, result.body.message));
|
||||
if (result.statusCode !== 201) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, formatError(result)));
|
||||
.set('Authorization', 'Bearer ' + dnsConfig.token)
|
||||
.send(data)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode === 422) return callback(new SubdomainError(SubdomainError.BAD_FIELD, result.body.message));
|
||||
if (result.statusCode !== 201) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
recordIds.push(safe.query(result.body, 'domain_record.id'));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
} else {
|
||||
superagent.put(DIGITALOCEAN_ENDPOINT + '/v2/domains/' + zoneName + '/records/' + result[i].id)
|
||||
.set('Authorization', 'Bearer ' + dnsConfig.token)
|
||||
.send(data)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
.set('Authorization', 'Bearer ' + dnsConfig.token)
|
||||
.send(data)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
// increment, as we have consumed the record
|
||||
++i;
|
||||
++i;
|
||||
|
||||
if (error && !error.response) return callback(error);
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode === 422) return callback(new SubdomainError(SubdomainError.BAD_FIELD, result.body.message));
|
||||
if (result.statusCode !== 200) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, formatError(result)));
|
||||
if (error && !error.response) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode === 422) return callback(new SubdomainError(SubdomainError.BAD_FIELD, result.body.message));
|
||||
if (result.statusCode !== 200) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
recordIds.push(safe.query(result.body, 'domain_record.id'));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
}, function (error) {
|
||||
}, function (error, id) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, 'unused');
|
||||
callback(null, '' + recordIds[0]); // DO ids are integers
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -166,18 +183,18 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
// FIXME we only handle the first one currently
|
||||
|
||||
superagent.del(DIGITALOCEAN_ENDPOINT + '/v2/domains/' + zoneName + '/records/' + tmp[0].id)
|
||||
.set('Authorization', 'Bearer ' + dnsConfig.token)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (result.statusCode === 404) return callback(null);
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode !== 204) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, formatError(result)));
|
||||
.set('Authorization', 'Bearer ' + dnsConfig.token)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode === 404) return callback(null);
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode !== 204) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
debug('del: done');
|
||||
debug('del: done');
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
return callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -204,7 +221,7 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Domain nameservers are not set to Digital Ocean'));
|
||||
}
|
||||
|
||||
const name = constants.ADMIN_LOCATION + (fqdn === zoneName ? '' : '.' + fqdn.slice(0, - zoneName.length - 1));
|
||||
const name = config.adminLocation() + (fqdn === zoneName ? '' : '.' + fqdn.slice(0, - zoneName.length - 1));
|
||||
|
||||
upsert(credentials, zoneName, name, 'A', [ ip ], function (error, changeId) {
|
||||
if (error) return callback(error);
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
upsert: upsert,
|
||||
get: get,
|
||||
del: del,
|
||||
waitForDns: require('./waitfordns.js'),
|
||||
verifyDnsConfig: verifyDnsConfig
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
config = require('../config.js'),
|
||||
debug = require('debug')('box:dns/gcdns'),
|
||||
dns = require('dns'),
|
||||
GCDNS = require('@google-cloud/dns'),
|
||||
SubdomainError = require('../subdomains.js').SubdomainError,
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
function getDnsCredentials(dnsConfig) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
|
||||
var config = {
|
||||
provider: dnsConfig.provider,
|
||||
projectId: dnsConfig.projectId,
|
||||
keyFilename: dnsConfig.keyFilename,
|
||||
email: dnsConfig.email
|
||||
};
|
||||
|
||||
if (dnsConfig.credentials) {
|
||||
config.credentials = {
|
||||
client_email: dnsConfig.credentials.client_email,
|
||||
private_key: dnsConfig.credentials.private_key
|
||||
};
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
function getZoneByName(dnsConfig, zoneName, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var gcdns = GCDNS(getDnsCredentials(dnsConfig));
|
||||
|
||||
gcdns.getZones(function (error, zones) {
|
||||
if (error && error.message === 'invalid_grant') return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, 'The key was probably revoked'));
|
||||
if (error && error.reason === 'No such domain') return callback(new SubdomainError(SubdomainError.NOT_FOUND, error.message));
|
||||
if (error && error.code === 403) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message));
|
||||
if (error && error.code === 404) return callback(new SubdomainError(SubdomainError.NOT_FOUND, error.message));
|
||||
if (error) {
|
||||
debug('gcdns.getZones', error);
|
||||
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error));
|
||||
}
|
||||
|
||||
var zone = zones.filter(function (zone) {
|
||||
return zone.metadata.dnsName.slice(0, -1) === zoneName; // the zone name contains a '.' at the end
|
||||
})[0];
|
||||
|
||||
if (!zone) return callback(new SubdomainError(SubdomainError.NOT_FOUND, 'no such zone'));
|
||||
|
||||
callback(null, zone); //zone.metadata ~= {name="", dnsName="", nameServers:[]}
|
||||
});
|
||||
}
|
||||
|
||||
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(util.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('add: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
|
||||
|
||||
getZoneByName(getDnsCredentials(dnsConfig), zoneName, function (error, zone) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var domain = (subdomain ? subdomain + '.' : '') + zoneName + '.';
|
||||
|
||||
zone.getRecords({ type: type, name: domain }, function (error, oldRecords) {
|
||||
if (error && error.code === 403) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message));
|
||||
if (error) {
|
||||
debug('upsert->zone.getRecords', error);
|
||||
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
|
||||
}
|
||||
|
||||
var newRecord = zone.record(type, {
|
||||
name: domain,
|
||||
data: values,
|
||||
ttl: 1
|
||||
});
|
||||
|
||||
zone.createChange({ delete: oldRecords, add: newRecord }, function(error, change) {
|
||||
if (error && error.code === 403) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message));
|
||||
if (error && error.code === 412) return callback(new SubdomainError(SubdomainError.STILL_BUSY, error.message));
|
||||
if (error) {
|
||||
debug('upsert->zone.createChange', error);
|
||||
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
|
||||
}
|
||||
|
||||
callback(null, change.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function get(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getZoneByName(getDnsCredentials(dnsConfig), zoneName, function (error, zone) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var params = {
|
||||
name: (subdomain ? subdomain + '.' : '') + zoneName + '.',
|
||||
type: type
|
||||
};
|
||||
|
||||
zone.getRecords(params, function (error, records) {
|
||||
if (error && error.code === 403) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message));
|
||||
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error));
|
||||
if (records.length === 0) return callback(null, [ ]);
|
||||
|
||||
return callback(null, records[0].data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(util.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getZoneByName(getDnsCredentials(dnsConfig), zoneName, function (error, zone) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var domain = (subdomain ? subdomain + '.' : '') + zoneName + '.';
|
||||
|
||||
zone.getRecords({ type: type, name: domain }, function(error, oldRecords) {
|
||||
if (error && error.code === 403) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message));
|
||||
if (error) {
|
||||
debug('del->zone.getRecords', error);
|
||||
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
|
||||
}
|
||||
|
||||
zone.deleteRecords(oldRecords, function (error, change) {
|
||||
if (error && error.code === 403) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message));
|
||||
if (error && error.code === 412) return callback(new SubdomainError(SubdomainError.STILL_BUSY, error.message));
|
||||
if (error) {
|
||||
debug('del->zone.createChange', error);
|
||||
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
|
||||
}
|
||||
|
||||
callback(null, change.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof fqdn, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var credentials = getDnsCredentials(dnsConfig);
|
||||
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
|
||||
|
||||
dns.resolveNs(zoneName, function (error, resolvedNS) {
|
||||
if (error && error.code === 'ENOTFOUND') return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
|
||||
if (error || !resolvedNS) return callback(new SubdomainError(SubdomainError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
|
||||
|
||||
getZoneByName(credentials, zoneName, function (error, zone) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var definedNS = zone.metadata.nameServers.sort().map(function(r) { return r.replace(/\.$/, ''); });
|
||||
if (!_.isEqual(definedNS, resolvedNS.sort())) {
|
||||
debug('verifyDnsConfig: %j and %j do not match', resolvedNS, definedNS);
|
||||
return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Domain nameservers are not set to Google Cloud DNS'));
|
||||
}
|
||||
|
||||
const name = config.adminLocation() + (fqdn === zoneName ? '' : '.' + fqdn.slice(0, - zoneName.length - 1));
|
||||
|
||||
upsert(credentials, zoneName, name, 'A', [ ip ], function (error, changeId) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: A record added with change id %s', changeId);
|
||||
|
||||
callback(null, credentials);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
+2
-2
@@ -10,7 +10,7 @@ exports = module.exports = {
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
constants = require('../constants.js'),
|
||||
config = require('../config.js'),
|
||||
debug = require('debug')('box:dns/manual'),
|
||||
dig = require('../dig.js'),
|
||||
dns = require('dns'),
|
||||
@@ -58,7 +58,7 @@ function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var adminDomain = constants.ADMIN_LOCATION + '.' + domain;
|
||||
var adminDomain = config.adminLocation() + '.' + domain;
|
||||
|
||||
dns.resolveNs(zoneName, function (error, nameservers) {
|
||||
if (error || !nameservers) return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Unable to get nameservers'));
|
||||
|
||||
+2
-2
@@ -13,7 +13,7 @@ exports = module.exports = {
|
||||
|
||||
var assert = require('assert'),
|
||||
AWS = require('aws-sdk'),
|
||||
constants = require('../constants.js'),
|
||||
config = require('../config.js'),
|
||||
debug = require('debug')('box:dns/route53'),
|
||||
dns = require('dns'),
|
||||
SubdomainError = require('../subdomains.js').SubdomainError,
|
||||
@@ -247,7 +247,7 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Domain nameservers are not set to Route53'));
|
||||
}
|
||||
|
||||
const name = constants.ADMIN_LOCATION + (fqdn === zoneName ? '' : '.' + fqdn.slice(0, - zoneName.length - 1));
|
||||
const name = config.adminLocation() + (fqdn === zoneName ? '' : '.' + fqdn.slice(0, - zoneName.length - 1));
|
||||
|
||||
upsert(credentials, zoneName, name, 'A', [ ip ], function (error, changeId) {
|
||||
if (error) return callback(error);
|
||||
|
||||
+32
-21
@@ -14,6 +14,7 @@ exports = module.exports = {
|
||||
deleteContainers: deleteContainers,
|
||||
createSubcontainer: createSubcontainer,
|
||||
getContainerIdByIp: getContainerIdByIp,
|
||||
inspect: inspect,
|
||||
execContainer: execContainer
|
||||
};
|
||||
|
||||
@@ -185,9 +186,9 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
'/run': {}
|
||||
},
|
||||
Labels: {
|
||||
"location": app.location,
|
||||
"appId": app.id,
|
||||
"isSubcontainer": String(!isAppContainer)
|
||||
'location': app.location,
|
||||
'appId': app.id,
|
||||
'isSubcontainer': String(!isAppContainer)
|
||||
},
|
||||
HostConfig: {
|
||||
Binds: addons.getBindsSync(app, app.manifest.addons),
|
||||
@@ -197,20 +198,28 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
PublishAllPorts: false,
|
||||
ReadonlyRootfs: app.debugMode ? !!app.debugMode.readonlyRootfs : true,
|
||||
RestartPolicy: {
|
||||
"Name": isAppContainer ? "always" : "no",
|
||||
"MaximumRetryCount": 0
|
||||
'Name': isAppContainer ? 'always' : 'no',
|
||||
'MaximumRetryCount': 0
|
||||
},
|
||||
CpuShares: 512, // relative to 1024 for system processes
|
||||
VolumesFrom: isAppContainer ? null : [ app.containerId + ":rw" ],
|
||||
VolumesFrom: isAppContainer ? null : [ app.containerId + ':rw' ],
|
||||
NetworkMode: 'cloudron',
|
||||
Dns: ['172.18.0.1'], // use internal dns
|
||||
DnsSearch: ['.'], // use internal dns
|
||||
SecurityOpt: enableSecurityOpt ? [ "apparmor=docker-cloudron-app" ] : null // profile available only on cloudron
|
||||
SecurityOpt: enableSecurityOpt ? [ 'apparmor=docker-cloudron-app' ] : null // profile available only on cloudron
|
||||
}
|
||||
};
|
||||
|
||||
var capabilities = manifest.capabilities || [];
|
||||
if (capabilities.includes('net_admin')) {
|
||||
containerOptions.HostConfig.CapAdd = [
|
||||
'NET_ADMIN'
|
||||
];
|
||||
}
|
||||
|
||||
containerOptions = _.extend(containerOptions, options);
|
||||
|
||||
debugApp(app, 'Creating container for %s with options %j', app.manifest.dockerImage, containerOptions);
|
||||
debugApp(app, 'Creating container for %s', app.manifest.dockerImage);
|
||||
|
||||
docker.createContainer(containerOptions, callback);
|
||||
});
|
||||
@@ -358,35 +367,37 @@ function getContainerIdByIp(ip, callback) {
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('get container by ip %s', ip);
|
||||
|
||||
var docker = exports.connection;
|
||||
|
||||
docker.listNetworks({}, function (error, result) {
|
||||
docker.getNetwork('cloudron').inspect(function (error, bridge) {
|
||||
if (error && error.statusCode === 404) return callback(new Error('Unable to find the cloudron network'));
|
||||
if (error) return callback(error);
|
||||
|
||||
var bridge;
|
||||
result.forEach(function (n) {
|
||||
if (n.Name === 'cloudron') bridge = n;
|
||||
});
|
||||
|
||||
if (!bridge) return callback(new Error('Unable to find the cloudron network'));
|
||||
|
||||
var containerId;
|
||||
for (var id in bridge.Containers) {
|
||||
if (bridge.Containers[id].IPv4Address.indexOf(ip) === 0) {
|
||||
if (bridge.Containers[id].IPv4Address.indexOf(ip + '/16') === 0) {
|
||||
containerId = id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!containerId) return callback(new Error('No container with that ip'));
|
||||
|
||||
debug('found container %s with ip %s', containerId, ip);
|
||||
|
||||
callback(null, containerId);
|
||||
});
|
||||
}
|
||||
|
||||
function inspect(containerId, callback) {
|
||||
assert.strictEqual(typeof containerId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var container = exports.connection.getContainer(containerId);
|
||||
|
||||
container.inspect(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function execContainer(containerId, cmd, options, callback) {
|
||||
assert.strictEqual(typeof containerId, 'string');
|
||||
assert(util.isArray(cmd));
|
||||
|
||||
+101
-2
@@ -3,6 +3,7 @@
|
||||
exports = module.exports = {
|
||||
verifyRelay: verifyRelay,
|
||||
getStatus: getStatus,
|
||||
checkRblStatus: checkRblStatus,
|
||||
|
||||
EmailError: EmailError
|
||||
};
|
||||
@@ -14,6 +15,7 @@ var assert = require('assert'),
|
||||
constants = require('./constants.js'),
|
||||
debug = require('debug')('box:email'),
|
||||
dig = require('./dig.js'),
|
||||
mailer = require('./mailer.js'),
|
||||
net = require('net'),
|
||||
nodemailer = require('nodemailer'),
|
||||
safe = require('safetydance'),
|
||||
@@ -23,6 +25,8 @@ var assert = require('assert'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
|
||||
|
||||
const digOptions = { server: '127.0.0.1', port: 53, timeout: 5000 };
|
||||
|
||||
function EmailError(reason, errorOrMessage) {
|
||||
@@ -128,7 +132,7 @@ function verifyRelay(relay, callback) {
|
||||
|
||||
function checkDkim(callback) {
|
||||
var dkim = {
|
||||
domain: constants.DKIM_SELECTOR + '._domainkey.' + config.fqdn(),
|
||||
domain: config.dkimSelector() + '._domainkey.' + config.fqdn(),
|
||||
type: 'TXT',
|
||||
expected: null,
|
||||
value: null,
|
||||
@@ -259,6 +263,100 @@ function checkPtr(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
// https://raw.githubusercontent.com/jawsome/node-dnsbl/master/list.json
|
||||
const RBL_LIST = [
|
||||
{
|
||||
"name": "Barracuda",
|
||||
"dns": "b.barracudacentral.org",
|
||||
"site": "http://www.barracudacentral.org/rbl/removal-request"
|
||||
},
|
||||
{
|
||||
"name": "SpamCop",
|
||||
"dns": "bl.spamcop.net",
|
||||
"site": "http://spamcop.net"
|
||||
},
|
||||
{
|
||||
"name": "Sorbs Aggregate Zone",
|
||||
"dns": "dnsbl.sorbs.net",
|
||||
"site": "http://dnsbl.sorbs.net/"
|
||||
},
|
||||
{
|
||||
"name": "Sorbs spam.dnsbl Zone",
|
||||
"dns": "spam.dnsbl.sorbs.net",
|
||||
"site": "http://sorbs.net"
|
||||
},
|
||||
{
|
||||
"name": "Composite Blocking List",
|
||||
"dns": "cbl.abuseat.org",
|
||||
"site": "http://www.abuseat.org"
|
||||
},
|
||||
{
|
||||
"name": "SpamHaus Zen",
|
||||
"dns": "zen.spamhaus.org",
|
||||
"site": "http://spamhaus.org"
|
||||
},
|
||||
{
|
||||
"name": "Multi SURBL",
|
||||
"dns": "multi.surbl.org",
|
||||
"site": "http://www.surbl.org"
|
||||
},
|
||||
{
|
||||
"name": "Spam Cannibal",
|
||||
"dns": "bl.spamcannibal.org",
|
||||
"site": "http://www.spamcannibal.org/cannibal.cgi"
|
||||
},
|
||||
{
|
||||
"name": "dnsbl.abuse.ch",
|
||||
"dns": "spam.abuse.ch",
|
||||
"site": "http://dnsbl.abuse.ch/"
|
||||
},
|
||||
{
|
||||
"name": "The Unsubscribe Blacklist(UBL)",
|
||||
"dns": "ubl.unsubscore.com ",
|
||||
"site": "http://www.lashback.com/blacklist/"
|
||||
},
|
||||
{
|
||||
"name": "UCEPROTECT Network",
|
||||
"dns": "dnsbl-1.uceprotect.net",
|
||||
"site": "http://www.uceprotect.net/en"
|
||||
}
|
||||
];
|
||||
|
||||
function checkRblStatus(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(error, ip);
|
||||
|
||||
var flippedIp = ip.split('.').reverse().join('.');
|
||||
|
||||
// https://tools.ietf.org/html/rfc5782
|
||||
async.map(RBL_LIST, function (rblServer, iteratorDone) {
|
||||
dig.resolve(flippedIp + '.' + rblServer.dns, 'A', digOptions, function (error, records) {
|
||||
if (error || !records) return iteratorDone(null, null); // not listed
|
||||
|
||||
debug('checkRblStatus: %s (ip: %s) is in the blacklist of %j', config.fqdn(), flippedIp, rblServer);
|
||||
|
||||
var result = _.extend({ }, rblServer);
|
||||
|
||||
dig.resolve(flippedIp + '.' + rblServer.dns, 'TXT', digOptions, function (error, txtRecords) {
|
||||
result.txtRecords = error || !txtRecords ? 'No txt record' : txtRecords;
|
||||
|
||||
debug('checkRblStatus: %s (error: %s) (txtRecords: %j)', config.fqdn(), error, txtRecords);
|
||||
|
||||
return iteratorDone(null, result);
|
||||
});
|
||||
});
|
||||
}, function (ignoredError, blacklistedServers) {
|
||||
blacklistedServers = blacklistedServers.filter(function(b) { return b !== null; });
|
||||
|
||||
debug('checkRblStatus: %s (ip: %s) servers: %j', config.fqdn(), ip, blacklistedServers);
|
||||
|
||||
return callback(null, { status: blacklistedServers.length === 0, ip: ip, servers: blacklistedServers });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getStatus(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -290,7 +388,8 @@ function getStatus(callback) {
|
||||
recordResult('dns.spf', checkSpf),
|
||||
recordResult('dns.dkim', checkDkim),
|
||||
recordResult('dns.ptr', checkPtr),
|
||||
recordResult('relay', checkOutboundPort25)
|
||||
recordResult('relay', checkOutboundPort25),
|
||||
recordResult('rbl', checkRblStatus)
|
||||
);
|
||||
} else {
|
||||
checks.push(recordResult('relay', checkSmtpRelay.bind(null, relay)));
|
||||
|
||||
+10
-9
@@ -6,7 +6,7 @@ exports = module.exports = {
|
||||
add: add,
|
||||
get: get,
|
||||
getAllPaged: getAllPaged,
|
||||
getByActionLastWeek: getByActionLastWeek,
|
||||
getByCreationTime: getByCreationTime,
|
||||
cleanup: cleanup,
|
||||
|
||||
// keep in sync with webadmin index.js filter and CLI tool
|
||||
@@ -20,6 +20,7 @@ exports = module.exports = {
|
||||
ACTION_APP_LOGIN: 'app.login',
|
||||
ACTION_BACKUP_FINISH: 'backup.finish',
|
||||
ACTION_BACKUP_START: 'backup.start',
|
||||
ACTION_BACKUP_CLEANUP: 'backup.cleanup',
|
||||
ACTION_CERTIFICATE_RENEWAL: 'certificate.renew',
|
||||
ACTION_CLI_MODE: 'settings.climode',
|
||||
ACTION_START: 'cloudron.start',
|
||||
@@ -35,7 +36,7 @@ var assert = require('assert'),
|
||||
debug = require('debug')('box:eventlog'),
|
||||
eventlogdb = require('./eventlogdb.js'),
|
||||
util = require('util'),
|
||||
uuid = require('node-uuid');
|
||||
uuid = require('uuid');
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
@@ -97,21 +98,21 @@ function getAllPaged(action, search, page, perPage, callback) {
|
||||
assert.strictEqual(typeof perPage, 'number');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
eventlogdb.getAllPaged(action, search, page, perPage, function (error, boxes) {
|
||||
eventlogdb.getAllPaged(action, search, page, perPage, function (error, events) {
|
||||
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, boxes);
|
||||
callback(null, events);
|
||||
});
|
||||
}
|
||||
|
||||
function getByActionLastWeek(action, callback) {
|
||||
assert(typeof action === 'string' || action === null);
|
||||
function getByCreationTime(creationTime, callback) {
|
||||
assert(util.isDate(creationTime));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
eventlogdb.getByActionLastWeek(action, function (error, boxes) {
|
||||
eventlogdb.getByCreationTime(creationTime, function (error, events) {
|
||||
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, boxes);
|
||||
callback(null, events);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -119,7 +120,7 @@ function cleanup(callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
var d = new Date();
|
||||
d.setDate(d.getDate() - 7); // 7 days ago
|
||||
d.setDate(d.getDate() - 10); // 10 days ago
|
||||
|
||||
// only cleanup high frequency events
|
||||
var actions = [
|
||||
|
||||
+5
-5
@@ -3,7 +3,7 @@
|
||||
exports = module.exports = {
|
||||
get: get,
|
||||
getAllPaged: getAllPaged,
|
||||
getByActionLastWeek: getByActionLastWeek,
|
||||
getByCreationTime: getByCreationTime,
|
||||
add: add,
|
||||
count: count,
|
||||
delByCreationTime: delByCreationTime,
|
||||
@@ -73,12 +73,12 @@ function getAllPaged(action, search, page, perPage, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getByActionLastWeek(action, callback) {
|
||||
assert(typeof action === 'string' || action === null);
|
||||
function getByCreationTime(creationTime, callback) {
|
||||
assert(util.isDate(creationTime));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var query = 'SELECT ' + EVENTLOGS_FIELDS + ' FROM eventlog WHERE action=? AND creationTime >= DATE_SUB(NOW(), INTERVAL 1 WEEK) ORDER BY creationTime DESC';
|
||||
database.query(query, [ action ], function (error, results) {
|
||||
var query = 'SELECT ' + EVENTLOGS_FIELDS + ' FROM eventlog WHERE creationTime >= ? ORDER BY creationTime DESC';
|
||||
database.query(query, [ creationTime ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
results.forEach(postProcess);
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ var assert = require('assert'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
groupdb = require('./groupdb.js'),
|
||||
util = require('util'),
|
||||
uuid = require('node-uuid');
|
||||
uuid = require('uuid');
|
||||
|
||||
// http://dustinsenos.com/articles/customErrorsInNode
|
||||
// http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
// Do not require anything here!
|
||||
|
||||
exports = module.exports = {
|
||||
// a major version makes all apps restore from backup
|
||||
// a major version makes all apps restore from backup. #451 must be fixed before we do this.
|
||||
// a minor version makes all apps re-configure themselves
|
||||
'version': '48.5.0',
|
||||
'version': '48.8.0',
|
||||
|
||||
'baseImages': [ 'cloudron/base:0.10.0' ],
|
||||
|
||||
@@ -15,10 +15,10 @@ exports = module.exports = {
|
||||
// This is because we upgrade using dumps instead of mysql_upgrade, pg_upgrade etc
|
||||
'images': {
|
||||
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:0.18.0' },
|
||||
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:0.17.0' },
|
||||
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:0.17.1' },
|
||||
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:0.13.0' },
|
||||
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:0.11.0' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:0.36.2' },
|
||||
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:0.11.0' }
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:0.39.1' },
|
||||
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:0.12.0' }
|
||||
}
|
||||
};
|
||||
|
||||
+2
-3
@@ -70,14 +70,13 @@ function cleanupTmpVolume(containerInfo, callback) {
|
||||
docker.getContainer(containerInfo.Id).exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true, Tty: false }, function (error, execContainer) {
|
||||
if (error) return callback(new Error('Failed to exec container : ' + error.message));
|
||||
|
||||
execContainer.start(function(err, stream) {
|
||||
execContainer.start({ hijack: true }, function (error, stream) {
|
||||
if (error) return callback(new Error('Failed to start exec container : ' + error.message));
|
||||
|
||||
stream.on('error', callback);
|
||||
stream.on('end', callback);
|
||||
|
||||
stream.setEncoding('utf8');
|
||||
stream.pipe(process.stdout);
|
||||
docker.modem.demuxStream(stream, process.stdout, process.stderr);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+123
-15
@@ -61,12 +61,74 @@ function getUsersWithAccessToApp(req, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
// helper function to deal with pagination
|
||||
function finalSend(results, req, res, next) {
|
||||
var min = 0;
|
||||
var max = results.length;
|
||||
var cookie = null;
|
||||
var pageSize = 0;
|
||||
|
||||
// check if this is a paging request, if so get the cookie for session info
|
||||
req.controls.forEach(function (control) {
|
||||
if (control.type === ldap.PagedResultsControl.OID) {
|
||||
pageSize = control.value.size;
|
||||
cookie = control.value.cookie;
|
||||
}
|
||||
});
|
||||
|
||||
function sendPagedResults(start, end) {
|
||||
start = (start < min) ? min : start;
|
||||
end = (end > max || end < min) ? max : end;
|
||||
var i;
|
||||
|
||||
for (i = start; i < end; i++) {
|
||||
res.send(results[i]);
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
if (cookie && Buffer.isBuffer(cookie)) {
|
||||
// we have pagination
|
||||
var first = min;
|
||||
if (cookie.length !== 0) {
|
||||
first = parseInt(cookie.toString(), 10);
|
||||
}
|
||||
var last = sendPagedResults(first, first + pageSize);
|
||||
|
||||
var resultCookie;
|
||||
if (last < max) {
|
||||
resultCookie = new Buffer(last.toString());
|
||||
} else {
|
||||
resultCookie = new Buffer('');
|
||||
}
|
||||
|
||||
res.controls.push(new ldap.PagedResultsControl({
|
||||
value: {
|
||||
size: pageSize, // correctness not required here
|
||||
cookie: resultCookie
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
// no pagination simply send all
|
||||
results.forEach(function (result) {
|
||||
res.send(result);
|
||||
});
|
||||
}
|
||||
|
||||
// all done
|
||||
res.end();
|
||||
next();
|
||||
}
|
||||
|
||||
function userSearch(req, res, next) {
|
||||
debug('user search: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id);
|
||||
|
||||
getUsersWithAccessToApp(req, function (error, result) {
|
||||
if (error) return next(error);
|
||||
|
||||
var results = [];
|
||||
|
||||
// send user objects
|
||||
result.forEach(function (entry) {
|
||||
// skip entries with empty username. Some apps like owncloud can't deal with this
|
||||
@@ -109,11 +171,11 @@ function userSearch(req, res, next) {
|
||||
if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString()));
|
||||
|
||||
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) {
|
||||
res.send(obj);
|
||||
results.push(obj);
|
||||
}
|
||||
});
|
||||
|
||||
res.end();
|
||||
finalSend(results, req, res, next);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -123,6 +185,8 @@ function groupSearch(req, res, next) {
|
||||
getUsersWithAccessToApp(req, function (error, result) {
|
||||
if (error) return next(error);
|
||||
|
||||
var results = [];
|
||||
|
||||
var groups = [{
|
||||
name: 'users',
|
||||
admin: false
|
||||
@@ -149,11 +213,43 @@ function groupSearch(req, res, next) {
|
||||
if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString()));
|
||||
|
||||
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) {
|
||||
res.send(obj);
|
||||
results.push(obj);
|
||||
}
|
||||
});
|
||||
|
||||
res.end();
|
||||
finalSend(results, req, res, next);
|
||||
});
|
||||
}
|
||||
|
||||
function groupUsersCompare(req, res, next) {
|
||||
debug('group users compare: dn %s, attribute %s, value %s (from %s)', req.dn.toString(), req.attribute, req.value, req.connection.ldap.id);
|
||||
|
||||
getUsersWithAccessToApp(req, function (error, result) {
|
||||
if (error) return next(error);
|
||||
|
||||
// we only support memberuid here, if we add new group attributes later add them here
|
||||
if (req.attribute === 'memberuid') {
|
||||
var found = result.find(function (u) { return u.id === req.value; });
|
||||
if (found) return res.end(true);
|
||||
}
|
||||
|
||||
res.end(false);
|
||||
});
|
||||
}
|
||||
|
||||
function groupAdminsCompare(req, res, next) {
|
||||
debug('group admins compare: dn %s, attribute %s, value %s (from %s)', req.dn.toString(), req.attribute, req.value, req.connection.ldap.id);
|
||||
|
||||
getUsersWithAccessToApp(req, function (error, result) {
|
||||
if (error) return next(error);
|
||||
|
||||
// we only support memberuid here, if we add new group attributes later add them here
|
||||
if (req.attribute === 'memberuid') {
|
||||
var found = result.find(function (u) { return u.id === req.value; });
|
||||
if (found && found.admin) return res.end(true);
|
||||
}
|
||||
|
||||
res.end(false);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -161,6 +257,7 @@ function mailboxSearch(req, res, next) {
|
||||
debug('mailbox search: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id);
|
||||
|
||||
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
var name = req.dn.rdns[0].attrs.cn.value.toLowerCase();
|
||||
// allow login via email
|
||||
var parts = name.split('@');
|
||||
@@ -188,9 +285,11 @@ function mailboxSearch(req, res, next) {
|
||||
var lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null);
|
||||
if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString()));
|
||||
|
||||
if (lowerCaseFilter.matches(obj.attributes)) res.send(obj);
|
||||
|
||||
res.end();
|
||||
if (lowerCaseFilter.matches(obj.attributes)) {
|
||||
finalSend([ obj ], req, res, next);
|
||||
} else {
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -198,6 +297,7 @@ function mailAliasSearch(req, res, next) {
|
||||
debug('mail alias get: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id);
|
||||
|
||||
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
mailboxdb.getAlias(req.dn.rdns[0].attrs.cn.value.toLowerCase(), function (error, alias) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
if (error) return next(new ldap.OperationsError(error.toString()));
|
||||
@@ -218,9 +318,11 @@ function mailAliasSearch(req, res, next) {
|
||||
var lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null);
|
||||
if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString()));
|
||||
|
||||
if (lowerCaseFilter.matches(obj.attributes)) res.send(obj);
|
||||
|
||||
res.end();
|
||||
if (lowerCaseFilter.matches(obj.attributes)) {
|
||||
finalSend([ obj ], req, res, next);
|
||||
} else {
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -228,6 +330,7 @@ function mailingListSearch(req, res, next) {
|
||||
debug('mailing list get: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id);
|
||||
|
||||
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
mailboxdb.getGroup(req.dn.rdns[0].attrs.cn.value.toLowerCase(), function (error, group) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
if (error) return next(new ldap.OperationsError(error.toString()));
|
||||
@@ -248,9 +351,11 @@ function mailingListSearch(req, res, next) {
|
||||
var lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null);
|
||||
if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString()));
|
||||
|
||||
if (lowerCaseFilter.matches(obj.attributes)) res.send(obj);
|
||||
|
||||
res.end();
|
||||
if (lowerCaseFilter.matches(obj.attributes)) {
|
||||
finalSend([ obj ], req, res, next);
|
||||
} else {
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -370,14 +475,17 @@ function start(callback) {
|
||||
gServer.bind('ou=recvmail,dc=cloudron', authenticateMailbox);
|
||||
gServer.bind('ou=sendmail,dc=cloudron', authenticateMailbox);
|
||||
|
||||
gServer.compare('cn=users,ou=groups,dc=cloudron', groupUsersCompare);
|
||||
gServer.compare('cn=admins,ou=groups,dc=cloudron', groupAdminsCompare);
|
||||
|
||||
// this is the bind for addons (after bind, they might search and authenticate)
|
||||
gServer.bind('ou=addons,dc=cloudron', function(req, res, next) {
|
||||
gServer.bind('ou=addons,dc=cloudron', function(req, res /*, next */) {
|
||||
debug('addons bind: %s', req.dn.toString()); // note: cn can be email or id
|
||||
res.end();
|
||||
});
|
||||
|
||||
// this is the bind for apps (after bind, they might search and authenticate user)
|
||||
gServer.bind('ou=apps,dc=cloudron', function(req, res, next) {
|
||||
gServer.bind('ou=apps,dc=cloudron', function(req, res /*, next */) {
|
||||
// TODO: validate password
|
||||
debug('application bind: %s', req.dn.toString());
|
||||
res.end();
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Generated by apptask for the /run mount
|
||||
|
||||
<%= volumePath %>/*.log <%= volumePath %>/*/*.log <%= volumePath %>/*/*/*.log {
|
||||
rotate 7
|
||||
daily
|
||||
compress
|
||||
maxsize=1M
|
||||
missingok
|
||||
delaycompress
|
||||
copytruncate
|
||||
}
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
Dear Cloudron Admin,
|
||||
|
||||
a new version <%= updateInfo.manifest.version %> of the app '<%= app.manifest.title %>' installed at <%= app.fqdn %> is available!
|
||||
|
||||
The app will update automatically tonight. Alternately, update immediately at <%= webadminUrl %>.
|
||||
A new version <%= updateInfo.manifest.version %> of the app '<%= app.manifest.title %>' installed at <%= app.fqdn %> is available!
|
||||
|
||||
Changes:
|
||||
<%= updateInfo.manifest.changelog %>
|
||||
|
||||
<% if (!hasSubscription) { -%>
|
||||
*Keep your Cloudron automatically up-to-date and secure by upgrading to a paid plan at* <%= webadminUrl %>/#/settings
|
||||
<% } -%>
|
||||
|
||||
Powered by https://cloudron.io
|
||||
|
||||
@@ -16,4 +17,35 @@ Sent at: <%= new Date().toUTCString() %>
|
||||
|
||||
<% } else { %>
|
||||
|
||||
<center>
|
||||
|
||||
<img src="<%= cloudronAvatarUrl %>" width="128px" height="128px"/>
|
||||
|
||||
<h3>Dear <%= cloudronName %> Admin,</h3>
|
||||
|
||||
<div style="width: 650px; text-align: left;">
|
||||
<p>
|
||||
A new version <%= updateInfo.manifest.version %> of the app '<%= app.manifest.title %>' installed at <%= app.fqdn %> is available!
|
||||
</p>
|
||||
|
||||
<h5>Changelog:</h5>
|
||||
<%- changelogHTML %>
|
||||
|
||||
<br/>
|
||||
|
||||
<% if (!hasSubscription) { %>
|
||||
<p>Keep your Cloudron automatically up-to-date and secure by upgrading to a <a href="<%= webadminUrl %>/#/settings">paid plan</a>.</p>
|
||||
<% } %>
|
||||
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
<div style="font-size: 10px; color: #333333; background: #ffffff;">
|
||||
Powered by <a href="https://cloudron.io">Cloudron</a>.
|
||||
</div>
|
||||
|
||||
</center>
|
||||
|
||||
<img src="https://analytics.cloudron.io/piwik.php?idsite=2&rec=1&e_c=CloudronEmail&e_a=update" style="border:0" alt="" />
|
||||
|
||||
<% } %>
|
||||
|
||||
@@ -4,15 +4,18 @@ Dear <%= cloudronName %> Admin,
|
||||
|
||||
Version <%= newBoxVersion %> for Cloudron <%= fqdn %> is now available!
|
||||
|
||||
Your Cloudron will update automatically tonight. Alternately, update immediately at <%= webadminUrl %>.
|
||||
|
||||
Changelog:
|
||||
<% for (var i = 0; i < changelog.length; i++) { %>
|
||||
* <%- changelog[i] %>
|
||||
<% } %>
|
||||
|
||||
Thank you,
|
||||
your Cloudron
|
||||
<% if (!hasSubscription) { -%>
|
||||
*Keep your Cloudron automatically up-to-date and secure by upgrading to a paid plan at* <%= webadminUrl %>/#/settings
|
||||
<% } -%>
|
||||
|
||||
Powered by https://cloudron.io
|
||||
|
||||
Sent at: <%= new Date().toUTCString() %>
|
||||
|
||||
<% } else { %>
|
||||
|
||||
@@ -27,11 +30,6 @@ your Cloudron
|
||||
Version <b><%= newBoxVersion %></b> for Cloudron <%= fqdn %> is now available!
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Your Cloudron will update automatically tonight.<br/>
|
||||
Alternately, update immediately <a href="<%= webadminUrl %>">here</a>.
|
||||
</p>
|
||||
|
||||
<h5>Changelog:</h5>
|
||||
<ul>
|
||||
<% for (var i = 0; i < changelogHTML.length; i++) { %>
|
||||
@@ -40,6 +38,11 @@ your Cloudron
|
||||
</ul>
|
||||
|
||||
<br/>
|
||||
|
||||
<% if (!hasSubscription) { %>
|
||||
<p>Keep your Cloudron automatically up-to-date and secure by upgrading to a <a href="<%= webadminUrl %>/#/settings">paid plan</a>.</p>
|
||||
<% } %>
|
||||
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,7 +2,19 @@
|
||||
|
||||
Dear <%= cloudronName %> Admin,
|
||||
|
||||
This is the weekly summary of activities on your Cloudron <%= fqdn %>.
|
||||
This is a summary of the activities on your Cloudron <%= fqdn %>.
|
||||
<% if (info.usersAdded.length) { -%>
|
||||
|
||||
The following users were added:
|
||||
<% for (var i = 0; i < info.usersAdded.length; i++) { -%>
|
||||
* <%- info.usersAdded[i].email %>
|
||||
<% }} -%>
|
||||
<% if (info.certRenewals.length) { -%>
|
||||
|
||||
The certificates of the following apps was renewed:
|
||||
<% for (var i = 0; i < info.certRenewals.length; i++) { -%>
|
||||
* <%- info.certRenewals[i].domain %> - <%- info.certRenewals[i].errorMessage || 'Success' %>
|
||||
<% }} -%>
|
||||
<% if (info.pendingBoxUpdate) { -%>
|
||||
|
||||
Cloudron v<%- info.pendingBoxUpdate.version %> is available:
|
||||
@@ -33,6 +45,14 @@ The following apps were updated:
|
||||
<% for (var j = 0; j < info.finishedAppUpdates[i].toManifest.changelog.trim().split('\n').length; j++) { -%>
|
||||
<%= info.finishedAppUpdates[i].toManifest.changelog.trim().split('\n')[j] %>
|
||||
<% }}} -%>
|
||||
<% if (info.finishedBackups.length) { -%>
|
||||
|
||||
Last successful backup: <%- info.finishedBackups[0].backupId || info.finishedBackups[0].filename %>
|
||||
<% } else { -%>
|
||||
|
||||
This Cloudron did **not** backup successfully in the last week!
|
||||
<% } -%>
|
||||
|
||||
<% if (!info.hasSubscription) { -%>
|
||||
|
||||
*Keep your Cloudron automatically up-to-date and secure by upgrading to a paid plan at* <%= webadminUrl %>/#/settings
|
||||
@@ -44,4 +64,115 @@ Sent at: <%= new Date().toUTCString() %>
|
||||
|
||||
<% } else { %>
|
||||
|
||||
<center>
|
||||
<div style="max-width: 800px; text-align: left; border: 1px solid lightgray; padding: 20px;">
|
||||
<center>
|
||||
<img src="<%= cloudronAvatarUrl %>" width="128px" height="128px"/>
|
||||
</center>
|
||||
|
||||
<br/>
|
||||
|
||||
<p>This is a summary of the activities on your Cloudron <a href="<%= webadminUrl %>"><%= cloudronName %></a> last week.</p>
|
||||
|
||||
<% if (info.usersAdded.length) { -%>
|
||||
<p><b>The following users were added:</b></p>
|
||||
<ul>
|
||||
<% for (var i = 0; i < info.usersAdded.length; i++) { %>
|
||||
<li><%- info.usersAdded[i].email %></li>
|
||||
<% } %>
|
||||
</ul>
|
||||
<% } %>
|
||||
|
||||
<% if (info.certRenewals.length) { -%>
|
||||
<p><b>The certificates of the following apps were renewed:</b></p>
|
||||
<ul>
|
||||
<% for (var i = 0; i < info.certRenewals.length; i++) { %>
|
||||
<li><%- info.certRenewals[i].domain %> - <%- info.certRenewals[i].errorMessage || 'Success' %></li>
|
||||
<% } %>
|
||||
</ul>
|
||||
<% } %>
|
||||
|
||||
<% if (info.pendingBoxUpdate) { -%>
|
||||
<p><b>Cloudron v<%- info.pendingBoxUpdate.version %> is available:</b></p>
|
||||
<ul>
|
||||
<% for (var i = 0; i < info.pendingBoxUpdate.changelog.length; i++) { %>
|
||||
<li><%- info.pendingBoxUpdate.changelog[i].replace(/^[\*,-] /, '') %></li>
|
||||
<% } %>
|
||||
</ul>
|
||||
<% } %>
|
||||
|
||||
<% if (info.pendingAppUpdates.length) { %>
|
||||
<p><b>Available app updates:</b></p>
|
||||
<ul>
|
||||
<% for (var i = 0; i < info.pendingAppUpdates.length; i++) { %>
|
||||
<li>
|
||||
<b><%= info.pendingAppUpdates[i].manifest.title %></b>
|
||||
<ul>
|
||||
<% for (var j = 0; j < info.pendingAppUpdates[i].manifest.changelog.trim().split('\n').length; j++) { %>
|
||||
<li><%= info.pendingAppUpdates[i].manifest.changelog.trim().split('\n')[j].replace(/^[\*,-] /, '') %></li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
<% } %>
|
||||
|
||||
<% if (info.finishedBoxUpdates.length) { %>
|
||||
<p><b>Your Cloudron was updated with the following releases:</b></p>
|
||||
<ul>
|
||||
<% for (var i = 0; i < info.finishedBoxUpdates.length; i++) { %>
|
||||
<li>
|
||||
<b><%= info.finishedBoxUpdates[i].boxUpdateInfo.version %></b>
|
||||
<ul>
|
||||
<% for (var j = 0; j < info.finishedBoxUpdates[i].boxUpdateInfo.changelog.length; j++) { %>
|
||||
<li><%= info.finishedBoxUpdates[i].boxUpdateInfo.changelog[j].replace(/^[\*,-] /, '') %></li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
<% } %>
|
||||
|
||||
<% if (info.finishedAppUpdates.length) { %>
|
||||
<p><b>The following apps were updated:</b></p>
|
||||
<ul>
|
||||
<% for (var i = 0; i < info.finishedAppUpdates.length; i++) { %>
|
||||
<li>
|
||||
<b><%= info.finishedAppUpdates[i].toManifest.title %></b> (package v<%= info.finishedAppUpdates[i].toManifest.version %>)
|
||||
<ul>
|
||||
<% for (var j = 0; j < info.finishedAppUpdates[i].toManifest.changelog.trim().split('\n').length; j++) { -%>
|
||||
<li><%= info.finishedAppUpdates[i].toManifest.changelog.trim().split('\n')[j].replace(/^[\*,-] /, '') %></li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
<% } %>
|
||||
|
||||
<% if (info.finishedBackups.length) { %>
|
||||
<p><b>Last successful backup : </b> <%= info.finishedBackups[0].backupId || info.finishedBackups[0].filename %> </p>
|
||||
<% } else { %>
|
||||
<p><b>This Cloudron did not backup successfully in the last week!</b></p>
|
||||
<% } %>
|
||||
|
||||
<br/>
|
||||
|
||||
<% if (!info.hasSubscription) { %>
|
||||
Keep your Cloudron automatically up-to-date and secure by upgrading to a <a href="<%= webadminUrl %>/#/settings">paid plan</a>.
|
||||
<% } %>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<center>
|
||||
<small>
|
||||
Powered by <a href="https://cloudron.io">Cloudron</a><br/>
|
||||
Sent on <%= new Date().toUTCString() %>
|
||||
</small>
|
||||
</center>
|
||||
</div>
|
||||
</center>
|
||||
|
||||
<img src="https://analytics.cloudron.io/piwik.php?idsite=2&rec=1&e_c=CloudronEmail&e_a=digest" style="border:0" alt="" />
|
||||
<% } %>
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<%if (format === 'text') { %>
|
||||
|
||||
New <%= type %> from <%= fqdn %>.
|
||||
|
||||
Sender: <%= user.email %>
|
||||
Sent at: <%= new Date().toUTCString() %>
|
||||
|
||||
Subject: <%= subject %>
|
||||
-----------------------------------------------------------
|
||||
<%= description %>
|
||||
|
||||
<% } else { %>
|
||||
|
||||
<% } %>
|
||||
@@ -0,0 +1,11 @@
|
||||
<%if (format === 'text') { %>
|
||||
|
||||
Test email from <%= fqdn %>,
|
||||
|
||||
If you can read this, your Cloudron email settings are good.
|
||||
|
||||
Sent at: <%= new Date().toUTCString() %>
|
||||
|
||||
<% } else { %>
|
||||
|
||||
<% } %>
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"format": "html",
|
||||
"webadminUrl": "https://my.cloudron.io",
|
||||
"fqdn": "my.cloudron.io",
|
||||
"cloudronName": "Smartserver",
|
||||
"cloudronAvatarUrl": "https://cloudron.io/img/logo.png",
|
||||
"info": {
|
||||
"pendingBoxUpdate": {
|
||||
"version": "1.3.7",
|
||||
"changelog": [
|
||||
"Feature one",
|
||||
"Feature two"
|
||||
]
|
||||
},
|
||||
"pendingAppUpdates": [{
|
||||
"manifest": {
|
||||
"title": "Wordpress",
|
||||
"version": "1.2.3",
|
||||
"changelog": "* This has changed\n * and that as well"
|
||||
}
|
||||
}],
|
||||
"finishedBoxUpdates": [{
|
||||
"boxUpdateInfo": {
|
||||
"version": "1.0.1",
|
||||
"changelog": [
|
||||
"Feature one",
|
||||
"Feature two"
|
||||
]
|
||||
}
|
||||
}, {
|
||||
"boxUpdateInfo": {
|
||||
"version": "1.0.2",
|
||||
"changelog": [
|
||||
"Feature one",
|
||||
"Feature two",
|
||||
"Feature three"
|
||||
]
|
||||
}
|
||||
}],
|
||||
"finishedAppUpdates": [{
|
||||
"toManifest": {
|
||||
"title": "Rocket.Chat",
|
||||
"version": "0.2.1",
|
||||
"changelog": "* This has changed\n * and that as well\n * some more"
|
||||
}
|
||||
}, {
|
||||
"toManifest": {
|
||||
"title": "Redmine",
|
||||
"version": "1.2.1",
|
||||
"changelog": "* This has changed\n * and that as well\n * some more"
|
||||
}
|
||||
}],
|
||||
"hasSubscription": false
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -134,7 +134,8 @@ function getGroup(name, callback) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
database.query('SELECT users.username FROM groupMembers INNER JOIN users ON groupMembers.userId = users.id WHERE groupMembers.groupId = ?', [ results[0].ownerId ], function (error, memberList) {
|
||||
// username can be null if the user has not signed up with the invite yet
|
||||
database.query('SELECT users.username FROM groupMembers INNER JOIN users ON groupMembers.userId = users.id WHERE groupMembers.groupId = ? AND users.username IS NOT NULL', [ results[0].ownerId ], function (error, memberList) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
results[0].members = memberList.map(function (m) { return m.username; });
|
||||
|
||||
+91
-51
@@ -23,12 +23,7 @@ exports = module.exports = {
|
||||
|
||||
certificateRenewalError: certificateRenewalError,
|
||||
|
||||
FEEDBACK_TYPE_FEEDBACK: 'feedback',
|
||||
FEEDBACK_TYPE_TICKET: 'ticket',
|
||||
FEEDBACK_TYPE_APP_MISSING: 'app_missing',
|
||||
FEEDBACK_TYPE_APP_ERROR: 'app_error',
|
||||
FEEDBACK_TYPE_UPGRADE_REQUEST: 'upgrade_request',
|
||||
sendFeedback: sendFeedback,
|
||||
sendTestMail: sendTestMail,
|
||||
|
||||
_getMailQueue: _getMailQueue,
|
||||
_clearMailQueue: _clearMailQueue
|
||||
@@ -149,7 +144,15 @@ function render(templateFile, params) {
|
||||
assert.strictEqual(typeof templateFile, 'string');
|
||||
assert.strictEqual(typeof params, 'object');
|
||||
|
||||
return ejs.render(safe.fs.readFileSync(path.join(MAIL_TEMPLATES_DIR, templateFile), 'utf8'), params);
|
||||
var content = null;
|
||||
|
||||
try {
|
||||
content = ejs.render(safe.fs.readFileSync(path.join(MAIL_TEMPLATES_DIR, templateFile), 'utf8'), params);
|
||||
} catch (e) {
|
||||
debug(`Error rendering ${templateFile}`, e);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
function getAdminEmails(callback) {
|
||||
@@ -159,6 +162,7 @@ function getAdminEmails(callback) {
|
||||
if (admins.length === 0) return callback(new Error('No admins on this cloudron')); // box not activated yet
|
||||
|
||||
var adminEmails = [ ];
|
||||
if (admins[0].alternateEmail) adminEmails.push(admins[0].alternateEmail);
|
||||
admins.forEach(function (admin) { adminEmails.push(admin.email); });
|
||||
|
||||
callback(null, adminEmails);
|
||||
@@ -232,7 +236,7 @@ function userAdded(user, inviteSent) {
|
||||
debug('Sending mail for userAdded %s including invite link', inviteSent ? 'not' : '');
|
||||
|
||||
getAdminEmails(function (error, adminEmails) {
|
||||
if (error) return console.log('Error getting admins', error);
|
||||
if (error) return debug('Error getting admins', error);
|
||||
|
||||
adminEmails = _.difference(adminEmails, [ user.email ]);
|
||||
|
||||
@@ -329,7 +333,7 @@ function appDied(app) {
|
||||
debug('Sending mail for app %s @ %s died', app.id, app.fqdn);
|
||||
|
||||
getAdminEmails(function (error, adminEmails) {
|
||||
if (error) return console.log('Error getting admins', error);
|
||||
if (error) return debug('Error getting admins', error);
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig().from,
|
||||
@@ -342,12 +346,13 @@ function appDied(app) {
|
||||
});
|
||||
}
|
||||
|
||||
function boxUpdateAvailable(newBoxVersion, changelog) {
|
||||
function boxUpdateAvailable(hasSubscription, newBoxVersion, changelog) {
|
||||
assert.strictEqual(typeof hasSubscription, 'boolean');
|
||||
assert.strictEqual(typeof newBoxVersion, 'string');
|
||||
assert(util.isArray(changelog));
|
||||
|
||||
getAdminEmails(function (error, adminEmails) {
|
||||
if (error) return console.log('Error getting admins', error);
|
||||
if (error) return debug('Error getting admins', error);
|
||||
|
||||
settings.getCloudronName(function (error, cloudronName) {
|
||||
if (error) {
|
||||
@@ -361,6 +366,7 @@ function boxUpdateAvailable(newBoxVersion, changelog) {
|
||||
fqdn: config.fqdn(),
|
||||
webadminUrl: config.adminOrigin(),
|
||||
newBoxVersion: newBoxVersion,
|
||||
hasSubscription: hasSubscription,
|
||||
changelog: changelog,
|
||||
changelogHTML: changelog.map(function (e) { return converter.makeHtml(e); }),
|
||||
cloudronName: cloudronName,
|
||||
@@ -373,7 +379,7 @@ function boxUpdateAvailable(newBoxVersion, changelog) {
|
||||
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
|
||||
templateDataHTML.format = 'html';
|
||||
|
||||
var mailOptions = {
|
||||
var mailOptions = {
|
||||
from: mailConfig().from,
|
||||
to: adminEmails.join(', '),
|
||||
subject: util.format('%s has a new update available', config.fqdn()),
|
||||
@@ -386,29 +392,13 @@ function boxUpdateAvailable(newBoxVersion, changelog) {
|
||||
});
|
||||
}
|
||||
|
||||
function appUpdateAvailable(app, updateInfo) {
|
||||
function appUpdateAvailable(app, hasSubscription, info) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof updateInfo, 'object');
|
||||
|
||||
getAdminEmails(function (error, adminEmails) {
|
||||
if (error) return console.log('Error getting admins', error);
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig().from,
|
||||
to: adminEmails.join(', '),
|
||||
subject: util.format('[%s] Update available for %s', config.fqdn(), app.fqdn),
|
||||
text: render('app_update_available.ejs', { fqdn: config.fqdn(), webadminUrl: config.adminOrigin(), app: app, updateInfo: updateInfo, format: 'text' })
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
});
|
||||
}
|
||||
|
||||
function sendDigest(info) {
|
||||
assert.strictEqual(typeof hasSubscription, 'boolean');
|
||||
assert.strictEqual(typeof info, 'object');
|
||||
|
||||
getAdminEmails(function (error, adminEmails) {
|
||||
if (error) return console.log('Error getting admins', error);
|
||||
if (error) return debug('Error getting admins', error);
|
||||
|
||||
settings.getCloudronName(function (error, cloudronName) {
|
||||
if (error) {
|
||||
@@ -416,11 +406,70 @@ function sendDigest(info) {
|
||||
cloudronName = 'Cloudron';
|
||||
}
|
||||
|
||||
var mailOptions = {
|
||||
var converter = new showdown.Converter();
|
||||
|
||||
var templateData = {
|
||||
fqdn: config.fqdn(),
|
||||
webadminUrl: config.adminOrigin(),
|
||||
hasSubscription: hasSubscription,
|
||||
app: app,
|
||||
updateInfo: info,
|
||||
changelogHTML: converter.makeHtml(info.manifest.changelog),
|
||||
cloudronName: cloudronName,
|
||||
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
|
||||
};
|
||||
|
||||
var templateDataText = JSON.parse(JSON.stringify(templateData));
|
||||
templateDataText.format = 'text';
|
||||
|
||||
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
|
||||
templateDataHTML.format = 'html';
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig().from,
|
||||
to: adminEmails.join(', '),
|
||||
subject: util.format('App %s has a new update available', app.fqdn),
|
||||
text: render('app_update_available.ejs', templateDataText),
|
||||
html: render('app_update_available.ejs', templateDataHTML)
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function sendDigest(info) {
|
||||
assert.strictEqual(typeof info, 'object');
|
||||
|
||||
getAdminEmails(function (error, adminEmails) {
|
||||
if (error) return debug('Error getting admins', error);
|
||||
|
||||
settings.getCloudronName(function (error, cloudronName) {
|
||||
if (error) {
|
||||
debug(error);
|
||||
cloudronName = 'Cloudron';
|
||||
}
|
||||
|
||||
var templateData = {
|
||||
fqdn: config.fqdn(),
|
||||
webadminUrl: config.adminOrigin(),
|
||||
cloudronName: cloudronName,
|
||||
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar',
|
||||
info: info
|
||||
};
|
||||
|
||||
var templateDataText = JSON.parse(JSON.stringify(templateData));
|
||||
templateDataText.format = 'text';
|
||||
|
||||
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
|
||||
templateDataHTML.format = 'html';
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig().from,
|
||||
to: adminEmails.join(', '),
|
||||
subject: util.format('[%s] Cloudron - Weekly activity digest', config.fqdn()),
|
||||
text: render('digest.ejs', { fqdn: config.fqdn(), webadminUrl: config.adminOrigin(), cloudronName: cloudronName, info: info, format: 'text' })
|
||||
text: render('digest.ejs', templateDataText),
|
||||
html: render('digest.ejs', templateDataHTML)
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
@@ -432,7 +481,7 @@ function outOfDiskSpace(message) {
|
||||
assert.strictEqual(typeof message, 'string');
|
||||
|
||||
getAdminEmails(function (error, adminEmails) {
|
||||
if (error) return console.log('Error getting admins', error);
|
||||
if (error) return debug('Error getting admins', error);
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig().from,
|
||||
@@ -449,7 +498,7 @@ function backupFailed(error) {
|
||||
var message = splatchError(error);
|
||||
|
||||
getAdminEmails(function (error, adminEmails) {
|
||||
if (error) return console.log('Error getting admins', error);
|
||||
if (error) return debug('Error getting admins', error);
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig().from,
|
||||
@@ -467,7 +516,7 @@ function certificateRenewalError(domain, message) {
|
||||
assert.strictEqual(typeof message, 'string');
|
||||
|
||||
getAdminEmails(function (error, adminEmails) {
|
||||
if (error) return console.log('Error getting admins', error);
|
||||
if (error) return debug('Error getting admins', error);
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig().from,
|
||||
@@ -485,7 +534,7 @@ function oomEvent(program, context) {
|
||||
assert.strictEqual(typeof context, 'string');
|
||||
|
||||
getAdminEmails(function (error, adminEmails) {
|
||||
if (error) return console.log('Error getting admins', error);
|
||||
if (error) return debug('Error getting admins', error);
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig().from,
|
||||
@@ -517,23 +566,14 @@ function unexpectedExit(program, context, callback) {
|
||||
sendMails([ mailOptions ], callback);
|
||||
}
|
||||
|
||||
function sendFeedback(user, type, subject, description) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof subject, 'string');
|
||||
assert.strictEqual(typeof description, 'string');
|
||||
|
||||
assert(type === exports.FEEDBACK_TYPE_TICKET ||
|
||||
type === exports.FEEDBACK_TYPE_FEEDBACK ||
|
||||
type === exports.FEEDBACK_TYPE_APP_MISSING ||
|
||||
type === exports.FEEDBACK_TYPE_UPGRADE_REQUEST ||
|
||||
type === exports.FEEDBACK_TYPE_APP_ERROR);
|
||||
function sendTestMail(email) {
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig().from,
|
||||
to: 'support@cloudron.io',
|
||||
subject: util.format('[%s] %s - %s', type, config.fqdn(), subject),
|
||||
text: render('feedback.ejs', { fqdn: config.fqdn(), type: type, user: user, subject: subject, description: description, format: 'text'})
|
||||
to: email,
|
||||
subject: util.format('Test Email from %s', config.fqdn()),
|
||||
text: render('test.ejs', { fqdn: config.fqdn(), format: 'text'})
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
|
||||
@@ -32,6 +32,7 @@ function configureAdmin(certFilePath, keyFilePath, configFileName, vhost, callba
|
||||
sourceDir: path.resolve(__dirname, '..'),
|
||||
adminOrigin: config.adminOrigin(),
|
||||
vhost: vhost, // if vhost is empty it will become the default_server
|
||||
hasIPv6: config.hasIPv6(),
|
||||
endpoint: 'admin',
|
||||
certFilePath: certFilePath,
|
||||
keyFilePath: keyFilePath,
|
||||
@@ -60,6 +61,7 @@ function configureApp(app, certFilePath, keyFilePath, callback) {
|
||||
sourceDir: sourceDir,
|
||||
adminOrigin: config.adminOrigin(),
|
||||
vhost: vhost,
|
||||
hasIPv6: config.hasIPv6(),
|
||||
port: app.httpPort,
|
||||
endpoint: endpoint,
|
||||
certFilePath: certFilePath,
|
||||
|
||||
@@ -15,64 +15,67 @@ app.controller('Controller', ['$scope', function ($scope) {
|
||||
|
||||
</script>
|
||||
|
||||
<center>
|
||||
<div class="layout-content">
|
||||
|
||||
<center>
|
||||
<br/>
|
||||
<h4>Hello <%= (user && user.email) ? user.email : '' %>, welcome to <%= cloudronName %>.</h4>
|
||||
<h2>Setup your account and password.</h2>
|
||||
</center>
|
||||
</center>
|
||||
|
||||
<div class="container" ng-app="Application" ng-controller="Controller">
|
||||
<div class="container" ng-app="Application" ng-controller="Controller">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<form action="/api/v1/session/account/setup" method="post" name="setupForm" autocomplete="off" role="form" novalidate>
|
||||
<input type="password" style="display: none;">
|
||||
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
|
||||
<input type="hidden" name="resetToken" value="<%= resetToken %>"/>
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<form action="/api/v1/session/account/setup" method="post" name="setupForm" autocomplete="off" role="form" novalidate>
|
||||
<input type="password" style="display: none;">
|
||||
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
|
||||
<input type="hidden" name="resetToken" value="<%= resetToken %>"/>
|
||||
|
||||
<center><p class="has-error"><%= error %></p></center>
|
||||
<center><p class="has-error"><%= error %></p></center>
|
||||
|
||||
<% if (user && user.username) { %>
|
||||
<div class="form-group"">
|
||||
<label class="control-label">Username</label>
|
||||
<input type="text" class="form-control" ng-model="username" name="username" readonly required>
|
||||
</div>
|
||||
<div class="form-group"">
|
||||
<label class="control-label">Username</label>
|
||||
<input type="text" class="form-control" ng-model="username" name="username" readonly required>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="form-group" ng-class="{ 'has-error': (setupForm.username.$dirty && setupForm.username.$invalid) }">
|
||||
<label class="control-label">Username</label>
|
||||
<div class="control-label" ng-show="setupForm.username.$dirty && setupForm.username.$invalid">
|
||||
<small ng-show="setupForm.username.$error.minlength">The username is too short</small>
|
||||
<small ng-show="setupForm.username.$error.maxlength">The username is too long</small>
|
||||
<small ng-show="setupForm.username.$dirty && setupForm.username.$invalid">Not a valid username</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="username" name="username" required autofocus>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (setupForm.username.$dirty && setupForm.username.$invalid) }">
|
||||
<label class="control-label">Username</label>
|
||||
<div class="control-label" ng-show="setupForm.username.$dirty && setupForm.username.$invalid">
|
||||
<small ng-show="setupForm.username.$error.minlength">The username is too short</small>
|
||||
<small ng-show="setupForm.username.$error.maxlength">The username is too long</small>
|
||||
<small ng-show="setupForm.username.$dirty && setupForm.username.$invalid">Not a valid username</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="username" name="username" required autofocus>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">Display Name</label>
|
||||
<input type="displayName" class="form-control" ng-model="displayName" name="displayName" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">Display Name</label>
|
||||
<input type="displayName" class="form-control" ng-model="displayName" name="displayName" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (setupForm.password.$dirty && setupForm.password.$invalid) }">
|
||||
<label class="control-label">New Password</label>
|
||||
<div class="control-label" ng-show="setupForm.password.$dirty && setupForm.password.$invalid">
|
||||
<small ng-show="setupForm.password.$dirty && setupForm.password.$invalid">Password must be 8-30 character with at least one uppercase, one numeric and one special character</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="password" name="password" ng-pattern="/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/" required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (setupForm.password.$dirty && setupForm.password.$invalid) }">
|
||||
<label class="control-label">New Password</label>
|
||||
<div class="control-label" ng-show="setupForm.password.$dirty && setupForm.password.$invalid">
|
||||
<small ng-show="setupForm.password.$dirty && setupForm.password.$invalid">Password must be 8-30 character with at least one uppercase, one numeric and one special character</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="password" name="password" ng-pattern="/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)) }">
|
||||
<label class="control-label">Repeat Password</label>
|
||||
<div class="control-label" ng-show="setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)">
|
||||
<small ng-show="setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)">Passwords don't match</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="passwordRepeat" name="passwordRepeat" required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)) }">
|
||||
<label class="control-label">Repeat Password</label>
|
||||
<div class="control-label" ng-show="setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)">
|
||||
<small ng-show="setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)">Passwords don't match</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="passwordRepeat" name="passwordRepeat" required>
|
||||
</div>
|
||||
|
||||
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Create" ng-disabled="setupForm.$invalid || password !== passwordRepeat"/>
|
||||
</form>
|
||||
</div>
|
||||
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Create" ng-disabled="setupForm.$invalid || password !== passwordRepeat"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% include footer %>
|
||||
|
||||
+14
-13
@@ -2,25 +2,26 @@
|
||||
|
||||
<!-- error tester -->
|
||||
|
||||
<br/>
|
||||
<div class="layout-content">
|
||||
|
||||
<div class="container">
|
||||
<div class="container" style="margin-top: 50px;">
|
||||
<div class="row">
|
||||
<div class="col-md-2"></div>
|
||||
<div class="col-md-8">
|
||||
<div class="alert alert-danger">
|
||||
<%- message %>
|
||||
</div>
|
||||
<div class="col-md-2"></div>
|
||||
<div class="col-md-8">
|
||||
<div class="alert alert-danger">
|
||||
<%- message %>
|
||||
</div>
|
||||
<div class="col-md-2"></div>
|
||||
</div>
|
||||
<div class="col-md-2"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-2"></div>
|
||||
<div class="col-md-8 text-center">
|
||||
<a href="<%- adminOrigin %>">Back</a>
|
||||
</div>
|
||||
<div class="col-md-2"></div>
|
||||
<div class="col-md-2"></div>
|
||||
<div class="col-md-8 text-center">
|
||||
<a href="<%- adminOrigin %>">Back</a>
|
||||
</div>
|
||||
<div class="col-md-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% include footer %>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
|
||||
<footer class="text-center">
|
||||
<span class="text-muted">© 2017 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
|
||||
<span class="text-muted"><a href="https://twitter.com/cloudron_io" target="_blank">Twitter <i class="fa fa-twitter"></i></a></span>
|
||||
<span class="text-muted"><a href="https://chat.cloudron.io" target="_blank">Chat <i class="fa fa-comments"></i></a></span>
|
||||
<span class="text-muted">© 2017 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
|
||||
<span class="text-muted"><a href="https://twitter.com/cloudron_io" target="_blank">Twitter <i class="fa fa-twitter"></i></a></span>
|
||||
<span class="text-muted"><a href="https://chat.cloudron.io" target="_blank">Chat <i class="fa fa-comments"></i></a></span>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -27,14 +27,15 @@
|
||||
|
||||
</head>
|
||||
|
||||
<body class="oauth">
|
||||
<body>
|
||||
|
||||
<div class="layout-root">
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar navbar-default navbar-static-top shadow" role="navigation" style="margin-bottom: 0">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<a href="/" class="navbar-brand navbar-brand-icon"><img src="/api/v1/cloudron/avatar?<%= Math.random() %>" width="40" height="40"/></a>
|
||||
<a href="/" class="navbar-brand"><%= cloudronName %></a>
|
||||
</div>
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<a href="/" class="navbar-brand navbar-brand-icon"><img src="/api/v1/cloudron/avatar?<%= Math.random() %>" width="40" height="40"/></a>
|
||||
<a href="/" class="navbar-brand"><%= cloudronName %></a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
+33
-37
@@ -2,45 +2,41 @@
|
||||
|
||||
<!-- login tester -->
|
||||
|
||||
<div class="container">
|
||||
<div class="layout-content">
|
||||
<div class="card" style="padding: 20px; margin-top: 50px; max-width: 620px;">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col-md-12" style="text-align: center;">
|
||||
<img width="128" height="128" src="<%= applicationLogo %>?<%= Math.random() %>"/>
|
||||
<h1><small>Login to</small> <%= applicationName %></h1>
|
||||
<br/>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<% if (error) { %>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h4 class="has-error"><%= error %></h4>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<form id="loginForm" action="" method="post">
|
||||
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputUsername">Username or Email</label>
|
||||
<input type="text" class="form-control" id="inputUsername" name="username" value="<%= username %>" autofocus required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputPassword">Password</label>
|
||||
<input type="password" class="form-control" name="password" id="inputPassword" value="<%= password %>" required>
|
||||
</div>
|
||||
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Sign in"/>
|
||||
</form>
|
||||
<a href="/api/v1/session/password/resetRequest.html">Reset your password</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12" style="text-align: center;">
|
||||
<img width="128" height="128" src="<%= applicationLogo %>?<%= Math.random() %>"/>
|
||||
<h1><small>Login to</small> <%= applicationName %></h1>
|
||||
<br/>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<% if (error) { -%>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h4 class="has-error"><%= error %></h4>
|
||||
</div>
|
||||
</div>
|
||||
<% } -%>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<form id="loginForm" action="" method="post">
|
||||
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputUsername">Username or Email</label>
|
||||
<input type="text" class="form-control" id="inputUsername" name="username" value="<%= username %>" autofocus required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputPassword">Password</label>
|
||||
<input type="password" class="form-control" name="password" id="inputPassword" value="<%= password %>" required>
|
||||
</div>
|
||||
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Sign in"/>
|
||||
</form>
|
||||
<a href="/api/v1/session/password/resetRequest.html">Reset your password</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -12,36 +12,41 @@ app.controller('Controller', [function () {}]);
|
||||
|
||||
</script>
|
||||
|
||||
<center>
|
||||
<h1>Hello <%= user.username %>, set a new password</h1>
|
||||
</center>
|
||||
<div class="layout-content">
|
||||
|
||||
<div class="container" ng-app="Application" ng-controller="Controller">
|
||||
<center>
|
||||
<h2>Hello <%= user.username %>, set a new password</h2>
|
||||
</center>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="container" ng-app="Application" ng-controller="Controller">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<form action="/api/v1/session/password/reset" method="post" name="resetForm" autocomplete="off" role="form" novalidate>
|
||||
<input type="password" style="display: none;">
|
||||
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
|
||||
<input type="hidden" name="resetToken" value="<%= resetToken %>"/>
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<form action="/api/v1/session/password/reset" method="post" name="resetForm" autocomplete="off" role="form" novalidate>
|
||||
<input type="password" style="display: none;">
|
||||
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
|
||||
<input type="hidden" name="resetToken" value="<%= resetToken %>"/>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': resetForm.password.$dirty && resetForm.password.$invalid }">
|
||||
<label class="control-label" for="inputPassword">New Password</label>
|
||||
<div class="control-label" ng-show="resetForm.password.$dirty && resetForm.password.$invalid">
|
||||
<small ng-show="resetForm.password.$dirty && resetForm.password.$invalid">Password must be 8-30 character with at least one uppercase, one numeric and one special character</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" id="inputPassword" ng-model="password" name="password" ng-pattern="/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/" autofocus required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': resetForm.passwordRepeat.$dirty && (password !== passwordRepeat) }">
|
||||
<label class="control-label" for="inputPasswordRepeat">Repeat Password</label>
|
||||
<div class="control-label" ng-show="resetForm.passwordRepeat.$dirty && (password !== passwordRepeat)">
|
||||
<small ng-show="resetForm.passwordRepeat.$dirty && (password !== passwordRepeat)">Passwords don't match</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" id="inputPasswordRepeat" ng-model="passwordRepeat" name="passwordRepeat" required>
|
||||
</div>
|
||||
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Create" ng-disabled="resetForm.$invalid || password !== passwordRepeat"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': resetForm.password.$dirty && resetForm.password.$invalid }">
|
||||
<label class="control-label" for="inputPassword">New Password</label>
|
||||
<div class="control-label" ng-show="resetForm.password.$dirty && resetForm.password.$invalid">
|
||||
<small ng-show="resetForm.password.$dirty && resetForm.password.$invalid">Password must be 8-30 character with at least one uppercase, one numeric and one special character</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" id="inputPassword" ng-model="password" name="password" ng-pattern="/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/" autofocus required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': resetForm.passwordRepeat.$dirty && (password !== passwordRepeat) }">
|
||||
<label class="control-label" for="inputPasswordRepeat">Repeat Password</label>
|
||||
<div class="control-label" ng-show="resetForm.passwordRepeat.$dirty && (password !== passwordRepeat)">
|
||||
<small ng-show="resetForm.passwordRepeat.$dirty && (password !== passwordRepeat)">Passwords don't match</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" id="inputPasswordRepeat" ng-model="passwordRepeat" name="passwordRepeat" required>
|
||||
</div>
|
||||
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Create" ng-disabled="resetForm.$invalid || password !== passwordRepeat"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% include footer %>
|
||||
|
||||
@@ -2,26 +2,29 @@
|
||||
|
||||
<!-- tester -->
|
||||
|
||||
<center>
|
||||
<h1>Reset your password</h1>
|
||||
</center>
|
||||
<div class="layout-content">
|
||||
|
||||
<br/>
|
||||
<center>
|
||||
<h2>Reset your password</h2>
|
||||
</center>
|
||||
|
||||
<div class="container">
|
||||
<br/>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<form action="/api/v1/session/password/resetRequest" method="post" autocomplete="off">
|
||||
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputIdentifier">Username or Email</label>
|
||||
<input type="text" class="form-control" id="inputIdentifier" name="identifier" autofocus required>
|
||||
</div>
|
||||
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Reset"/>
|
||||
</form>
|
||||
<a href="/api/v1/session/login">Login</a>
|
||||
</div>
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<form action="/api/v1/session/password/resetRequest" method="post" autocomplete="off">
|
||||
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="inputIdentifier">Username or Email</label>
|
||||
<input type="text" class="form-control" id="inputIdentifier" name="identifier" autofocus required>
|
||||
</div>
|
||||
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Reset"/>
|
||||
</form>
|
||||
<a href="/api/v1/session/login">Login</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% include footer %>
|
||||
|
||||
@@ -2,21 +2,24 @@
|
||||
|
||||
<!-- tester -->
|
||||
|
||||
<center>
|
||||
<h1>Password reset successful</h1>
|
||||
</center>
|
||||
<div class="layout-content">
|
||||
|
||||
<br/>
|
||||
<center>
|
||||
<h2>Password reset successful</h2>
|
||||
</center>
|
||||
|
||||
<div class="container">
|
||||
<br/>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<center class="col-md-6 col-md-offset-3">
|
||||
<p>An email was sent to you with a link to set a new password.</p>
|
||||
<br/>
|
||||
<br/>
|
||||
If you have not received any email, simply <a href="/api/v1/session/password/resetRequest.html">try again</a>.
|
||||
</center>
|
||||
<div class="col-md-6 col-md-offset-3 text-center">
|
||||
<p>An email was sent to you with a link to set a new password.</p>
|
||||
<br/>
|
||||
<br/>
|
||||
If you have not received any email, simply <a href="/api/v1/session/password/resetRequest.html">try again</a>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% include footer %>
|
||||
|
||||
+7
-4
@@ -7,7 +7,8 @@ var config = require('./config.js'),
|
||||
exports = module.exports = {
|
||||
CLOUDRON_DEFAULT_AVATAR_FILE: path.join(__dirname + '/../assets/avatar.png'),
|
||||
INFRA_VERSION_FILE: path.join(config.baseDir(), 'platformdata/INFRA_VERSION'),
|
||||
BACKUP_RESULT_FILE: path.join(config.baseDir(), 'platformdata/backupresult'),
|
||||
BACKUP_RESULT_FILE: path.join(config.baseDir(), 'platformdata/backup/result.txt'),
|
||||
BACKUP_LOG_FILE: path.join(config.baseDir(), 'platformdata/backup/logs.txt'),
|
||||
|
||||
OLD_DATA_DIR: path.join(config.baseDir(), 'data'),
|
||||
PLATFORM_DATA_DIR: path.join(config.baseDir(), 'platformdata'),
|
||||
@@ -17,16 +18,18 @@ exports = module.exports = {
|
||||
ACME_CHALLENGES_DIR: path.join(config.baseDir(), 'platformdata/acme'),
|
||||
ADDON_CONFIG_DIR: path.join(config.baseDir(), 'platformdata/addons'),
|
||||
COLLECTD_APPCONFIG_DIR: path.join(config.baseDir(), 'platformdata/collectd/collectd.conf.d'),
|
||||
MAIL_DATA_DIR: path.join(config.baseDir(), 'platformdata/mail'),
|
||||
LOGROTATE_CONFIG_DIR: path.join(config.baseDir(), 'platformdata/logrotate.d'),
|
||||
NGINX_CONFIG_DIR: path.join(config.baseDir(), 'platformdata/nginx'),
|
||||
NGINX_APPCONFIG_DIR: path.join(config.baseDir(), 'platformdata/nginx/applications'),
|
||||
NGINX_CERT_DIR: path.join(config.baseDir(), 'platformdata/nginx/cert'),
|
||||
BACKUP_INFO_DIR: path.join(config.baseDir(), 'platformdata/backup'),
|
||||
SNAPSHOT_INFO_FILE: path.join(config.baseDir(), 'platformdata/backup/snapshot-info.json'),
|
||||
|
||||
// this is not part of appdata because an icon may be set before install
|
||||
ACME_ACCOUNT_KEY_FILE: path.join(config.baseDir(), 'boxdata/acme/acme.key'),
|
||||
APP_ICONS_DIR: path.join(config.baseDir(), 'boxdata/appicons'),
|
||||
MAIL_DATA_DIR: path.join(config.baseDir(), 'boxdata/mail'),
|
||||
ACME_ACCOUNT_KEY_FILE: path.join(config.baseDir(), 'boxdata/acme/acme.key'),
|
||||
APP_CERTS_DIR: path.join(config.baseDir(), 'boxdata/certs'),
|
||||
CLOUDRON_AVATAR_FILE: path.join(config.baseDir(), 'boxdata/avatar.png'),
|
||||
FIRST_RUN_FILE: path.join(config.baseDir(), 'boxdata/first_run'),
|
||||
UPDATE_CHECKER_FILE: path.join(config.baseDir(), 'boxdata/updatechecker.json')
|
||||
};
|
||||
|
||||
+13
-11
@@ -162,7 +162,7 @@ function startMysql(callback) {
|
||||
const memoryLimit = (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 256;
|
||||
|
||||
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mysql_vars.sh',
|
||||
'MYSQL_ROOT_PASSWORD=' + rootPassword +'\nMYSQL_ROOT_HOST=172.18.0.1', 'utf8')) {
|
||||
'MYSQL_ROOT_PASSWORD=' + rootPassword +'\nMYSQL_ROOT_HOST=172.18.0.1', 'utf8')) {
|
||||
return callback(new Error('Could not create mysql var file:' + safe.error.message));
|
||||
}
|
||||
|
||||
@@ -238,7 +238,7 @@ function createMailConfig(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const fqdn = config.fqdn();
|
||||
const mailFqdn = config.adminFqdn();
|
||||
const mailFqdn = config.mailFqdn();
|
||||
const alertsFrom = 'no-reply@' + config.fqdn();
|
||||
|
||||
debug('createMailConfig: generating mail config');
|
||||
@@ -254,17 +254,17 @@ function createMailConfig(callback) {
|
||||
var mailFromValidation = result[settings.MAIL_FROM_VALIDATION_KEY];
|
||||
|
||||
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mail/mail.ini',
|
||||
`mail_domain=${fqdn}\nmail_server_name=${mailFqdn}\nalerts_from=${alertsFrom}\nalerts_to=${alertsTo}\ncatch_all=${catchAll}\nmail_from_validation=${mailFromValidation}\n`, 'utf8')) {
|
||||
`mail_domain=${fqdn}\nmail_server_name=${mailFqdn}\nalerts_from=${alertsFrom}\nalerts_to=${alertsTo}\ncatch_all=${catchAll}\nmail_from_validation=${mailFromValidation}\ndkim_selector=${config.dkimSelector()}\n`, 'utf8')) {
|
||||
return callback(new Error('Could not create mail var file:' + safe.error.message));
|
||||
}
|
||||
|
||||
var relay = result[settings.MAIL_RELAY_KEY];
|
||||
|
||||
const enabled = relay.provider !== 'cloudron-smtp' ? true : false,
|
||||
host = relay.host || '',
|
||||
port = relay.port || 25,
|
||||
username = relay.username || '',
|
||||
password = relay.password || '';
|
||||
host = relay.host || '',
|
||||
port = relay.port || 25,
|
||||
username = relay.username || '',
|
||||
password = relay.password || '';
|
||||
|
||||
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mail/smtp_forward.ini',
|
||||
`enable_outbound=${enabled}\nhost=${host}\nport=${port}\nenable_tls=true\nauth_type=plain\nauth_user=${username}\nauth_pass=${password}`, 'utf8')) {
|
||||
@@ -283,13 +283,13 @@ function startMail(callback) {
|
||||
// mail container uses /app/data for backed up data and /run for restart-able data
|
||||
|
||||
const tag = infra.images.mail.tag;
|
||||
const dataDir = paths.PLATFORM_DATA_DIR;
|
||||
const memoryLimit = Math.max((1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 128, 256);
|
||||
|
||||
// admin and mail share the same certificate
|
||||
certificates.getAdminCertificate(function (error, cert, key) {
|
||||
if (error) return callback(error);
|
||||
|
||||
// the setup script copies dhparams.pem to /addons/mail
|
||||
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mail/tls_cert.pem', cert)) return callback(new Error('Could not create cert file:' + safe.error.message));
|
||||
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mail/tls_key.pem', key)) return callback(new Error('Could not create key file:' + safe.error.message));
|
||||
|
||||
@@ -311,8 +311,8 @@ function startMail(callback) {
|
||||
--dns 172.18.0.1 \
|
||||
--dns-search=. \
|
||||
--env ENABLE_MDA=${mailConfig.enabled} \
|
||||
-v "${dataDir}/mail:/app/data" \
|
||||
-v "${dataDir}/addons/mail:/etc/mail" \
|
||||
-v "${paths.MAIL_DATA_DIR}:/app/data" \
|
||||
-v "${paths.PLATFORM_DATA_DIR}/addons/mail:/etc/mail" \
|
||||
${ports} \
|
||||
--read-only -v /run -v /tmp ${tag}`;
|
||||
|
||||
@@ -329,7 +329,9 @@ function startMail(callback) {
|
||||
|
||||
async.mapSeries(records, function (record, iteratorCallback) {
|
||||
subdomains.upsert(record.subdomain, record.type, record.values, iteratorCallback);
|
||||
}, callback);
|
||||
}, NOOP_CALLBACK); // do not crash if DNS creds do not work in startup sequence
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+14
-3
@@ -2,8 +2,9 @@
|
||||
|
||||
exports = module.exports = {
|
||||
set: set,
|
||||
setDetail: setDetail,
|
||||
clear: clear,
|
||||
get: get,
|
||||
getAll: getAll,
|
||||
|
||||
UPDATE: 'update',
|
||||
BACKUP: 'backup',
|
||||
@@ -29,12 +30,22 @@ function set(tag, percent, message) {
|
||||
|
||||
progress[tag] = {
|
||||
percent: percent,
|
||||
message: message
|
||||
message: message,
|
||||
detail: ''
|
||||
};
|
||||
|
||||
debug('%s: %s %s', tag, percent, message);
|
||||
}
|
||||
|
||||
function setDetail(tag, detail) {
|
||||
assert.strictEqual(typeof tag, 'string');
|
||||
assert.strictEqual(typeof detail, 'string');
|
||||
|
||||
if (!progress[tag]) return debug('unable to set detail %s', detail);
|
||||
|
||||
progress[tag].detail = detail;
|
||||
}
|
||||
|
||||
function clear(tag) {
|
||||
assert.strictEqual(typeof tag, 'string');
|
||||
|
||||
@@ -43,6 +54,6 @@ function clear(tag) {
|
||||
debug('clearing %s', tag);
|
||||
}
|
||||
|
||||
function get() {
|
||||
function getAll() {
|
||||
return progress;
|
||||
}
|
||||
|
||||
+107
-7
@@ -17,8 +17,12 @@ exports = module.exports = {
|
||||
stopApp: stopApp,
|
||||
startApp: startApp,
|
||||
exec: exec,
|
||||
execWebSocket: execWebSocket,
|
||||
|
||||
cloneApp: cloneApp
|
||||
cloneApp: cloneApp,
|
||||
|
||||
uploadFile: uploadFile,
|
||||
downloadFile: downloadFile
|
||||
};
|
||||
|
||||
var apps = require('../apps.js'),
|
||||
@@ -30,7 +34,8 @@ var apps = require('../apps.js'),
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
paths = require('../paths.js'),
|
||||
safe = require('safetydance'),
|
||||
util = require('util');
|
||||
util = require('util'),
|
||||
WebSocket = require('ws');
|
||||
|
||||
function auditSource(req) {
|
||||
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
|
||||
@@ -47,7 +52,6 @@ function removeInternalAppFields(app) {
|
||||
health: app.health,
|
||||
location: app.location,
|
||||
accessRestriction: app.accessRestriction,
|
||||
lastBackupId: app.lastBackupId,
|
||||
manifest: app.manifest,
|
||||
portBindings: app.portBindings,
|
||||
iconUrl: app.iconUrl,
|
||||
@@ -58,7 +62,8 @@ function removeInternalAppFields(app) {
|
||||
xFrameOptions: app.xFrameOptions,
|
||||
sso: app.sso,
|
||||
debugMode: app.debugMode,
|
||||
robotsTxt: app.robotsTxt
|
||||
robotsTxt: app.robotsTxt,
|
||||
enableBackup: app.enableBackup
|
||||
};
|
||||
}
|
||||
|
||||
@@ -115,6 +120,7 @@ function installApp(req, res, next) {
|
||||
if ('icon' in data && typeof data.icon !== 'string') return next(new HttpError(400, 'icon is not a string'));
|
||||
|
||||
if (data.backupId && typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be string or null'));
|
||||
if (data.backupFormat && typeof data.backupFormat !== 'string') return next(new HttpError(400, 'backupFormat must be string or null'));
|
||||
|
||||
// falsy values in cert and key unset the cert
|
||||
if (data.key && typeof data.cert !== 'string') return next(new HttpError(400, 'cert must be a string'));
|
||||
@@ -130,6 +136,7 @@ function installApp(req, res, next) {
|
||||
if (data.xFrameOptions && typeof data.xFrameOptions !== 'string') return next(new HttpError(400, 'xFrameOptions must be a string'));
|
||||
|
||||
if ('sso' in data && typeof data.sso !== 'boolean') return next(new HttpError(400, 'sso must be a boolean'));
|
||||
if ('enableBackup' in data && typeof data.enableBackup !== 'boolean') return next(new HttpError(400, 'enableBackup must be a boolean'));
|
||||
|
||||
if (('debugMode' in data) && typeof data.debugMode !== 'object') return next(new HttpError(400, 'debugMode must be an object'));
|
||||
|
||||
@@ -171,6 +178,8 @@ function configureApp(req, res, next) {
|
||||
if (data.altDomain && typeof data.altDomain !== 'string') return next(new HttpError(400, 'altDomain must be a string'));
|
||||
if (data.xFrameOptions && typeof data.xFrameOptions !== 'string') return next(new HttpError(400, 'xFrameOptions must be a string'));
|
||||
|
||||
if ('enableBackup' in data && typeof data.enableBackup !== 'boolean') return next(new HttpError(400, 'enableBackup must be a boolean'));
|
||||
|
||||
if (('debugMode' in data) && typeof data.debugMode !== 'object') return next(new HttpError(400, 'debugMode must be an object'));
|
||||
|
||||
if (data.robotsTxt && typeof data.robotsTxt !== 'string') return next(new HttpError(400, 'robotsTxt must be a string'));
|
||||
@@ -304,17 +313,15 @@ function updateApp(req, res, next) {
|
||||
if ('appStoreId' in data && typeof data.appStoreId !== 'string') return next(new HttpError(400, 'appStoreId must be a string'));
|
||||
if (!data.manifest && !data.appStoreId) return next(new HttpError(400, 'appStoreId or manifest is required'));
|
||||
|
||||
if ('portBindings' in data && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
|
||||
if ('icon' in data && typeof data.icon !== 'string') return next(new HttpError(400, 'icon is not a string'));
|
||||
if ('force' in data && typeof data.force !== 'boolean') return next(new HttpError(400, 'force must be a boolean'));
|
||||
|
||||
debug('Update app id:%s to manifest:%j with portBindings:%j', req.params.id, data.manifest, data.portBindings);
|
||||
debug('Update app id:%s to manifest:%j', req.params.id, data.manifest);
|
||||
|
||||
apps.update(req.params.id, req.body, auditSource(req), function (error) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, { }));
|
||||
@@ -455,6 +462,58 @@ function exec(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function execWebSocket(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
debug('Execing websocket into app id:%s and cmd:%s', req.params.id, req.query.cmd);
|
||||
|
||||
var cmd = null;
|
||||
if (req.query.cmd) {
|
||||
cmd = safe.JSON.parse(req.query.cmd);
|
||||
if (!util.isArray(cmd) || cmd.length < 1) return next(new HttpError(400, 'cmd must be array with atleast size 1'));
|
||||
}
|
||||
|
||||
var columns = req.query.columns ? parseInt(req.query.columns, 10) : null;
|
||||
if (isNaN(columns)) return next(new HttpError(400, 'columns must be a number'));
|
||||
|
||||
var rows = req.query.rows ? parseInt(req.query.rows, 10) : null;
|
||||
if (isNaN(rows)) return next(new HttpError(400, 'rows must be a number'));
|
||||
|
||||
var tty = req.query.tty === 'true' ? true : false;
|
||||
|
||||
apps.exec(req.params.id, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
|
||||
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
console.log('Connected to terminal');
|
||||
|
||||
req.clearTimeout();
|
||||
|
||||
res.handleUpgrade(function (ws) {
|
||||
duplexStream.on('end', function () { ws.close(); });
|
||||
duplexStream.on('close', function () { ws.close(); });
|
||||
duplexStream.on('error', function (error) {
|
||||
console.error('duplexStream error:', error);
|
||||
});
|
||||
duplexStream.on('data', function (data) {
|
||||
if (ws.readyState !== WebSocket.OPEN) return;
|
||||
ws.send(data.toString());
|
||||
});
|
||||
|
||||
ws.on('error', function (error) {
|
||||
console.error('websocket error:', error);
|
||||
});
|
||||
ws.on('message', function (msg) {
|
||||
duplexStream.write(msg);
|
||||
});
|
||||
ws.on('close', function () {
|
||||
// Clean things up, if any?
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function listBackups(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
@@ -471,3 +530,44 @@ function listBackups(req, res, next) {
|
||||
next(new HttpSuccess(200, { backups: result }));
|
||||
});
|
||||
}
|
||||
|
||||
function uploadFile(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
debug('uploadFile: %s %j -> %s', req.params.id, req.files, req.query.file);
|
||||
|
||||
if (typeof req.query.file !== 'string' || !req.query.file) return next(new HttpError(400, 'file query argument must be provided'));
|
||||
if (!req.files.file) return next(new HttpError(400, 'file must be provided as multipart'));
|
||||
|
||||
apps.uploadFile(req.params.id, req.files.file.path, req.query.file, function (error) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
debug('uploadFile: done');
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
});
|
||||
}
|
||||
|
||||
function downloadFile(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
debug('downloadFile: ', req.params.id, req.query.file);
|
||||
|
||||
if (typeof req.query.file !== 'string' || !req.query.file) return next(new HttpError(400, 'file query argument must be provided'));
|
||||
|
||||
apps.downloadFile(req.params.id, req.query.file, function (error, stream, info) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
var headers = {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Content-Disposition': 'attachment; filename="' + info.filename + '"'
|
||||
};
|
||||
if (info.size) headers['Content-Length'] = info.size;
|
||||
|
||||
res.writeHead(200, headers);
|
||||
|
||||
stream.pipe(res);
|
||||
});
|
||||
}
|
||||
|
||||
+84
-24
@@ -14,10 +14,14 @@ exports = module.exports = {
|
||||
update: update,
|
||||
feedback: feedback,
|
||||
checkForUpdates: checkForUpdates,
|
||||
getLogs: getLogs
|
||||
getLogs: getLogs,
|
||||
getLogStream: getLogStream,
|
||||
sendTestMail: sendTestMail
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
var appstore = require('../appstore.js'),
|
||||
AppstoreError = require('../appstore.js').AppstoreError,
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
cloudron = require('../cloudron.js'),
|
||||
CloudronError = cloudron.CloudronError,
|
||||
@@ -64,13 +68,13 @@ function activate(req, res, next) {
|
||||
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/setup/done').query({ setupToken: req.query.setupToken })
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return next(new HttpError(500, error));
|
||||
if (result.statusCode === 403) return next(new HttpError(403, 'Invalid token'));
|
||||
if (result.statusCode === 409) return next(new HttpError(409, 'Already setup'));
|
||||
if (result.statusCode !== 201) return next(new HttpError(500, result.text || 'Internal error'));
|
||||
if (error && !error.response) return next(new HttpError(500, error));
|
||||
if (result.statusCode === 403) return next(new HttpError(403, 'Invalid token'));
|
||||
if (result.statusCode === 409) return next(new HttpError(409, 'Already setup'));
|
||||
if (result.statusCode !== 201) return next(new HttpError(500, result.text || 'Internal error'));
|
||||
|
||||
next(new HttpSuccess(201, info));
|
||||
});
|
||||
next(new HttpSuccess(201, info));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -98,15 +102,15 @@ function setupTokenAuth(req, res, next) {
|
||||
if (typeof req.query.setupToken !== 'string' || !req.query.setupToken) return next(new HttpError(400, 'setupToken must be a non empty string'));
|
||||
|
||||
superagent.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/setup/verify').query({ setupToken:req.query.setupToken })
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return next(new HttpError(500, error));
|
||||
if (result.statusCode === 403) return next(new HttpError(403, 'Invalid token'));
|
||||
if (result.statusCode === 409) return next(new HttpError(409, 'Already setup'));
|
||||
if (result.statusCode !== 200) return next(new HttpError(500, result.text || 'Internal error'));
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return next(new HttpError(500, error));
|
||||
if (result.statusCode === 403) return next(new HttpError(403, 'Invalid token'));
|
||||
if (result.statusCode === 409) return next(new HttpError(409, 'Already setup'));
|
||||
if (result.statusCode !== 200) return next(new HttpError(500, result.text || 'Internal error'));
|
||||
|
||||
next();
|
||||
});
|
||||
next();
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
@@ -140,7 +144,7 @@ function getStatus(req, res, next) {
|
||||
}
|
||||
|
||||
function getProgress(req, res, next) {
|
||||
return next(new HttpSuccess(200, progress.get()));
|
||||
return next(new HttpSuccess(200, progress.getAll()));
|
||||
}
|
||||
|
||||
function reboot(req, res, next) {
|
||||
@@ -183,6 +187,10 @@ function getConfig(req, res, next) {
|
||||
cloudron.getConfig(function (error, cloudronConfig) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
if (!req.user.admin) {
|
||||
cloudronConfig = _.pick(cloudronConfig, 'apiServerOrigin', 'webServerOrigin', 'fqdn', 'version', 'progress', 'isCustomDomain', 'isDemo', 'cloudronName', 'provider');
|
||||
}
|
||||
|
||||
next(new HttpSuccess(200, cloudronConfig));
|
||||
});
|
||||
}
|
||||
@@ -218,17 +226,20 @@ function checkForUpdates(req, res, next) {
|
||||
function feedback(req, res, next) {
|
||||
assert.strictEqual(typeof req.user, 'object');
|
||||
|
||||
if (req.body.type !== mailer.FEEDBACK_TYPE_FEEDBACK &&
|
||||
req.body.type !== mailer.FEEDBACK_TYPE_TICKET &&
|
||||
req.body.type !== mailer.FEEDBACK_TYPE_APP_MISSING &&
|
||||
req.body.type !== mailer.FEEDBACK_TYPE_UPGRADE_REQUEST &&
|
||||
req.body.type !== mailer.FEEDBACK_TYPE_APP_ERROR) return next(new HttpError(400, 'type must be either "ticket", "feedback", "app_missing", "app_error" or "upgrade_request"'));
|
||||
const VALID_TYPES = [ 'feedback', 'ticket', 'app_missing', 'app_error', 'upgrade_request' ];
|
||||
|
||||
if (typeof req.body.type !== 'string' || !req.body.type) return next(new HttpError(400, 'type must be string'));
|
||||
if (VALID_TYPES.indexOf(req.body.type) === -1) return next(new HttpError(400, 'unknown type'));
|
||||
if (typeof req.body.subject !== 'string' || !req.body.subject) return next(new HttpError(400, 'subject must be string'));
|
||||
if (typeof req.body.description !== 'string' || !req.body.description) return next(new HttpError(400, 'description must be string'));
|
||||
|
||||
mailer.sendFeedback(req.user, req.body.type, req.body.subject, req.body.description);
|
||||
appstore.sendFeedback(_.extend(req.body, { email: req.user.alternateEmail || req.user.email, displayName: req.user.displayName }), function (error) {
|
||||
if (error && error.reason === AppstoreError.BILLING_REQUIRED) return next(new HttpError(402, 'Login to App Store to create support tickets. You can also email support@cloudron.io'));
|
||||
if (error) return next(new HttpError(503, 'Error contacting cloudron.io. Please email support@cloudron.io'));
|
||||
|
||||
next(new HttpSuccess(201, {}));
|
||||
});
|
||||
|
||||
next(new HttpSuccess(201, {}));
|
||||
}
|
||||
|
||||
function getLogs(req, res, next) {
|
||||
@@ -257,3 +268,52 @@ function getLogs(req, res, next) {
|
||||
logStream.pipe(res);
|
||||
});
|
||||
}
|
||||
|
||||
function getLogStream(req, res, next) {
|
||||
var lines = req.query.lines ? parseInt(req.query.lines, 10) : -10; // we ignore last-event-id
|
||||
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a valid number'));
|
||||
|
||||
var units = req.query.units || 'all';
|
||||
|
||||
function sse(id, data) { return 'id: ' + id + '\ndata: ' + data + '\n\n'; }
|
||||
|
||||
if (req.headers.accept !== 'text/event-stream') return next(new HttpError(400, 'This API call requires EventStream'));
|
||||
|
||||
var options = {
|
||||
lines: lines,
|
||||
follow: true,
|
||||
units: units.split(','),
|
||||
format: req.query.format
|
||||
};
|
||||
|
||||
cloudron.getLogs(options, function (error, logStream) {
|
||||
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(404, 'Invalid type'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
'X-Accel-Buffering': 'no', // disable nginx buffering
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
});
|
||||
res.write('retry: 3000\n');
|
||||
res.on('close', logStream.close);
|
||||
logStream.on('data', function (data) {
|
||||
var obj = JSON.parse(data);
|
||||
res.write(sse(obj.monotonicTimestamp, JSON.stringify(obj))); // send timestamp as id
|
||||
});
|
||||
logStream.on('end', res.end.bind(res));
|
||||
logStream.on('error', res.end.bind(res, null));
|
||||
});
|
||||
}
|
||||
|
||||
function sendTestMail(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (!req.body.email || typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be a non-empty string'));
|
||||
|
||||
mailer.sendTestMail(req.body.email);
|
||||
|
||||
next(new HttpSuccess(202));
|
||||
}
|
||||
|
||||
@@ -4,8 +4,7 @@ exports = module.exports = {
|
||||
enabled: enabled,
|
||||
setEnabled: setEnabled,
|
||||
status: status,
|
||||
login: login,
|
||||
apps: apps
|
||||
login: login
|
||||
};
|
||||
|
||||
var developer = require('../developer.js'),
|
||||
@@ -52,9 +51,3 @@ function login(req, res, next) {
|
||||
})(req, res, next);
|
||||
}
|
||||
|
||||
function apps(req, res, next) {
|
||||
developer.getNonApprovedApps(function (error, result) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
next(new HttpSuccess(200, { apps: result }));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
var appdb = require('../appdb'),
|
||||
apps = require('../apps'),
|
||||
assert = require('assert'),
|
||||
auth = require('../auth.js'),
|
||||
authcodedb = require('../authcodedb'),
|
||||
clients = require('../clients'),
|
||||
ClientsError = clients.ClientsError,
|
||||
@@ -533,6 +534,25 @@ function scope(requestedScope) {
|
||||
];
|
||||
}
|
||||
|
||||
function websocketAuth(requestedScopes, req, res, next) {
|
||||
assert(Array.isArray(requestedScopes));
|
||||
|
||||
if (typeof req.query.access_token !== 'string') return next(new HttpError(401, 'Unauthorized'));
|
||||
|
||||
auth.accessTokenAuth(req.query.access_token, function (error, user, info) {
|
||||
if (error) return next(new HttpError(500, error.message));
|
||||
if (!user) return next(new HttpError(401, 'Unauthorized'));
|
||||
|
||||
req.user = user;
|
||||
req.authInfo = info;
|
||||
|
||||
var error = validateRequestedScopes(req, requestedScopes);
|
||||
if (error) return next(new HttpError(401, error.message));
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
// Cross-site request forgery protection middleware for login form
|
||||
var csrf = [
|
||||
middleware.csrf(),
|
||||
@@ -559,5 +579,6 @@ exports = module.exports = {
|
||||
token: token,
|
||||
validateRequestedScopes: validateRequestedScopes,
|
||||
scope: scope,
|
||||
websocketAuth: websocketAuth,
|
||||
csrf: csrf
|
||||
};
|
||||
|
||||
@@ -274,6 +274,8 @@ function setBackupConfig(req, res, next) {
|
||||
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required'));
|
||||
if (typeof req.body.retentionSecs !== 'number') return next(new HttpError(400, 'retentionSecs is required'));
|
||||
if ('key' in req.body && typeof req.body.key !== 'string') return next(new HttpError(400, 'key must be a string'));
|
||||
if (typeof req.body.format !== 'string') return next(new HttpError(400, 'format must be a string'));
|
||||
if ('acceptSelfSignedCerts' in req.body && typeof req.body.acceptSelfSignedCerts !== 'boolean') return next(new HttpError(400, 'format must be a boolean'));
|
||||
|
||||
settings.setBackupConfig(req.body, function (error) {
|
||||
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
|
||||
@@ -22,7 +22,7 @@ var appdb = require('../../appdb.js'),
|
||||
hock = require('hock'),
|
||||
http = require('http'),
|
||||
https = require('https'),
|
||||
js2xml = require('js2xmlparser'),
|
||||
js2xml = require('js2xmlparser').parse,
|
||||
ldap = require('../../ldap.js'),
|
||||
net = require('net'),
|
||||
nock = require('nock'),
|
||||
@@ -34,14 +34,14 @@ var appdb = require('../../appdb.js'),
|
||||
taskmanager = require('../../taskmanager.js'),
|
||||
tokendb = require('../../tokendb.js'),
|
||||
url = require('url'),
|
||||
uuid = require('node-uuid'),
|
||||
uuid = require('uuid'),
|
||||
_ = require('underscore');
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
// Test image information
|
||||
var TEST_IMAGE_REPO = 'cloudron/test';
|
||||
var TEST_IMAGE_TAG = '24.0.1';
|
||||
var TEST_IMAGE_TAG = '25.2.0';
|
||||
var TEST_IMAGE = TEST_IMAGE_REPO + ':' + TEST_IMAGE_TAG;
|
||||
// var TEST_IMAGE_ID = child_process.execSync('docker inspect --format={{.Id}} ' + TEST_IMAGE).toString('utf8').trim();
|
||||
|
||||
@@ -269,7 +269,6 @@ describe('App API', function () {
|
||||
it('app install fails - missing manifest', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(res.body.message).to.eql('appStoreId or manifest is required');
|
||||
@@ -280,7 +279,7 @@ describe('App API', function () {
|
||||
it('app install fails - null manifest', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send({ manifest: null, password: PASSWORD })
|
||||
.send({ manifest: null })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(res.body.message).to.eql('appStoreId or manifest is required');
|
||||
@@ -291,7 +290,7 @@ describe('App API', function () {
|
||||
it('app install fails - bad manifest format', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send({ manifest: 'epic', password: PASSWORD })
|
||||
.send({ manifest: 'epic' })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(res.body.message).to.eql('manifest must be an object');
|
||||
@@ -302,7 +301,7 @@ describe('App API', function () {
|
||||
it('app install fails - empty appStoreId format', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send({ manifest: null, appStoreId: '', password: PASSWORD })
|
||||
.send({ manifest: null, appStoreId: '' })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(res.body.message).to.eql('appStoreId or manifest is required');
|
||||
@@ -323,7 +322,7 @@ describe('App API', function () {
|
||||
it('app install fails - invalid location', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send({ manifest: APP_MANIFEST, password: PASSWORD, location: '!awesome', accessRestriction: null })
|
||||
.send({ manifest: APP_MANIFEST, location: '!awesome', accessRestriction: null })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(res.body.message).to.eql('Hostname can only contain alphanumerics and hyphen');
|
||||
@@ -334,7 +333,7 @@ describe('App API', function () {
|
||||
it('app install fails - invalid location type', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send({ manifest: APP_MANIFEST, password: PASSWORD, location: 42, accessRestriction: null })
|
||||
.send({ manifest: APP_MANIFEST, location: 42, accessRestriction: null })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(res.body.message).to.eql('location is required');
|
||||
@@ -345,10 +344,10 @@ describe('App API', function () {
|
||||
it('app install fails - reserved admin location', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send({ manifest: APP_MANIFEST, password: PASSWORD, location: constants.ADMIN_LOCATION, accessRestriction: null })
|
||||
.send({ manifest: APP_MANIFEST, location: 'my', accessRestriction: null })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(res.body.message).to.eql(constants.ADMIN_LOCATION + ' is reserved');
|
||||
expect(res.body.message).to.eql('my is reserved');
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -356,7 +355,7 @@ describe('App API', function () {
|
||||
it('app install fails - reserved api location', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send({ manifest: APP_MANIFEST, password: PASSWORD, location: constants.API_LOCATION, accessRestriction: null })
|
||||
.send({ manifest: APP_MANIFEST, location: constants.API_LOCATION, accessRestriction: null })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(res.body.message).to.eql(constants.API_LOCATION + ' is reserved');
|
||||
@@ -367,7 +366,7 @@ describe('App API', function () {
|
||||
it('app install fails - portBindings must be object', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send({ manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: 23, accessRestriction: null })
|
||||
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: 23, accessRestriction: null })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(res.body.message).to.eql('portBindings must be an object');
|
||||
@@ -378,7 +377,7 @@ describe('App API', function () {
|
||||
it('app install fails - accessRestriction is required', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send({ manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: {} })
|
||||
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: {} })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(res.body.message).to.eql('accessRestriction is required');
|
||||
@@ -389,7 +388,7 @@ describe('App API', function () {
|
||||
it('app install fails - accessRestriction type is wrong', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send({ manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: {}, accessRestriction: '' })
|
||||
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: {}, accessRestriction: '' })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(res.body.message).to.eql('accessRestriction is required');
|
||||
@@ -400,7 +399,7 @@ describe('App API', function () {
|
||||
it('app install fails for non admin', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token_1 })
|
||||
.send({ manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: null })
|
||||
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: null, accessRestriction: null })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(403);
|
||||
done();
|
||||
@@ -412,7 +411,7 @@ describe('App API', function () {
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send({ appStoreId: APP_STORE_ID, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: { users: [ 'someuser' ], groups: [] } })
|
||||
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION, portBindings: null, accessRestriction: { users: [ 'someuser' ], groups: [] } })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(fake.isDone()).to.be.ok();
|
||||
@@ -426,7 +425,7 @@ describe('App API', function () {
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send({ appStoreId: APP_STORE_ID, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: null })
|
||||
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION, portBindings: null, accessRestriction: null })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(503);
|
||||
expect(fake1.isDone()).to.be.ok();
|
||||
@@ -442,7 +441,7 @@ describe('App API', function () {
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send({ appStoreId: APP_STORE_ID, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: { users: [ 'someuser' ], groups: [] } })
|
||||
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION, portBindings: null, accessRestriction: { users: [ 'someuser' ], groups: [] } })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(202);
|
||||
expect(res.body.id).to.be.a('string');
|
||||
@@ -455,7 +454,7 @@ describe('App API', function () {
|
||||
it('app install fails because of conflicting location', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send({ manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: null })
|
||||
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: null, accessRestriction: null })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(409);
|
||||
done();
|
||||
@@ -565,7 +564,7 @@ describe('App API', function () {
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send({ appStoreId: APP_STORE_ID, password: PASSWORD, location: APP_LOCATION_2, portBindings: null, accessRestriction: null })
|
||||
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION_2, portBindings: null, accessRestriction: null })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(202);
|
||||
expect(res.body.id).to.be.a('string');
|
||||
@@ -649,7 +648,7 @@ describe('App installation', function () {
|
||||
awsHockInstance
|
||||
.get('/2013-04-01/hostedzone')
|
||||
.max(Infinity)
|
||||
.reply(200, js2xml('ListHostedZonesResponse', awsHostedZones, { arrayMap: { HostedZones: 'HostedZone'} }), { 'Content-Type': 'application/xml' })
|
||||
.reply(200, js2xml('ListHostedZonesResponse', awsHostedZones, { wrapHandlers: { HostedZones: () => 'HostedZone'} }), { 'Content-Type': 'application/xml' })
|
||||
.filteringPathRegEx(/name=[^&]*/, 'name=location')
|
||||
.get('/2013-04-01/hostedzone/ZONEID/rrset?maxitems=1&name=location&type=A')
|
||||
.max(Infinity)
|
||||
@@ -695,7 +694,7 @@ describe('App installation', function () {
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/install')
|
||||
.query({ access_token: token })
|
||||
.send({ appStoreId: APP_STORE_ID, password: PASSWORD, location: APP_LOCATION, portBindings: { ECHO_SERVER_PORT: 7171 }, accessRestriction: null })
|
||||
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION, portBindings: { ECHO_SERVER_PORT: 7171 }, accessRestriction: null })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(202);
|
||||
expect(fake1.isDone()).to.be.ok();
|
||||
@@ -986,7 +985,7 @@ describe('App installation', function () {
|
||||
it('cannot reconfigure app with bad location', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD, location: 1234, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null })
|
||||
.send({ location: 1234, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
@@ -996,7 +995,7 @@ describe('App installation', function () {
|
||||
it('cannot reconfigure app with bad accessRestriction', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: false })
|
||||
.send({ portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: false })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
@@ -1006,7 +1005,7 @@ describe('App installation', function () {
|
||||
it('cannot reconfigure app with only the cert, no key', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null, cert: validCert1 })
|
||||
.send({ location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null, cert: validCert1 })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
@@ -1016,7 +1015,7 @@ describe('App installation', function () {
|
||||
it('cannot reconfigure app with only the key, no cert', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, key: validKey1 })
|
||||
.send({ location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, key: validKey1 })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
@@ -1026,7 +1025,7 @@ describe('App installation', function () {
|
||||
it('cannot reconfigure app with cert not being a string', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null, cert: 1234, key: validKey1 })
|
||||
.send({ location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null, cert: 1234, key: validKey1 })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
@@ -1036,7 +1035,7 @@ describe('App installation', function () {
|
||||
it('cannot reconfigure app with key not being a string', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, cert: validCert1, key: 1234 })
|
||||
.send({ location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, cert: validCert1, key: 1234 })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
@@ -1046,7 +1045,7 @@ describe('App installation', function () {
|
||||
it('non admin cannot reconfigure app', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
|
||||
.query({ access_token: token_1 })
|
||||
.send({ password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null })
|
||||
.send({ location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(403);
|
||||
done();
|
||||
@@ -1056,7 +1055,7 @@ describe('App installation', function () {
|
||||
it('can reconfigure app', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 } })
|
||||
.send({ location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 } })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(202);
|
||||
checkConfigureStatus(0, done);
|
||||
@@ -1098,7 +1097,7 @@ describe('App installation', function () {
|
||||
it('can reconfigure app with custom certificate', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
|
||||
.query({ access_token: token })
|
||||
.send({ password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null, cert: validCert1, key: validKey1 })
|
||||
.send({ location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null, cert: validCert1, key: validKey1 })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(202);
|
||||
checkConfigureStatus(0, done);
|
||||
|
||||
@@ -10,20 +10,16 @@ var appdb = require('../../appdb.js'),
|
||||
config = require('../../config.js'),
|
||||
database = require('../../database.js'),
|
||||
expect = require('expect.js'),
|
||||
hock = require('hock'),
|
||||
http = require('http'),
|
||||
nock = require('nock'),
|
||||
superagent = require('superagent'),
|
||||
server = require('../../server.js'),
|
||||
settings = require('../../settings.js'),
|
||||
url = require('url');
|
||||
settings = require('../../settings.js');
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
|
||||
var token = null;
|
||||
|
||||
var server;
|
||||
function setup(done) {
|
||||
nock.cleanAll();
|
||||
config._reset();
|
||||
@@ -40,19 +36,19 @@ function setup(done) {
|
||||
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(201);
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(201);
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
// stash token for further use
|
||||
token = result.body.token;
|
||||
// stash token for further use
|
||||
token = result.body.token;
|
||||
|
||||
callback();
|
||||
});
|
||||
callback();
|
||||
});
|
||||
},
|
||||
|
||||
function addApp(callback) {
|
||||
@@ -61,7 +57,7 @@ function setup(done) {
|
||||
},
|
||||
|
||||
function createSettings(callback) {
|
||||
settings.setBackupConfig({ provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' }, callback);
|
||||
settings.setBackupConfig({ provider: 'filesystem', backupFolder: '/tmp', format: 'tgz' }, callback);
|
||||
}
|
||||
], done);
|
||||
}
|
||||
@@ -75,19 +71,12 @@ function cleanup(done) {
|
||||
}
|
||||
|
||||
describe('Backups API', function () {
|
||||
var apiHockInstance = hock.createHock({ throwOnUnmatched: false }), apiHockServer;
|
||||
var scope1 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN')
|
||||
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey' } }, { 'Content-Type': 'application/json' });
|
||||
|
||||
before(setup);
|
||||
before(function (done) {
|
||||
apiHockInstance
|
||||
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN')
|
||||
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey' } }, { 'Content-Type': 'application/json' });
|
||||
var port = parseInt(url.parse(config.apiServerOrigin()).port, 10);
|
||||
apiHockServer = http.createServer(apiHockInstance.handler).listen(port, done);
|
||||
});
|
||||
|
||||
after(function (done) {
|
||||
apiHockServer.close();
|
||||
done();
|
||||
});
|
||||
after(cleanup);
|
||||
@@ -95,37 +84,35 @@ describe('Backups API', function () {
|
||||
describe('create', function () {
|
||||
it('fails due to mising token', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/backups')
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to wrong token', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/backups')
|
||||
.query({ access_token: token.toUpperCase() })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
.query({ access_token: token.toUpperCase() })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/backups')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(202);
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(202);
|
||||
|
||||
function checkAppstoreServerCalled() {
|
||||
apiHockInstance.done(function (error) {
|
||||
if (!error) return done();
|
||||
function checkAppstoreServerCalled() {
|
||||
if (scope1.isDone()) return done();
|
||||
|
||||
setTimeout(checkAppstoreServerCalled, 100);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
checkAppstoreServerCalled();
|
||||
});
|
||||
checkAppstoreServerCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ var async = require('async'),
|
||||
database = require('../../database.js'),
|
||||
oauth2 = require('../oauth2.js'),
|
||||
expect = require('expect.js'),
|
||||
uuid = require('node-uuid'),
|
||||
uuid = require('uuid'),
|
||||
nock = require('nock'),
|
||||
hat = require('hat'),
|
||||
superagent = require('superagent'),
|
||||
|
||||
+407
-290
@@ -9,20 +9,22 @@ var async = require('async'),
|
||||
config = require('../../config.js'),
|
||||
database = require('../../database.js'),
|
||||
expect = require('expect.js'),
|
||||
http = require('http'),
|
||||
locker = require('../../locker.js'),
|
||||
nock = require('nock'),
|
||||
os = require('os'),
|
||||
superagent = require('superagent'),
|
||||
server = require('../../server.js'),
|
||||
settings = require('../../settings.js'),
|
||||
shell = require('../../shell.js');
|
||||
shell = require('../../shell.js'),
|
||||
tokendb = require('../../tokendb.js');
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
|
||||
var token = null; // authentication token
|
||||
var USERNAME_1 = 'userTheFirst', EMAIL_1 = 'taO@zen.mac', userId_1, token_1;
|
||||
|
||||
var server;
|
||||
function setup(done) {
|
||||
nock.cleanAll();
|
||||
config._reset();
|
||||
@@ -31,7 +33,7 @@ function setup(done) {
|
||||
|
||||
server.start(function (error) {
|
||||
if (error) return done(error);
|
||||
settings.setBackupConfig({ provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' }, done);
|
||||
settings.setBackupConfig({ provider: 'filesystem', backupFolder: '/tmp', format: 'tgz' }, done);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -64,89 +66,89 @@ describe('Cloudron', function () {
|
||||
|
||||
it('fails due to missing setupToken', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.send({ username: '', password: 'somepassword', email: 'admin@foo.bar' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
.send({ username: '', password: 'somepassword', email: 'admin@foo.bar' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to internal server error on appstore side', function (done) {
|
||||
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(500, { message: 'this is wrong' });
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: 'someuser', password: 'strong#A3asdf', email: 'admin@foo.bar' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(500);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
done();
|
||||
});
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: 'someuser', password: 'strong#A3asdf', email: 'admin@foo.bar' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(500);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to empty username', function (done) {
|
||||
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: '', password: 'ADSFsdf$%436', email: 'admin@foo.bar' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
done();
|
||||
});
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: '', password: 'ADSFsdf$%436', email: 'admin@foo.bar' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to empty password', function (done) {
|
||||
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: 'someuser', password: '', email: 'admin@foo.bar' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
done();
|
||||
});
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: 'someuser', password: '', email: 'admin@foo.bar' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to empty email', function (done) {
|
||||
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: 'someuser', password: 'ADSF#asd546', email: '' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
done();
|
||||
});
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: 'someuser', password: 'ADSF#asd546', email: '' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to wrong displayName type', function (done) {
|
||||
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: 'someuser', password: 'ADSF?#asd546', email: 'admin@foo.bar', displayName: 1234 })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
done();
|
||||
});
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: 'someuser', password: 'ADSF?#asd546', email: 'admin@foo.bar', displayName: 1234 })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to invalid email', function (done) {
|
||||
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: 'someuser', password: 'ADSF#asd546', email: 'invalidemail' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
done();
|
||||
});
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: 'someuser', password: 'ADSF#asd546', email: 'invalidemail' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
@@ -154,27 +156,27 @@ describe('Cloudron', function () {
|
||||
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: 'someuser', password: 'ADSF#asd546', email: 'admin@foo.bar', displayName: 'tester' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(201);
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
done();
|
||||
});
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: 'someuser', password: 'ADSF#asd546', email: 'admin@foo.bar', displayName: 'tester' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(201);
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails the second time', function (done) {
|
||||
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: 'someuser', password: 'ADSF#asd546', email: 'admin@foo.bar' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(409);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
done();
|
||||
});
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: 'someuser', password: 'ADSF#asd546', email: 'admin@foo.bar' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(409);
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -187,22 +189,36 @@ describe('Cloudron', function () {
|
||||
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
|
||||
|
||||
config._reset();
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
// stash token for further use
|
||||
token = result.body.token;
|
||||
// stash token for further use
|
||||
token = result.body.token;
|
||||
|
||||
callback();
|
||||
});
|
||||
callback();
|
||||
});
|
||||
},
|
||||
|
||||
function (callback) {
|
||||
superagent.post(SERVER_URL + '/api/v1/users')
|
||||
.query({ access_token: token })
|
||||
.send({ username: USERNAME_1, email: EMAIL_1, invite: false })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(201);
|
||||
|
||||
token_1 = tokendb.generateToken();
|
||||
userId_1 = result.body.id;
|
||||
|
||||
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
|
||||
tokendb.add(token_1, userId_1, 'test-client-id', Date.now() + 100000, '*', callback);
|
||||
});
|
||||
}
|
||||
], done);
|
||||
});
|
||||
|
||||
@@ -210,60 +226,85 @@ describe('Cloudron', function () {
|
||||
|
||||
it('cannot get without token', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/cloudron/config')
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds without appstore', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/cloudron/config')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.apiServerOrigin).to.eql('http://localhost:6060');
|
||||
expect(result.body.webServerOrigin).to.eql(null);
|
||||
expect(result.body.fqdn).to.eql(config.fqdn());
|
||||
expect(result.body.isCustomDomain).to.eql(true);
|
||||
expect(result.body.progress).to.be.an('object');
|
||||
expect(result.body.update).to.be.an('object');
|
||||
expect(result.body.version).to.eql(config.version());
|
||||
expect(result.body.developerMode).to.be.a('boolean');
|
||||
expect(result.body.size).to.eql(null);
|
||||
expect(result.body.region).to.eql(null);
|
||||
expect(result.body.memory).to.eql(os.totalmem());
|
||||
expect(result.body.cloudronName).to.be.a('string');
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.apiServerOrigin).to.eql('http://localhost:6060');
|
||||
expect(result.body.webServerOrigin).to.eql(null);
|
||||
expect(result.body.fqdn).to.eql(config.fqdn());
|
||||
expect(result.body.isCustomDomain).to.eql(true);
|
||||
expect(result.body.progress).to.be.an('object');
|
||||
expect(result.body.update).to.be.an('object');
|
||||
expect(result.body.version).to.eql(config.version());
|
||||
expect(result.body.developerMode).to.be.a('boolean');
|
||||
expect(result.body.size).to.eql(null);
|
||||
expect(result.body.region).to.eql(null);
|
||||
expect(result.body.memory).to.eql(os.totalmem());
|
||||
expect(result.body.cloudronName).to.be.a('string');
|
||||
|
||||
done();
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
it('succeeds (admin)', function (done) {
|
||||
var scope = nock(config.apiServerOrigin())
|
||||
.get('/api/v1/boxes/localhost?token=' + config.token())
|
||||
.reply(200, { box: { region: 'sfo', size: '1gb' }, user: { }});
|
||||
.get('/api/v1/boxes/localhost?token=' + config.token())
|
||||
.reply(200, { box: { region: 'sfo', size: '1gb' }, user: { }});
|
||||
|
||||
superagent.get(SERVER_URL + '/api/v1/cloudron/config')
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.apiServerOrigin).to.eql('http://localhost:6060');
|
||||
expect(result.body.webServerOrigin).to.eql(null);
|
||||
expect(result.body.fqdn).to.eql(config.fqdn());
|
||||
expect(result.body.isCustomDomain).to.eql(true);
|
||||
expect(result.body.progress).to.be.an('object');
|
||||
expect(result.body.update).to.be.an('object');
|
||||
expect(result.body.version).to.eql(config.version());
|
||||
expect(result.body.developerMode).to.be.a('boolean');
|
||||
expect(result.body.size).to.eql('1gb');
|
||||
expect(result.body.region).to.eql('sfo');
|
||||
expect(result.body.memory).to.eql(os.totalmem());
|
||||
expect(result.body.cloudronName).to.be.a('string');
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(200);
|
||||
expect(result.body.apiServerOrigin).to.eql('http://localhost:6060');
|
||||
expect(result.body.webServerOrigin).to.eql(null);
|
||||
expect(result.body.fqdn).to.eql(config.fqdn());
|
||||
expect(result.body.isCustomDomain).to.eql(true);
|
||||
expect(result.body.progress).to.be.an('object');
|
||||
expect(result.body.update).to.be.an('object');
|
||||
expect(result.body.version).to.eql(config.version());
|
||||
expect(result.body.developerMode).to.be.a('boolean');
|
||||
expect(result.body.size).to.eql('1gb');
|
||||
expect(result.body.region).to.eql('sfo');
|
||||
expect(result.body.memory).to.eql(os.totalmem());
|
||||
expect(result.body.cloudronName).to.be.a('string');
|
||||
expect(result.body.provider).to.be.a('string');
|
||||
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
|
||||
done();
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds (non-admin)', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/cloudron/config')
|
||||
.query({ access_token: token_1 })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(200);
|
||||
|
||||
expect(result.body.apiServerOrigin).to.eql('http://localhost:6060');
|
||||
expect(result.body.webServerOrigin).to.eql(null);
|
||||
expect(result.body.fqdn).to.eql(config.fqdn());
|
||||
expect(result.body.isCustomDomain).to.eql(true);
|
||||
expect(result.body.progress).to.be.an('object');
|
||||
expect(result.body.version).to.eql(config.version());
|
||||
expect(result.body.cloudronName).to.be.a('string');
|
||||
expect(result.body.provider).to.be.a('string');
|
||||
|
||||
expect(result.body.update).to.be(undefined);
|
||||
expect(result.body.size).to.be(undefined);
|
||||
expect(result.body.region).to.be(undefined);
|
||||
expect(result.body.memory).to.be(undefined);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -278,18 +319,18 @@ describe('Cloudron', function () {
|
||||
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
// stash token for further use
|
||||
token = result.body.token;
|
||||
// stash token for further use
|
||||
token = result.body.token;
|
||||
|
||||
callback();
|
||||
});
|
||||
callback();
|
||||
});
|
||||
}
|
||||
], done);
|
||||
});
|
||||
@@ -301,73 +342,73 @@ describe('Cloudron', function () {
|
||||
|
||||
it('fails without token', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ size: 'small', region: 'sfo'})
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
.send({ size: 'small', region: 'sfo'})
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails without password', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ size: 'small', region: 'sfo'})
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
.send({ size: 'small', region: 'sfo'})
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds without size', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ region: 'sfo', password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(202);
|
||||
done();
|
||||
});
|
||||
.send({ region: 'sfo', password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(202);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with wrong size type', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ size: 4, region: 'sfo', password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
.send({ size: 4, region: 'sfo', password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds without region', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ size: 'small', password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(202);
|
||||
done();
|
||||
});
|
||||
.send({ size: 'small', password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(202);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with wrong region type', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ size: 'small', region: 4, password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
.send({ size: 'small', region: 4, password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails when in wrong state', function (done) {
|
||||
var scope2 = nock(config.apiServerOrigin())
|
||||
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN')
|
||||
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
|
||||
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN')
|
||||
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
|
||||
|
||||
var scope3 = nock(config.apiServerOrigin())
|
||||
.post('/api/v1/boxes/' + config.fqdn() + '/backupDone?token=APPSTORE_TOKEN', function (body) {
|
||||
return body.boxVersion && body.restoreKey && !body.appId && !body.appVersion && body.appBackupIds.length === 0;
|
||||
})
|
||||
.reply(200, { id: 'someid' });
|
||||
.post('/api/v1/boxes/' + config.fqdn() + '/backupDone?token=APPSTORE_TOKEN', function (body) {
|
||||
return body.boxVersion && body.restoreKey && !body.appId && !body.appVersion && body.appBackupIds.length === 0;
|
||||
})
|
||||
.reply(200, { id: 'someid' });
|
||||
|
||||
var scope1 = nock(config.apiServerOrigin())
|
||||
.post('/api/v1/boxes/' + config.fqdn() + '/migrate?token=APPSTORE_TOKEN', function (body) {
|
||||
@@ -377,22 +418,22 @@ describe('Cloudron', function () {
|
||||
injectShellMock();
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ size: 'small', region: 'sfo', password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(202);
|
||||
.send({ size: 'small', region: 'sfo', password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(202);
|
||||
|
||||
function checkAppstoreServerCalled() {
|
||||
if (scope1.isDone() && scope2.isDone() && scope3.isDone()) {
|
||||
restoreShellMock();
|
||||
return done();
|
||||
function checkAppstoreServerCalled() {
|
||||
if (scope1.isDone() && scope2.isDone() && scope3.isDone()) {
|
||||
restoreShellMock();
|
||||
return done();
|
||||
}
|
||||
|
||||
setTimeout(checkAppstoreServerCalled, 100);
|
||||
}
|
||||
|
||||
setTimeout(checkAppstoreServerCalled, 100);
|
||||
}
|
||||
|
||||
checkAppstoreServerCalled();
|
||||
});
|
||||
checkAppstoreServerCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
@@ -401,34 +442,34 @@ describe('Cloudron', function () {
|
||||
}).reply(202, {});
|
||||
|
||||
var scope2 = nock(config.apiServerOrigin())
|
||||
.post('/api/v1/boxes/' + config.fqdn() + '/backupDone?token=APPSTORE_TOKEN', function (body) {
|
||||
return body.boxVersion && body.restoreKey && !body.appId && !body.appVersion && body.appBackupIds.length === 0;
|
||||
})
|
||||
.reply(200, { id: 'someid' });
|
||||
.post('/api/v1/boxes/' + config.fqdn() + '/backupDone?token=APPSTORE_TOKEN', function (body) {
|
||||
return body.boxVersion && body.restoreKey && !body.appId && !body.appVersion && body.appBackupIds.length === 0;
|
||||
})
|
||||
.reply(200, { id: 'someid' });
|
||||
|
||||
var scope3 = nock(config.apiServerOrigin())
|
||||
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN')
|
||||
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
|
||||
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN')
|
||||
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
|
||||
|
||||
injectShellMock();
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ size: 'small', region: 'sfo', password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(202);
|
||||
.send({ size: 'small', region: 'sfo', password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(202);
|
||||
|
||||
function checkAppstoreServerCalled() {
|
||||
if (scope1.isDone() && scope2.isDone() && scope3.isDone()) {
|
||||
restoreShellMock();
|
||||
return done();
|
||||
function checkAppstoreServerCalled() {
|
||||
if (scope1.isDone() && scope2.isDone() && scope3.isDone()) {
|
||||
restoreShellMock();
|
||||
return done();
|
||||
}
|
||||
|
||||
setTimeout(checkAppstoreServerCalled, 100);
|
||||
}
|
||||
|
||||
setTimeout(checkAppstoreServerCalled, 100);
|
||||
}
|
||||
|
||||
checkAppstoreServerCalled();
|
||||
});
|
||||
checkAppstoreServerCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -441,21 +482,19 @@ describe('Cloudron', function () {
|
||||
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
|
||||
|
||||
config._reset();
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
// stash token for further use
|
||||
token = result.body.token;
|
||||
// stash token for further use
|
||||
token = result.body.token;
|
||||
|
||||
callback();
|
||||
});
|
||||
callback();
|
||||
});
|
||||
},
|
||||
], done);
|
||||
});
|
||||
@@ -464,111 +503,189 @@ describe('Cloudron', function () {
|
||||
|
||||
it('fails without token', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/feedback')
|
||||
.send({ type: 'ticket', subject: 'some subject', description: 'some description' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
.send({ type: 'ticket', subject: 'some subject', description: 'some description' })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails without type', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/feedback')
|
||||
.send({ subject: 'some subject', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
.send({ subject: 'some subject', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with empty type', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/feedback')
|
||||
.send({ type: '', subject: 'some subject', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
.send({ type: '', subject: 'some subject', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with unknown type', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/feedback')
|
||||
.send({ type: 'foobar', subject: 'some subject', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with ticket type', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/feedback')
|
||||
.send({ type: 'ticket', subject: 'some subject', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(201);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with app type', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/feedback')
|
||||
.send({ type: 'app_missing', subject: 'some subject', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(201);
|
||||
done();
|
||||
});
|
||||
.send({ type: 'foobar', subject: 'some subject', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails without description', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/feedback')
|
||||
.send({ type: 'ticket', subject: 'some subject' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
.send({ type: 'ticket', subject: 'some subject' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with empty subject', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/feedback')
|
||||
.send({ type: 'ticket', subject: '', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
.send({ type: 'ticket', subject: '', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with empty description', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/feedback')
|
||||
.send({ type: 'ticket', subject: 'some subject', description: '' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with feedback type', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/feedback')
|
||||
.send({ type: 'feedback', subject: 'some subject', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(201);
|
||||
done();
|
||||
});
|
||||
.send({ type: 'ticket', subject: 'some subject', description: '' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails without subject', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/feedback')
|
||||
.send({ type: 'ticket', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
.send({ type: 'ticket', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with ticket type', function (done) {
|
||||
var scope1 = nock(config.apiServerOrigin()).post('/api/v1/exchangeBoxTokenWithUserToken?token=APPSTORE_TOKEN').reply(201, { userId: 'USER_ID', cloudronId: 'CLOUDRON_ID', token: 'ACCESS_TOKEN' });
|
||||
var scope2 = nock(config.apiServerOrigin())
|
||||
.filteringRequestBody(function (/* unusedBody */) { return ''; }) // strip out body
|
||||
.post('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/feedback?accessToken=ACCESS_TOKEN')
|
||||
.reply(201, { });
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/feedback')
|
||||
.send({ type: 'ticket', subject: 'some subject', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(201);
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with app type', function (done) {
|
||||
var scope1 = nock(config.apiServerOrigin()).post('/api/v1/exchangeBoxTokenWithUserToken?token=APPSTORE_TOKEN').reply(201, { userId: 'USER_ID', cloudronId: 'CLOUDRON_ID', token: 'ACCESS_TOKEN' });
|
||||
var scope2 = nock(config.apiServerOrigin())
|
||||
.filteringRequestBody(function (/* unusedBody */) { return ''; }) // strip out body
|
||||
.post('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/feedback?accessToken=ACCESS_TOKEN')
|
||||
.reply(201, { });
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/feedback')
|
||||
.send({ type: 'app_missing', subject: 'some subject', description: 'some description' })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(201);
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('logs', function () {
|
||||
before(function (done) {
|
||||
async.series([
|
||||
setup,
|
||||
|
||||
function (callback) {
|
||||
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
|
||||
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
// stash token for further use
|
||||
token = result.body.token;
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
], done);
|
||||
});
|
||||
|
||||
after(cleanup);
|
||||
|
||||
it('logStream - requires event-stream accept header', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/cloudron/logstream')
|
||||
.query({ access_token: token, fromLine: 0 })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.be(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('logStream - stream logs', function (done) {
|
||||
var options = {
|
||||
port: config.get('port'), host: 'localhost', path: '/api/v1/cloudron/logstream?units=all&lines=10&access_token=' + token,
|
||||
headers: { 'Accept': 'text/event-stream', 'Connection': 'keep-alive' }
|
||||
};
|
||||
|
||||
// superagent doesn't work. maybe https://github.com/visionmedia/superagent/issues/420
|
||||
var req = http.get(options, function (res) {
|
||||
var data = '';
|
||||
res.on('data', function (d) { data += d.toString('utf8'); });
|
||||
setTimeout(function checkData() {
|
||||
var dataMessageFound = false;
|
||||
|
||||
expect(data.length).to.not.be(0);
|
||||
data.split('\n').forEach(function (line) {
|
||||
if (line.indexOf('id: ') === 0) {
|
||||
expect(parseInt(line.substr('id: '.length), 10)).to.be.a('number');
|
||||
} else if (line.indexOf('data: ') === 0) {
|
||||
var message = JSON.parse(line.slice('data: '.length)).message;
|
||||
if (Array.isArray(message) || typeof message === 'string') dataMessageFound = true;
|
||||
}
|
||||
});
|
||||
|
||||
expect(dataMessageFound).to.be.ok();
|
||||
|
||||
req.abort();
|
||||
done();
|
||||
}, 1000);
|
||||
res.on('error', done);
|
||||
});
|
||||
|
||||
req.on('error', done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
'use strict';
|
||||
|
||||
var expect = require('expect.js'),
|
||||
uuid = require('node-uuid'),
|
||||
uuid = require('uuid'),
|
||||
async = require('async'),
|
||||
hat = require('hat'),
|
||||
urlParse = require('url').parse,
|
||||
|
||||
@@ -70,7 +70,7 @@ describe('REST API', function () {
|
||||
.send("some invalid non-strict json")
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(400);
|
||||
expect(result.body.message).to.be('Bad JSON');
|
||||
expect(result.body.message).to.be('Failed to parse body');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,18 +12,23 @@ var appdb = require('../../appdb.js'),
|
||||
expect = require('expect.js'),
|
||||
hock = require('hock'),
|
||||
http = require('http'),
|
||||
MockS3 = require('mock-aws-s3'),
|
||||
nock = require('nock'),
|
||||
superagent = require('superagent'),
|
||||
os = require('os'),
|
||||
path = require('path'),
|
||||
rimraf = require('rimraf'),
|
||||
s3 = require('../../storage/s3.js'),
|
||||
safe = require('safetydance'),
|
||||
server = require('../../server.js'),
|
||||
settings = require('../../settings.js'),
|
||||
settingsdb = require('../../settingsdb.js'),
|
||||
superagent = require('superagent'),
|
||||
url = require('url');
|
||||
|
||||
var SERVER_URL = 'http://localhost:' + config.get('port');
|
||||
|
||||
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
|
||||
var token = null;
|
||||
|
||||
var server;
|
||||
function setup(done) {
|
||||
config.setVersion('1.2.3');
|
||||
|
||||
@@ -37,19 +42,16 @@ function setup(done) {
|
||||
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
|
||||
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(201);
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
.query({ setupToken: 'somesetuptoken' })
|
||||
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
|
||||
.end(function (error, result) {
|
||||
expect(result).to.be.ok();
|
||||
expect(result.statusCode).to.eql(201);
|
||||
expect(scope1.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
// stash token for further use
|
||||
token = result.body.token;
|
||||
|
||||
callback();
|
||||
});
|
||||
callback();
|
||||
});
|
||||
},
|
||||
|
||||
function addApp(callback) {
|
||||
@@ -58,12 +60,20 @@ function setup(done) {
|
||||
},
|
||||
|
||||
function createSettings(callback) {
|
||||
settings.setBackupConfig({ provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' }, callback);
|
||||
MockS3.config.basePath = path.join(os.tmpdir(), 's3-sysadmin-test-buckets/');
|
||||
|
||||
s3._mockInject(MockS3);
|
||||
|
||||
safe.fs.mkdirSync('/tmp/box-sysadmin-test');
|
||||
settingsdb.set(settings.BACKUP_CONFIG_KEY, JSON.stringify({ provider: 'caas', token: 'BACKUP_TOKEN', key: 'key', prefix: 'boxid', format: 'tgz'}), callback);
|
||||
}
|
||||
], done);
|
||||
}
|
||||
|
||||
function cleanup(done) {
|
||||
s3._mockRestore();
|
||||
rimraf.sync(MockS3.config.basePath);
|
||||
|
||||
database._clear(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
|
||||
@@ -93,19 +103,19 @@ describe('Internal API', function () {
|
||||
describe('backup', function () {
|
||||
it('succeeds', function (done) {
|
||||
superagent.post(config.sysadminOrigin() + '/api/v1/backup')
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(202);
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(202);
|
||||
|
||||
function checkAppstoreServerCalled() {
|
||||
apiHockInstance.done(function (error) {
|
||||
if (!error) return done();
|
||||
function checkAppstoreServerCalled() {
|
||||
apiHockInstance.done(function (error) {
|
||||
if (!error) return done();
|
||||
|
||||
setTimeout(checkAppstoreServerCalled, 100);
|
||||
});
|
||||
}
|
||||
setTimeout(checkAppstoreServerCalled, 100);
|
||||
});
|
||||
}
|
||||
|
||||
checkAppstoreServerCalled();
|
||||
});
|
||||
checkAppstoreServerCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+2
-2
@@ -145,8 +145,8 @@ function doTask(appId, taskName, callback) {
|
||||
apps.get(appId, function (error, app) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (app.installationState !== appdb.ISTATE_INSTALLED || app.runState !== appdb.RSTATE_RUNNING) {
|
||||
debug('task %s skipped. app %s is not installed/running', taskName, app.id);
|
||||
if (app.installationState !== appdb.ISTATE_INSTALLED || app.runState !== appdb.RSTATE_RUNNING || app.health !== appdb.HEALTH_HEALTHY) {
|
||||
debug('task %s skipped. app %s is not installed/running/healthy', taskName, app.id);
|
||||
return callback();
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,9 @@ if [[ $# == 1 && "$1" == "--check" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cmd="$1"
|
||||
appid="$2"
|
||||
|
||||
if [[ "${BOX_ENV}" == "cloudron" ]]; then
|
||||
# when restoring the cloudron with many apps, the apptasks rush in to restart
|
||||
# collectd which makes systemd/collectd very unhappy and puts the collectd in
|
||||
@@ -19,10 +22,17 @@ if [[ "${BOX_ENV}" == "cloudron" ]]; then
|
||||
for i in {1..10}; do
|
||||
echo "Restarting collectd"
|
||||
if systemctl restart collectd; then
|
||||
exit 0
|
||||
break
|
||||
fi
|
||||
echo "Failed to reload collectd. Maybe some other apptask is restarting it"
|
||||
sleep $((RANDOM%30))
|
||||
done
|
||||
|
||||
# delete old stats when uninstalling an app
|
||||
if [[ "${cmd}" == "remove" ]]; then
|
||||
echo "Removing collectd stats of ${appid}"
|
||||
|
||||
rm -rf ${HOME}/platformdata/graphite/whisper/collectd/localhost/*${appid}*
|
||||
fi
|
||||
fi
|
||||
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
echo "This script should be run as root." > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
echo "No arguments supplied"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$1" == "--check" ]]; then
|
||||
echo "OK"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cmd="$1"
|
||||
appid="$2"
|
||||
|
||||
if [[ "${cmd}" == "add" ]]; then
|
||||
# TODO prevent this script from moving the file from $1 into a random dir with using a relative ../ path
|
||||
if [[ "${BOX_ENV}" == "cloudron" ]]; then
|
||||
readonly destination_file_path="${HOME}/platformdata/logrotate.d/${appid}"
|
||||
else
|
||||
readonly destination_file_path="${HOME}/.cloudron_test/platformdata/logrotate.d/${appid}"
|
||||
fi
|
||||
|
||||
mv "${3}" "${destination_file_path}"
|
||||
chown root:root "${destination_file_path}"
|
||||
elif [[ "${cmd}" == "remove" ]]; then
|
||||
if [[ "${BOX_ENV}" == "cloudron" ]]; then
|
||||
rm -rf "${HOME}/platformdata/logrotate.d/${appid}"
|
||||
else
|
||||
rm -rf "${HOME}/.cloudron_test/platformdata/logrotate.d/${appid}"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
echo "This script should be run as root." > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
echo "No arguments supplied"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$1" == "--check" ]]; then
|
||||
echo "OK"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Running node with memory constraints"
|
||||
|
||||
# note BOX_ENV and NODE_ENV are derived from parent process
|
||||
exec env "DEBUG=box*,connect-lastmile" /usr/bin/node --max_old_space_size=300 "$@"
|
||||
+14
-3
@@ -17,10 +17,21 @@ if [[ "$1" == "--check" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# this script is called from redis addon as well!
|
||||
|
||||
appid="$1"
|
||||
rmdir="$2"
|
||||
|
||||
if [[ "${BOX_ENV}" == "cloudron" ]]; then
|
||||
readonly app_data_dir="${HOME}/appsdata/$1"
|
||||
rm -rf "${app_data_dir}"
|
||||
readonly app_data_dir="${HOME}/appsdata/${appid}"
|
||||
else
|
||||
readonly app_data_dir="${HOME}/.cloudron_test/appsdata/$1"
|
||||
readonly app_data_dir="${HOME}/.cloudron_test/appsdata/${appid}"
|
||||
fi
|
||||
|
||||
# the approach below ensures symlinked contents are also deleted
|
||||
|
||||
find -H "${app_data_dir}" -mindepth 1 -delete || true # -H means resolve symlink in args
|
||||
|
||||
if [[ "${rmdir}" == "true" ]]; then
|
||||
rm -rf "${app_data_dir}"
|
||||
fi
|
||||
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
'use strict';
|
||||
|
||||
require('supererror')({ splatchError: true });
|
||||
|
||||
var tar = require('tar-fs');
|
||||
|
||||
var sourceDir = process.argv[2];
|
||||
|
||||
if (sourceDir === '--check') return console.log('OK');
|
||||
|
||||
process.stderr.write('Packing ' + sourceDir + '\n');
|
||||
|
||||
tar.pack('/', {
|
||||
dereference: false, // pack the symlink and not what it points to
|
||||
entries: [ sourceDir ],
|
||||
map: function(header) {
|
||||
header.name = header.name.replace(new RegExp('^' + sourceDir + '(/?)'), '.$1'); // make paths relative
|
||||
return header;
|
||||
},
|
||||
strict: false // do not error for unknown types (skip fifo, char/block devices)
|
||||
}).pipe(process.stdout);
|
||||
+53
-38
@@ -19,7 +19,8 @@ var assert = require('assert'),
|
||||
middleware = require('./middleware'),
|
||||
passport = require('passport'),
|
||||
path = require('path'),
|
||||
routes = require('./routes/index.js');
|
||||
routes = require('./routes/index.js'),
|
||||
ws = require('ws');
|
||||
|
||||
var gHttpServer = null;
|
||||
var gSysadminHttpServer = null;
|
||||
@@ -28,6 +29,8 @@ function initializeExpressSync() {
|
||||
var app = express();
|
||||
var httpServer = http.createServer(app);
|
||||
|
||||
const wsServer = new ws.Server({ noServer: true }); // in noServer mode, we have to handle 'upgrade' and call handleUpgrade
|
||||
|
||||
var QUERY_LIMIT = '1mb', // max size for json and urlencoded queries (see also client_max_body_size in nginx)
|
||||
FIELD_LIMIT = 2 * 1024 * 1024; // max fields that can appear in multipart
|
||||
|
||||
@@ -56,29 +59,29 @@ function initializeExpressSync() {
|
||||
router.del = router.delete; // amend router.del for readability further on
|
||||
|
||||
app
|
||||
.use(middleware.timeout(REQUEST_TIMEOUT))
|
||||
.use(json)
|
||||
.use(urlencoded)
|
||||
.use(middleware.cookieParser())
|
||||
.use(middleware.cors({ origins: [ '*' ], allowCredentials: false }))
|
||||
.use(middleware.session({
|
||||
secret: hat(128), // we only use the session during oauth, and already have an in-memory session store, so we can safely change that during restarts
|
||||
resave: true,
|
||||
saveUninitialized: true,
|
||||
cookie: {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
secure: process.env.BOX_ENV !== 'test',
|
||||
maxAge: 600000
|
||||
}
|
||||
}))
|
||||
.use(passport.initialize())
|
||||
.use(passport.session())
|
||||
.use(router)
|
||||
.use(middleware.lastMile());
|
||||
.use(middleware.timeout(REQUEST_TIMEOUT))
|
||||
.use(json)
|
||||
.use(urlencoded)
|
||||
.use(middleware.cookieParser())
|
||||
.use(middleware.cors({ origins: [ '*' ], allowCredentials: false }))
|
||||
.use(middleware.session({
|
||||
secret: hat(128), // we only use the session during oauth, and already have an in-memory session store, so we can safely change that during restarts
|
||||
resave: true,
|
||||
saveUninitialized: true,
|
||||
cookie: {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
secure: process.env.BOX_ENV !== 'test',
|
||||
maxAge: 600000
|
||||
}
|
||||
}))
|
||||
.use(passport.initialize())
|
||||
.use(passport.session())
|
||||
.use(router)
|
||||
.use(middleware.lastMile());
|
||||
|
||||
// NOTE: these limits have to be in sync with nginx limits
|
||||
var FILE_SIZE_LIMIT = '1mb', // max file size that can be uploaded (see also client_max_body_size in nginx)
|
||||
var FILE_SIZE_LIMIT = '256mb', // max file size that can be uploaded (see also client_max_body_size in nginx)
|
||||
FILE_TIMEOUT = 60 * 1000; // increased timeout for file uploads (1 min)
|
||||
|
||||
var multipart = middleware.multipart({ maxFieldsSize: FIELD_LIMIT, limit: FILE_SIZE_LIMIT, timeout: FILE_TIMEOUT });
|
||||
@@ -105,22 +108,23 @@ function initializeExpressSync() {
|
||||
router.post('/api/v1/developer', developerScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.developer.setEnabled);
|
||||
router.get ('/api/v1/developer', developerScope, routes.developer.enabled, routes.developer.status);
|
||||
router.post('/api/v1/developer/login', routes.developer.enabled, routes.developer.login);
|
||||
router.get ('/api/v1/developer/apps', developerScope, routes.developer.enabled, routes.developer.apps);
|
||||
|
||||
// cloudron routes
|
||||
router.get ('/api/v1/cloudron/config', cloudronScope, routes.cloudron.getConfig);
|
||||
router.post('/api/v1/cloudron/update', cloudronScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.cloudron.update);
|
||||
router.post('/api/v1/cloudron/update', cloudronScope, routes.user.requireAdmin, routes.cloudron.update);
|
||||
router.post('/api/v1/cloudron/check_for_updates', cloudronScope, routes.user.requireAdmin, routes.cloudron.checkForUpdates);
|
||||
router.post('/api/v1/cloudron/reboot', cloudronScope, routes.user.requireAdmin, routes.cloudron.reboot);
|
||||
router.post('/api/v1/cloudron/migrate', cloudronScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.cloudron.migrate);
|
||||
router.get ('/api/v1/cloudron/graphs', cloudronScope, routes.user.requireAdmin, routes.graphs.getGraphs);
|
||||
router.get ('/api/v1/cloudron/disks', cloudronScope, routes.user.requireAdmin, routes.cloudron.getDisks);
|
||||
router.get ('/api/v1/cloudron/logs', cloudronScope, routes.user.requireAdmin, routes.cloudron.getLogs);
|
||||
router.get ('/api/v1/cloudron/logstream', cloudronScope, routes.user.requireAdmin, routes.cloudron.getLogStream);
|
||||
router.get ('/api/v1/cloudron/ssh/authorized_keys', cloudronScope, routes.user.requireAdmin, routes.ssh.getAuthorizedKeys);
|
||||
router.put ('/api/v1/cloudron/ssh/authorized_keys', cloudronScope, routes.user.requireAdmin, routes.ssh.addAuthorizedKey);
|
||||
router.get ('/api/v1/cloudron/ssh/authorized_keys/:identifier', cloudronScope, routes.user.requireAdmin, routes.ssh.getAuthorizedKey);
|
||||
router.del ('/api/v1/cloudron/ssh/authorized_keys/:identifier', cloudronScope, routes.user.requireAdmin, routes.ssh.delAuthorizedKey);
|
||||
router.get ('/api/v1/cloudron/eventlog', settingsScope, routes.user.requireAdmin, routes.eventlog.get);
|
||||
router.get ('/api/v1/cloudron/eventlog', cloudronScope, routes.user.requireAdmin, routes.eventlog.get);
|
||||
router.post('/api/v1/cloudron/send_test_mail', cloudronScope, routes.user.requireAdmin, routes.cloudron.sendTestMail);
|
||||
|
||||
// profile api, working off the user behind the provided token
|
||||
router.get ('/api/v1/profile', profileScope, routes.profile.get);
|
||||
@@ -178,8 +182,8 @@ function initializeExpressSync() {
|
||||
|
||||
router.post('/api/v1/apps/install', appsScope, routes.user.requireAdmin, routes.apps.installApp);
|
||||
router.post('/api/v1/apps/:id/uninstall', appsScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.apps.uninstallApp);
|
||||
router.post('/api/v1/apps/:id/configure', appsScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.apps.configureApp);
|
||||
router.post('/api/v1/apps/:id/update', appsScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.apps.updateApp);
|
||||
router.post('/api/v1/apps/:id/configure', appsScope, routes.user.requireAdmin, routes.apps.configureApp);
|
||||
router.post('/api/v1/apps/:id/update', appsScope, routes.user.requireAdmin, routes.apps.updateApp);
|
||||
router.post('/api/v1/apps/:id/restore', appsScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.apps.restoreApp);
|
||||
router.post('/api/v1/apps/:id/backup', appsScope, routes.user.requireAdmin, routes.apps.backupApp);
|
||||
router.get ('/api/v1/apps/:id/backups', appsScope, routes.user.requireAdmin, routes.apps.listBackups);
|
||||
@@ -188,7 +192,11 @@ function initializeExpressSync() {
|
||||
router.get ('/api/v1/apps/:id/logstream', appsScope, routes.user.requireAdmin, routes.apps.getLogStream);
|
||||
router.get ('/api/v1/apps/:id/logs', appsScope, routes.user.requireAdmin, routes.apps.getLogs);
|
||||
router.get ('/api/v1/apps/:id/exec', routes.developer.enabled, appsScope, routes.user.requireAdmin, routes.apps.exec);
|
||||
// websocket cannot do bearer authentication
|
||||
router.get ('/api/v1/apps/:id/execws', routes.oauth2.websocketAuth.bind(null, [ clients.SCOPE_APPS ]), routes.user.requireAdmin, routes.apps.execWebSocket);
|
||||
router.post('/api/v1/apps/:id/clone', appsScope, routes.user.requireAdmin, routes.apps.cloneApp);
|
||||
router.get ('/api/v1/apps/:id/download', appsScope, routes.user.requireAdmin, routes.apps.downloadFile);
|
||||
router.post('/api/v1/apps/:id/upload', appsScope, routes.user.requireAdmin, multipart, routes.apps.uploadFile);
|
||||
|
||||
// settings routes (these are for the settings tab - avatar & name have public routes for normal users. see above)
|
||||
router.get ('/api/v1/settings/autoupdate_pattern', settingsScope, routes.user.requireAdmin, routes.settings.getAutoupdatePattern);
|
||||
@@ -236,12 +244,19 @@ function initializeExpressSync() {
|
||||
// create a node response object for express
|
||||
var res = new http.ServerResponse({});
|
||||
res.assignSocket(socket);
|
||||
res.sendUpgradeHandshake = function () { // could extend express.response as well
|
||||
socket.write('HTTP/1.1 101 TCP Handshake\r\n' +
|
||||
'Upgrade: tcp\r\n' +
|
||||
'Connection: Upgrade\r\n' +
|
||||
'\r\n');
|
||||
};
|
||||
|
||||
if (req.headers.upgrade === 'websocket') {
|
||||
res.handleUpgrade = function (callback) {
|
||||
wsServer.handleUpgrade(req, socket, head, callback);
|
||||
};
|
||||
} else {
|
||||
res.sendUpgradeHandshake = function () { // could extend express.response as well
|
||||
socket.write('HTTP/1.1 101 TCP Handshake\r\n' +
|
||||
'Upgrade: tcp\r\n' +
|
||||
'Connection: Upgrade\r\n' +
|
||||
'\r\n');
|
||||
};
|
||||
}
|
||||
|
||||
// route through express middleware. if we provide no callback, express will provide a 'finalhandler'
|
||||
// TODO: it's not clear if socket needs to be destroyed
|
||||
@@ -268,11 +283,11 @@ function initializeSysadminExpressSync() {
|
||||
router.del = router.delete; // amend router.del for readability further on
|
||||
|
||||
app
|
||||
.use(middleware.timeout(REQUEST_TIMEOUT))
|
||||
.use(json)
|
||||
.use(urlencoded)
|
||||
.use(router)
|
||||
.use(middleware.lastMile());
|
||||
.use(middleware.timeout(REQUEST_TIMEOUT))
|
||||
.use(json)
|
||||
.use(urlencoded)
|
||||
.use(router)
|
||||
.use(middleware.lastMile());
|
||||
|
||||
// Sysadmin routes
|
||||
router.post('/api/v1/backup', routes.sysadmin.backup);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user