Compare commits

..

189 Commits

Author SHA1 Message Date
Girish Ramakrishnan e3964fd710 Fix crash in setUpdateSuccess 2019-09-04 16:11:59 -07:00
Girish Ramakrishnan e66961b814 merge registerSubdomains and registerAlternateDomains
also, merge unregisterSubdomain and unregisterAlternateDomains
also, fix crash where app.oldConfig is used
2019-09-03 19:55:41 -07:00
Girish Ramakrishnan 4176e5a98e Add note in schema 2019-09-03 19:04:12 -07:00
Girish Ramakrishnan 45cf8a62d1 remove obsolete comment 2019-09-03 18:52:37 -07:00
Girish Ramakrishnan b1380819ba debug taskId 2019-09-03 16:06:28 -07:00
Girish Ramakrishnan 57fa457596 Typo in error handling 2019-09-03 15:55:57 -07:00
Girish Ramakrishnan de1e218ce9 Return BAD_FIELD if dataDir conflicts 2019-09-03 15:17:48 -07:00
Girish Ramakrishnan e117ee2bef Cleanup app error codes
1. The error classes (like AppsError) now take a 3rd argument details.
We can attach anything in this 3rd argument and this gets sent in the
REST response as well.

2. The HttpError class is now HttpError(statusCode, errorOrMessage). If
it's an error object, it will take the message and other things which
were attached above from it and send them across. Previously, we used to
mark this case an internal error all the time.

3. AppsError only has generic codes now. The UI code then simply checks
for additional information that we attached to show errors. For example,
BAD_FIELD will have a field: 'xx' indicating which field is at fault.
ALREADY_EXISTS has information on which domain or port caused a problem.
The advantage here is we can drop all these error codes that are
specific to each model code.

4. Maybe some day, we can remove all these error classes and have only
one generic class. AppsError right now is pretty generic already. We can
use that error code everywhere... No need to translate errors also
everywhere.

5. Finally, in the router code, I have this function toHttpError (in
apps.js) which is also so much cleaner than what we have now. We keep
writing the same stuff over and over.
2019-09-03 10:39:02 -07:00
Girish Ramakrishnan a9e101d9f4 Add note on why it is BAD_STATE 2019-09-02 13:55:43 -07:00
Girish Ramakrishnan a2f8203a42 Add location conflict error code 2019-09-02 12:42:28 -07:00
Girish Ramakrishnan b9ee127775 Send detail in apps error 2019-09-02 12:41:32 -07:00
Girish Ramakrishnan 6668bb3e8a Handle BAD_STATE as well 2019-09-02 12:17:48 -07:00
Girish Ramakrishnan 5fd129e509 send reason code as part of details 2019-09-01 21:22:46 -07:00
Girish Ramakrishnan d59c1f53b9 apps: add detail to http error messages 2019-09-01 18:35:06 -07:00
Girish Ramakrishnan d2f38c1abc Remove unused error code 2019-09-01 17:39:07 -07:00
Girish Ramakrishnan c0a1db6941 Send details as part of AppsError
the last mile module has been updated to pipe through additional properties.
2019-09-01 13:42:25 -07:00
Girish Ramakrishnan fc10b4a79b Update lastmile 2019-09-01 13:34:40 -07:00
Girish Ramakrishnan 9da2117e99 Re-enable configure/restore test 2019-08-30 16:12:35 -07:00
Girish Ramakrishnan 7e030b149b More changes 2019-08-30 15:51:50 -07:00
Girish Ramakrishnan bd23abd265 tasks: make error a json
also, handle case where we never got to handle task exit cleanly
2019-08-30 14:49:45 -07:00
Girish Ramakrishnan dd0fb8292c Move state enums to the model code 2019-08-30 13:21:51 -07:00
Girish Ramakrishnan b4cbf63519 Remove installationState contraint when settings health 2019-08-30 12:57:59 -07:00
Girish Ramakrishnan 4fd04fa349 Add proper error codes 2019-08-30 12:42:38 -07:00
Girish Ramakrishnan c22cdb8d81 Return error object in the API 2019-08-30 11:34:04 -07:00
Johannes Zellner eb963b2eb4 Add externalldap pagination 2019-08-30 20:26:09 +02:00
Girish Ramakrishnan 7d299908c9 Fix tests 2019-08-30 10:49:43 -07:00
Girish Ramakrishnan 2585282f86 errorMessage -> errorJson 2019-08-30 10:02:24 -07:00
Johannes Zellner f25d5b3304 Deliver the user account source in the profile api 2019-08-30 13:36:37 +02:00
Johannes Zellner 6e878faa8b Also sync fallbackEmail from ldap 2019-08-30 13:10:49 +02:00
Johannes Zellner 15a6cbe62b Make sure all password change input fields use the same validation pattern 2019-08-30 12:14:32 +02:00
Johannes Zellner 76b0b214ec Do not sync non-ldap users from ldap if usernames match
We might want to make that option in the future depending on use-cases
2019-08-30 10:20:04 +02:00
Johannes Zellner f5c643c960 Add some debugging logs when users are created or updated 2019-08-30 10:20:04 +02:00
Johannes Zellner ca8e0613fb Skip notifications for ldap syncer events 2019-08-30 10:20:04 +02:00
Johannes Zellner 0c9334d0d2 Ensure we wait for all user sync db actions to finish 2019-08-30 10:20:04 +02:00
Johannes Zellner 712dc97e9b Move the basic ldap argument validation 2019-08-30 10:20:04 +02:00
Johannes Zellner 4df48c97ec Ignore the bindDn user in the syncer 2019-08-30 10:20:04 +02:00
Johannes Zellner fe3ea53cda Ldap usually uses cn as displayName 2019-08-30 10:20:04 +02:00
Johannes Zellner d385c80882 Use external ldap bind for users from ldap source 2019-08-30 10:20:04 +02:00
Johannes Zellner b823213c94 Create and update users from external ldap 2019-08-30 10:20:04 +02:00
Johannes Zellner 4b86311ab9 Add user source property to schema 2019-08-30 10:20:04 +02:00
Johannes Zellner b9efa8f445 Use tasks api for external ldap syncer 2019-08-30 10:20:04 +02:00
Johannes Zellner f8db12346d Perform some basic static input validation for external ldap 2019-08-30 10:20:04 +02:00
Johannes Zellner 4d3948f81f Improve external ldap error reporting 2019-08-30 10:20:04 +02:00
Johannes Zellner 5431d50206 Also check errors when no bindDn is provided 2019-08-30 10:20:04 +02:00
Johannes Zellner 6db078c26a Handle externalldap errors correctly in settings route 2019-08-30 10:20:04 +02:00
Johannes Zellner f61e9c7f27 Catch basic protocol errors 2019-08-30 10:20:04 +02:00
Johannes Zellner 567d92ce00 Add external ldap enabled boolean flag 2019-08-30 10:20:04 +02:00
Johannes Zellner 7a6d26c5da Add settings route handler for external ldap configs 2019-08-30 10:20:04 +02:00
Johannes Zellner 046ac85177 Add initial externalldap code to validate ldap configs 2019-08-30 10:20:04 +02:00
Girish Ramakrishnan f0fd088247 Pick values from updateConfig 2019-08-29 20:50:45 -07:00
Girish Ramakrishnan 5ec0d1e691 Add to changes 2019-08-29 15:10:55 -07:00
Girish Ramakrishnan 9391a934c3 Do not update on uninstall 2019-08-29 14:38:42 -07:00
Girish Ramakrishnan bb62e6a318 clear taskId in the parent process 2019-08-29 13:43:45 -07:00
Girish Ramakrishnan 0da6539c48 Add progressCallback to run commands 2019-08-29 13:41:11 -07:00
Girish Ramakrishnan 9cf833dab2 Use taskId instead of states to check bad state
a) this is because, we have install state and run state.
b) we have to put taskId as part of the transaction to prevent race
2019-08-29 13:15:40 -07:00
Girish Ramakrishnan ed57260fcf add note on why it is a state 2019-08-29 11:07:19 -07:00
Girish Ramakrishnan c98f625c4c Make force update as task arg 2019-08-29 10:59:05 -07:00
Girish Ramakrishnan f3008064e4 Fix installation states
App operations can only be done in 'installed' or 'error' state.
If some other operation is in progress, you have to cancel it first.

This guarantees that the old app command got killed.
2019-08-29 10:14:23 -07:00
Girish Ramakrishnan 1faee00764 Better progress text when waiting for other tasks
Fixes #630
2019-08-28 22:13:50 -07:00
Girish Ramakrishnan a40505e2ee Remove pause flag, we already have platform lock 2019-08-28 22:13:50 -07:00
Girish Ramakrishnan 484202b4c6 better variable name 2019-08-28 21:31:42 -07:00
Girish Ramakrishnan 6a7fc17c60 Make restore/configure use scheduleTask 2019-08-28 15:36:50 -07:00
Girish Ramakrishnan 05d3897ae2 Make apps test work again 2019-08-28 15:30:23 -07:00
Girish Ramakrishnan 9f1210202a port taskmanager to use tasks 2019-08-28 15:17:53 -07:00
Girish Ramakrishnan be6b172d6f Remove app task eventlog 2019-08-28 13:24:05 -07:00
Girish Ramakrishnan fef9e0a5c1 Handle app task crashes 2019-08-28 13:19:47 -07:00
Girish Ramakrishnan b84b033bf3 typo 2019-08-28 12:51:00 -07:00
Girish Ramakrishnan b30ff1f55a rework task API to be two-phase
this lets us avoid this EE based API. we now add and then start
explicitly.
2019-08-28 10:39:40 -07:00
Girish Ramakrishnan c6be0b290b updateConfig is no more 2019-08-27 22:03:43 -07:00
Girish Ramakrishnan 33cfd7a629 Add 'success' virtual field to the tasks 2019-08-27 21:36:52 -07:00
Girish Ramakrishnan 5952a5c69d Send taskId in the response 2019-08-27 21:35:40 -07:00
Girish Ramakrishnan 20de563925 rename installationProgress to errorMessage 2019-08-27 20:08:35 -07:00
Girish Ramakrishnan 7da80b4c62 Ensure log directory 2019-08-27 16:36:19 -07:00
Girish Ramakrishnan 15d765be6d Comment out couple of tests 2019-08-27 16:36:19 -07:00
Girish Ramakrishnan bfe2f116a7 Make restoreConfigJson, oldConfigJson, updateConfigJson as task args 2019-08-27 16:36:15 -07:00
Girish Ramakrishnan f535b3de2f Add logFile option to startTask 2019-08-27 15:26:26 -07:00
Girish Ramakrishnan e560c18b57 apptask is not a separate process anymore 2019-08-27 12:31:59 -07:00
Girish Ramakrishnan aecb99b6a3 Use task API in run commands 2019-08-27 12:31:59 -07:00
Girish Ramakrishnan 7da17f8190 Use task API in app backup 2019-08-27 12:31:59 -07:00
Girish Ramakrishnan 1964270a4f Use task API in app update 2019-08-27 12:31:59 -07:00
Girish Ramakrishnan f45b61d95c Use task API for app restore 2019-08-27 12:31:59 -07:00
Girish Ramakrishnan ff11c38169 Use task API for app clone 2019-08-27 12:31:59 -07:00
Girish Ramakrishnan 3e67067431 Use task API for app uninstall 2019-08-27 12:31:59 -07:00
Girish Ramakrishnan 824f00d1e8 Use task API for app configure 2019-08-27 12:31:59 -07:00
Girish Ramakrishnan 96d19f59a4 Use task API for app install 2019-08-27 12:31:59 -07:00
Girish Ramakrishnan 42c6fe50d2 Make progressCallback take an optional callback 2019-08-27 12:31:59 -07:00
Girish Ramakrishnan 9242f7095a Migrate apptask to use tasks framework 2019-08-27 12:31:59 -07:00
Girish Ramakrishnan 99c9fbc38f add taskId to appdb 2019-08-27 12:31:55 -07:00
Girish Ramakrishnan 0d31207ad7 add taskId to tasks table 2019-08-26 14:27:29 -07:00
Girish Ramakrishnan 8af7dbc35a group -> list 2019-08-23 16:36:19 -07:00
Johannes Zellner d0a373cb15 Refresh dns records for dynamic dns setting every hour
Often home IPs change at the full hour, so we give it 5min to settle
2019-08-23 16:20:26 +02:00
Johannes Zellner 3dc87bbca8 Allow embedding pages from same origin 2019-08-22 11:37:54 +02:00
Girish Ramakrishnan a55c399585 cloudron-support: Use the PROVIDER file 2019-08-21 21:23:22 -07:00
Girish Ramakrishnan f74aa24dd2 cloudron-setup: make it work for old installs
need this for the auto-update test
2019-08-21 21:18:08 -07:00
Girish Ramakrishnan 1aa7eb4478 Collect and aggregate du information twice a day 2019-08-21 13:45:52 -07:00
Girish Ramakrishnan 0c7002ba59 settings.adminOrigin has to be loaded after setAdmin
the dangers of caching
2019-08-21 13:26:15 -07:00
Girish Ramakrishnan fd6dd1ea18 Add timestamp to the logs 2019-08-21 10:16:57 -07:00
Girish Ramakrishnan aa74d5cd82 Add a note 2019-08-20 19:47:24 -07:00
Girish Ramakrishnan 8fc10a0bdd Add note 2019-08-20 15:26:00 -07:00
Girish Ramakrishnan 809ed0f0dc clear db to stop the scheduler 2019-08-20 13:42:03 -07:00
Girish Ramakrishnan b8a4e1c4a3 Use docker for apps-test 2019-08-20 13:34:18 -07:00
Girish Ramakrishnan d9e45f732b Fix error message 2019-08-20 13:22:43 -07:00
Girish Ramakrishnan ca025b36f7 Define DockerError.EXTERNAL_ERROR 2019-08-20 13:11:06 -07:00
Girish Ramakrishnan bfb719d35e Remove use of dockerProxy 2019-08-20 11:50:54 -07:00
Girish Ramakrishnan 2a1b61107f Make the ldap test work 2019-08-20 11:45:00 -07:00
Johannes Zellner 969cee7c90 Rebuilding docker container takes a long time, so callback early
Otherwise the service restart request will just time out in the
dashboard
2019-08-20 12:12:24 +02:00
Johannes Zellner 7a3f579d3e Do not crash if a service without active docker container needs restart 2019-08-20 12:06:49 +02:00
Johannes Zellner 288d5efa88 Return DockerError instead of generic Error 2019-08-20 12:06:22 +02:00
Johannes Zellner 7be821963c Ensure we report stopped status for addons without a running docker container 2019-08-20 11:27:35 +02:00
Girish Ramakrishnan a236f8992a graphite: Fixup healthcheck url 2019-08-19 22:27:53 -07:00
Girish Ramakrishnan a5c2257f39 Update changelog 2019-08-19 19:10:30 -07:00
Girish Ramakrishnan 9d3b4ba816 store docker df output as well 2019-08-19 16:15:31 -07:00
Girish Ramakrishnan 43bf0767f1 remove docker proxy from tests
this is too complicated and also makes it hard to keep up with upstream API
2019-08-19 14:35:23 -07:00
Girish Ramakrishnan b301e5b151 Add dockerDataDisk to disks response 2019-08-19 14:14:13 -07:00
Girish Ramakrishnan 2b484c0382 collect maildata size separately 2019-08-19 13:23:31 -07:00
Johannes Zellner f40ab4e2d5 Use git+https for cloudron-io/df 2019-08-19 09:19:06 +02:00
Girish Ramakrishnan c0a27380e9 Add to changes 2019-08-18 21:59:20 -07:00
Girish Ramakrishnan 0d7a3f43c4 Collect du information 2019-08-18 21:52:41 -07:00
Girish Ramakrishnan 8195e439f3 Return all disks
We now return the disk information per app as well
2019-08-16 10:29:56 -07:00
Johannes Zellner b5edbf716c Add interox provider option 2019-08-14 14:47:08 +02:00
Johannes Zellner 466265fde1 Allow iframe embedding of cloudron.io pages 2019-08-14 14:44:12 +02:00
Girish Ramakrishnan 40033e09cd Check disk space before create app/box backups
Fixes #642
2019-08-13 10:55:02 -07:00
Johannes Zellner 573663412c Add more fuzzy user search
Fixes #646
2019-08-13 15:23:26 +02:00
Johannes Zellner 17599417f7 WIP 2019-08-13 15:16:17 +02:00
Girish Ramakrishnan 0ece6d8b0e Add dataDir to schema 2019-08-12 22:16:45 -07:00
Girish Ramakrishnan e0ac0393fe typo 2019-08-12 21:47:22 -07:00
Girish Ramakrishnan 6d38b3255c Check available disk space before update
Part of #642
2019-08-12 21:09:22 -07:00
Girish Ramakrishnan 477ff424d6 Check if we have enough disk space for docker
Part of #642
2019-08-12 20:47:53 -07:00
Girish Ramakrishnan a843104348 sftp: typo 2019-08-12 11:31:59 -07:00
Girish Ramakrishnan 0f4bc0981a graphs: fix render forwarding 2019-08-12 11:01:12 -07:00
Girish Ramakrishnan 07f6351465 Make graphite dashboard appear again 2019-08-11 22:45:11 -07:00
Girish Ramakrishnan 1b26e86365 Fix test 2019-08-10 09:37:42 -07:00
Girish Ramakrishnan 94b4bf94c0 Merge active flag into update route 2019-08-08 08:17:08 -07:00
Girish Ramakrishnan d5de05b633 Send user active flag 2019-08-08 07:19:50 -07:00
Girish Ramakrishnan 0ab6cad048 Add user enable/disable flag 2019-08-08 06:31:46 -07:00
Girish Ramakrishnan 9833ad548b Better progress message 2019-08-07 06:23:28 -07:00
Girish Ramakrishnan aa1ba3b226 Make apps-test pass 2019-08-06 10:27:19 -07:00
Girish Ramakrishnan 3774d4de28 Use API for pullImage
This allows to get proper error code

This mostly reverts commit 734286ba2e.
2019-08-06 09:46:09 -07:00
Girish Ramakrishnan e4961726bc Try to make apps-test work 2019-08-05 16:16:56 -07:00
Girish Ramakrishnan 77cf7d0da6 Bump test version 2019-08-05 06:39:16 -07:00
Girish Ramakrishnan a993e0b228 Add fullstop 2019-08-04 15:35:42 -07:00
Girish Ramakrishnan 43671a9fd6 Clear update task progress after update
Fixes #635
2019-08-04 10:21:42 -07:00
Girish Ramakrishnan 49cfd1e9b7 Add notification for box update
Fixes #634
2019-08-04 05:44:04 -07:00
Girish Ramakrishnan 58d4a4f54f quoting and fullstop 2019-08-03 10:36:38 -07:00
Girish Ramakrishnan e4e328ba6a Make user event titles better 2019-08-03 10:17:07 -07:00
Girish Ramakrishnan fd6bc955ff Remove extra line 2019-08-03 09:41:16 -07:00
Girish Ramakrishnan 511a18e0ed Display app changelog and version
part of #634
2019-08-03 09:22:13 -07:00
Girish Ramakrishnan e29d224a92 Be a bit more specific 2019-07-31 15:45:25 -07:00
Girish Ramakrishnan bb48ffb01f Fixup UA for easier detection (other than IP) 2019-07-31 15:43:15 -07:00
Girish Ramakrishnan 31fd3411f7 Add to changes 2019-07-30 15:41:03 -07:00
Girish Ramakrishnan a737d2675e Fix logrotation rules
* explicitly specify the dirs that are getting rotated
* app log rules are now moved to logrotate.ejs
* we keep task logs for a week

Some testing notes:
* touch -d "10 days ago" foo
* logrotate /etc/logrotate.conf -v to test rotation. there is a state
file created in /var/lib/logrotate/status. If we have a 'daily' rule,
it will get processed only after a log line in status exists and it's atleast
1 day old timestamp.

https://github.com/logrotate/logrotate/blob/master/logrotate.c is quite
readable
2019-07-30 15:37:15 -07:00
Girish Ramakrishnan fd462659cd tmp cleaner: only remove files and not directories
some apps like rocket.chat create directories in tmp and removing those
directories causes problems (for example, uploading)
2019-07-30 14:06:04 -07:00
Johannes Zellner cb10d0d465 Add time4vps provider 2019-07-29 20:54:41 +02:00
Girish Ramakrishnan 61f1c4884c Refactor logic so that settings.adminDomain is read in the closure 2019-07-27 19:21:49 -07:00
Girish Ramakrishnan 2cd00de6e3 initCache after every restore 2019-07-27 19:09:09 -07:00
Girish Ramakrishnan d3c5d53eae silence mysql warning 2019-07-26 22:35:44 -07:00
Girish Ramakrishnan 6dfafae342 move the comment 2019-07-26 22:19:14 -07:00
Girish Ramakrishnan 2f861c3309 specify the database 2019-07-26 22:12:40 -07:00
Girish Ramakrishnan af388f0f16 IP based restore 2019-07-26 21:37:33 -07:00
Girish Ramakrishnan c36cc86c5f init cache in various out of process workers 2019-07-26 19:38:42 -07:00
Girish Ramakrishnan 02f195b25c typo 2019-07-26 15:02:03 -07:00
Girish Ramakrishnan 18623fd9b7 cloudron.conf can be removed post migration 2019-07-26 14:55:36 -07:00
Girish Ramakrishnan 9b74bb73aa config.js is dead, long live config.js
we use settings now
2019-07-26 14:51:51 -07:00
Girish Ramakrishnan ee9636b496 move use of TEST and CLOUDRON to constants 2019-07-26 10:13:20 -07:00
Girish Ramakrishnan 5c2cbd7840 Move config.baseDir to paths 2019-07-26 10:07:08 -07:00
Girish Ramakrishnan 7fbac6cc17 typo 2019-07-26 08:44:37 -07:00
Girish Ramakrishnan 9e7e9d66bf move provider into sysinfo
this is ideally "auto-detectable" runtime information
2019-07-26 07:33:22 -07:00
Girish Ramakrishnan 7fe66aa7fa Remove unused settings.get 2019-07-25 16:31:02 -07:00
Girish Ramakrishnan 2dda0efe83 Move config.database to db code itself 2019-07-25 16:12:42 -07:00
Girish Ramakrishnan 59620ca473 config.get is dead 2019-07-25 16:08:54 -07:00
Girish Ramakrishnan 12eae1eff2 Make port a constant 2019-07-25 16:08:54 -07:00
Girish Ramakrishnan b03bf87b7d remove unused function 2019-07-25 16:08:54 -07:00
Girish Ramakrishnan c32718b164 Make ldap and docker proxy port as constants 2019-07-25 16:08:54 -07:00
Girish Ramakrishnan a6ea12fedc Make internal smtp port a constant 2019-07-25 16:08:54 -07:00
Girish Ramakrishnan 2d260eb0d5 Make sysadminPort a constant 2019-07-25 16:08:51 -07:00
Girish Ramakrishnan d7dd069ae0 Use constants.version instead of config.version 2019-07-25 15:02:14 -07:00
Girish Ramakrishnan 6a77a58489 Move hasIPv6 into sysinfo 2019-07-25 14:35:08 -07:00
Girish Ramakrishnan c30ac5f927 Add setting key names 2019-07-25 14:35:04 -07:00
Girish Ramakrishnan 437f7ef890 Migrate cloudron.conf into db 2019-07-25 14:34:16 -07:00
Girish Ramakrishnan 1f7347e8de Make custom.yml as part of the backup 2019-07-25 10:28:42 -07:00
Girish Ramakrishnan 96f59d7cfe config: edition is long gone 2019-07-24 22:32:16 -07:00
Girish Ramakrishnan d55f65c7c9 Better error message 2019-07-24 22:11:22 -07:00
Girish Ramakrishnan 9a0d5b918f totp: set window to 2
see https://github.com/speakeasyjs/speakeasy#specifying-a-window-for-verifying-hotp-and-totp

A TOTP is incremented every step time-step seconds. By default, the time-step is
30 seconds. Window of 2 means, +- 2 steps.

Fixes #633
2019-07-23 14:45:54 -07:00
Girish Ramakrishnan 3553fbc7b6 Add wasabi storage backend 2019-07-22 16:44:56 -07:00
Girish Ramakrishnan 55d53f13d9 Improve error message 2019-07-18 10:28:37 -07:00
Johannes Zellner 27369a650c Fix disk full docs link 2019-07-16 15:10:56 +02:00
Girish Ramakrishnan 913f0d5d97 Update changes file 2019-07-15 10:50:14 -07:00
Girish Ramakrishnan ada63ec697 Add app.adminEmail 2019-07-12 14:29:35 -07:00
Girish Ramakrishnan 117f06e971 Fix issue where tar backups with files > 8GB was corrupt
Fixes #640
2019-07-10 14:58:54 -07:00
126 changed files with 2884 additions and 2482 deletions
+16
View File
@@ -1649,3 +1649,19 @@
[4.1.7]
* Fix issue where login looped when admin bit was removed
[4.2.0]
* Fix issue where tar backups with files > 8GB was corrupt
* Add SparkPost as mail relay backend
* Add Wasabi storage backend
* TOTP tokens are now checked for with +- 60 seconds
* IP based restore
* Fix issue where task logs were not getting rotated correctly
* Add notification for box update
* User enable/disable flag
* Check disk space before various operations like install, update, backup etc
* Collect per app du information
* Set Cloudron specific UA for healthchecks
* Show message why an app task is 'pending'
* Rework app task system so that we can now pass dynamic arguments
* Add external LDAP server integration
+3 -14
View File
@@ -14,25 +14,14 @@
require('supererror')({ splatchError: true });
let async = require('async'),
config = require('./src/config.js'),
ldap = require('./src/ldap.js'),
constants = require('./src/constants.js'),
dockerProxy = require('./src/dockerproxy.js'),
ldap = require('./src/ldap.js'),
server = require('./src/server.js');
console.log();
console.log('==========================================');
console.log(' Cloudron will use the following settings ');
console.log('==========================================');
console.log();
console.log(' Environment: ', config.CLOUDRON ? 'CLOUDRON' : 'TEST');
console.log(' Version: ', config.version());
console.log(' Admin Origin: ', config.adminOrigin());
console.log(' Appstore API server origin: ', config.apiServerOrigin());
console.log(' Appstore Web server origin: ', config.webServerOrigin());
console.log(' SysAdmin Port: ', config.get('sysadminPort'));
console.log(' LDAP Server Port: ', config.get('ldapPort'));
console.log(' Docker Proxy Port: ', config.get('dockerProxyPort'));
console.log();
console.log(` Cloudron ${constants.VERSION} `);
console.log('==========================================');
console.log();
@@ -0,0 +1,29 @@
'use strict';
var async = require('async'),
fs = require('fs'),
superagent = require('superagent');
exports.up = function(db, callback) {
if (!fs.existsSync('/etc/cloudron/cloudron.conf')) {
console.log('Unable to locate cloudron.conf');
return callback();
}
const config = JSON.parse(fs.readFileSync('/etc/cloudron/cloudron.conf', 'utf8'));
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
// we use replace instead of insert because the cloudron-setup adds api/web_server_origin even for legacy setups
db.runSql.bind(db, 'REPLACE INTO settings (name, value) VALUES(?, ?)', [ 'api_server_origin', config.apiServerOrigin ]),
db.runSql.bind(db, 'REPLACE INTO settings (name, value) VALUES(?, ?)', [ 'web_server_origin', config.webServerOrigin ]),
db.runSql.bind(db, 'REPLACE INTO settings (name, value) VALUES(?, ?)', [ 'admin_domain', config.adminDomain ]),
db.runSql.bind(db, 'REPLACE INTO settings (name, value) VALUES(?, ?)', [ 'admin_fqdn', config.adminFqdn ]),
db.runSql.bind(db, 'REPLACE INTO settings (name, value) VALUES(?, ?)', [ 'demo', config.isDemo ]),
db.runSql.bind(db, 'COMMIT')
], callback);
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,17 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE users ADD COLUMN active BOOLEAN DEFAULT 1', function (error) {
if (error) return callback(error);
callback();
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP COLUMN active', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,17 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE apps ADD COLUMN taskId INTEGER'),
db.runSql.bind(db, 'ALTER TABLE apps ADD CONSTRAINT apps_task_constraint FOREIGN KEY(taskId) REFERENCES tasks(id)')
], callback);
};
exports.down = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE app DROP FOREIGN KEY apps_task_constraint'),
db.runSql.bind(db, 'ALTER TABLE apps DROP COLUMN taskId'),
], callback);
};
@@ -0,0 +1,12 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps DROP updateConfigJson, DROP restoreConfigJson, DROP oldConfigJson', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps CHANGE installationProgress errorJson TEXT', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps CHANGE errorJson installationProgress TEXT', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,17 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE users ADD COLUMN source VARCHAR(128) DEFAULT ""', function (error) {
if (error) return callback(error);
callback();
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP COLUMN source', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE tasks CHANGE errorMessage errorJson TEXT', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE tasks CHANGE errorJson errorMessage TEXT', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
+8 -9
View File
@@ -27,6 +27,7 @@ CREATE TABLE IF NOT EXISTS users(
twoFactorAuthenticationSecret VARCHAR(128) DEFAULT "",
twoFactorAuthenticationEnabled BOOLEAN DEFAULT false,
admin BOOLEAN DEFAULT false,
source VARCHAR(128) DEFAULT "",
PRIMARY KEY(id));
@@ -64,7 +65,6 @@ CREATE TABLE IF NOT EXISTS apps(
id VARCHAR(128) NOT NULL UNIQUE,
appStoreId VARCHAR(128) NOT NULL,
installationState VARCHAR(512) NOT NULL,
installationProgress TEXT,
runState VARCHAR(512),
health VARCHAR(128),
healthTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the app last responded
@@ -87,12 +87,11 @@ CREATE TABLE IF NOT EXISTS apps(
mailboxName VARCHAR(128), // mailbox of this app. default allocated as '.app'
label VARCHAR(128), // display name
tagsJson VARCHAR(2048), // array of tags
dataDir VARCHAR(256) UNIQUE,
taskId INTEGER, // current task
errorJson TEXT,
// the following fields do not belong here, they can be removed when we use a queue for apptask
restoreConfigJson VARCHAR(256), // used to pass backupId to restore from to apptask
oldConfigJson TEXT, // used to pass old config to apptask (configure, restore)
updateConfigJson TEXT, // used to pass new config to apptask (update)
FOREIGN KEY(taskId) REFERENCES tasks(id),
PRIMARY KEY(id));
CREATE TABLE IF NOT EXISTS appPortBindings(
@@ -201,7 +200,7 @@ CREATE TABLE IF NOT EXISTS subdomains(
appId VARCHAR(128) NOT NULL,
domain VARCHAR(128) NOT NULL,
subdomain VARCHAR(128) NOT NULL,
type VARCHAR(128) NOT NULL,
type VARCHAR(128) NOT NULL, /* primary or redirect */
FOREIGN KEY(domain) REFERENCES domains(domain),
FOREIGN KEY(appId) REFERENCES apps(id),
@@ -212,8 +211,8 @@ CREATE TABLE IF NOT EXISTS tasks(
type VARCHAR(32) NOT NULL,
percent INTEGER DEFAULT 0,
message TEXT,
errorMessage TEXT,
result TEXT,
errorJson TEXT,
resultJson TEXT,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id));
+81 -23
View File
@@ -225,11 +225,68 @@
}
},
"@sindresorhus/df": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/df/-/df-3.1.0.tgz",
"integrity": "sha512-lWC2M3nT61HaRsO+DH2E/UFpLHhtyjO0kQA7pVyxTPctw+O6eCMfqXB9c05Fd2kvb3pGZs5gnlCSuskbuciLFQ==",
"version": "git+https://github.com/cloudron-io/df.git#7669c60e09e23f5c50d6613d6aa5caf55b4b3f2d",
"from": "git+https://github.com/cloudron-io/df.git#type",
"requires": {
"execa": "^1.0.0"
"execa": "^2.0.1"
},
"dependencies": {
"execa": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/execa/-/execa-2.0.4.tgz",
"integrity": "sha512-VcQfhuGD51vQUQtKIq2fjGDLDbL6N1DTQVpYzxZ7LPIXw3HqTuIz6uxRmpV1qf8i31LHf2kjiaGI+GdHwRgbnQ==",
"requires": {
"cross-spawn": "^6.0.5",
"get-stream": "^5.0.0",
"is-stream": "^2.0.0",
"merge-stream": "^2.0.0",
"npm-run-path": "^3.0.0",
"onetime": "^5.1.0",
"p-finally": "^2.0.0",
"signal-exit": "^3.0.2",
"strip-final-newline": "^2.0.0"
}
},
"get-stream": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz",
"integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==",
"requires": {
"pump": "^3.0.0"
}
},
"is-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz",
"integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw=="
},
"npm-run-path": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-3.1.0.tgz",
"integrity": "sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg==",
"requires": {
"path-key": "^3.0.0"
}
},
"p-finally": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz",
"integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw=="
},
"path-key": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.0.tgz",
"integrity": "sha512-8cChqz0RP6SHJkMt48FW0A7+qUOn+OsnOsVtzI59tZ8m+5bCSk7hzwET0pulwOM2YMn9J1efb07KB9l9f30SGg=="
},
"pump": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
"requires": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
}
}
},
"@types/caseless": {
@@ -900,26 +957,12 @@
"integrity": "sha1-F03MUSQ7nqwj+NmCFa62aU4uihI="
},
"connect-lastmile": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/connect-lastmile/-/connect-lastmile-1.0.2.tgz",
"integrity": "sha1-qfAolFHK4L3UgZFIZEa9eKHTCEQ=",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/connect-lastmile/-/connect-lastmile-1.2.1.tgz",
"integrity": "sha512-4sFIHpSC0MaVgJYseKypMdb+qUsVjxnILmissTpYkKTRvtAUq2C/UkKlMUuNQMX4jt+Os6CRWjat4+G5vzkb0w==",
"requires": {
"debug": "~2.1.0"
},
"dependencies": {
"debug": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz",
"integrity": "sha1-zoqxte6PvuK/o7Yzyrk9NmtjQY4=",
"requires": {
"ms": "0.7.0"
}
},
"ms": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-0.7.0.tgz",
"integrity": "sha1-hlvpTC5zl62KV9pqYzpuLzB5i4M="
}
"debug": "~4.1.1",
"underscore": "^1.9.1"
}
},
"connect-timeout": {
@@ -2748,6 +2791,11 @@
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
},
"merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="
},
"methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
@@ -3722,6 +3770,11 @@
"resolved": false,
"integrity": "sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw="
},
"pretty-bytes": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.3.0.tgz",
"integrity": "sha512-hjGrh+P926p4R4WbaB6OckyRtO0F0/lQBiT+0gnxjV+5kjPBrfVBFCsCLbMqVQeydvIoouYTCmmEURiH3R1Bdg=="
},
"process-nextick-args": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
@@ -4722,6 +4775,11 @@
"resolved": false,
"integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8="
},
"strip-final-newline": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="
},
"strip-indent": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz",
+3 -2
View File
@@ -16,14 +16,14 @@
"dependencies": {
"@google-cloud/dns": "^1.1.0",
"@google-cloud/storage": "^2.5.0",
"@sindresorhus/df": "^3.1.0",
"@sindresorhus/df": "git+https://github.com/cloudron-io/df.git#type",
"async": "^2.6.2",
"aws-sdk": "^2.476.0",
"body-parser": "^1.19.0",
"cloudron-manifestformat": "^2.15.0",
"connect": "^3.7.0",
"connect-ensure-login": "^0.1.1",
"connect-lastmile": "^1.0.2",
"connect-lastmile": "^1.2.1",
"connect-timeout": "^1.9.0",
"cookie-parser": "^1.4.4",
"cookie-session": "^1.3.3",
@@ -57,6 +57,7 @@
"passport-http-bearer": "^1.0.1",
"passport-local": "^1.0.0",
"passport-oauth2-client-password": "^0.1.2",
"pretty-bytes": "^5.3.0",
"progress-stream": "^2.0.0",
"proxy-middleware": "^0.15.0",
"qrcode": "^1.3.3",
+15 -3
View File
@@ -92,8 +92,9 @@ fi
echo "Running cloudron-setup with args : $@" > "${LOG_FILE}"
# validate arguments in the absence of data
readonly AVAILABLE_PROVIDERS="azure, caas, cloudscale, contabo, digitalocean, ec2, exoscale, galaxygate, gce, hetzner, interox, lightsail, linode, netcup, ovh, rosehosting, scaleway, time4vps, upcloud, vultr or generic"
if [[ -z "${provider}" ]]; then
echo "--provider is required (azure, contabo, digitalocean, ec2, exoscale, galaxygate, gce, hetzner, lightsail, linode, netcup, ovh, rosehosting, scaleway, upcloud, vultr or generic)"
echo "--provider is required ($AVAILABLE_PROVIDERS)"
exit 1
elif [[ \
"${provider}" != "ami" && \
@@ -106,9 +107,10 @@ elif [[ \
"${provider}" != "ec2" && \
"${provider}" != "exoscale" && \
"${provider}" != "galaxygate" && \
"${provider}" != "digitalocean" && \
"${provider}" != "gce" && \
"${provider}" != "hetzner" && \
"${provider}" != "interox" && \
"${provider}" != "interox-image" && \
"${provider}" != "lightsail" && \
"${provider}" != "linode" && \
"${provider}" != "linode-stackscript" && \
@@ -117,12 +119,14 @@ elif [[ \
"${provider}" != "ovh" && \
"${provider}" != "rosehosting" && \
"${provider}" != "scaleway" && \
"${provider}" != "time4vps" && \
"${provider}" != "time4vps-image" && \
"${provider}" != "upcloud" && \
"${provider}" != "upcloud-image" && \
"${provider}" != "vultr" && \
"${provider}" != "generic" \
]]; then
echo "--provider must be one of: azure, cloudscale.ch, contabo, digitalocean, ec2, exoscale, galaxygate, gce, hetzner, lightsail, linode, netcup, ovh, rosehosting, scaleway, upcloud, vultr or generic"
echo "--provider must be one of: $AVAILABLE_PROVIDERS"
exit 1
fi
@@ -199,6 +203,10 @@ fi
# NOTE: this install script only supports 3.x and above
echo "=> Installing version ${version} (this takes some time) ..."
mkdir -p /etc/cloudron
# this file is used >= 4.2
echo "${provider}" > /etc/cloudron/PROVIDER
# this file is unused <= 4.2 and exists to make legacy installations work. the start script will remove this file anyway
cat > "/etc/cloudron/cloudron.conf" <<CONF_END
{
"apiServerOrigin": "${apiServerOrigin}",
@@ -214,6 +222,10 @@ if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" &>> "${LOG_FILE}"; then
exit 1
fi
# only needed for >= 4.2
mysql -uroot -ppassword -e "REPLACE INTO box.settings (name, value) VALUES ('api_server_origin', '${apiServerOrigin}');" 2>/dev/null
mysql -uroot -ppassword -e "REPLACE INTO box.settings (name, value) VALUES ('web_server_origin', '${webServerOrigin}');" 2>/dev/null
echo -n "=> Waiting for cloudron to be ready (this takes some time) ..."
while true; do
echo -n "."
+4 -4
View File
@@ -52,7 +52,7 @@ if [[ "`df --output="avail" / | sed -n 2p`" -lt "10240" ]]; then
echo ""
df -h
echo ""
echo "To recover from a full disk, follow the guide at https://cloudron.io/documentation/server/#recovery-after-disk-full"
echo "To recover from a full disk, follow the guide at https://cloudron.io/documentation/troubleshooting/#recovery-after-disk-full"
exit 1
fi
@@ -68,8 +68,8 @@ echo -n "Generating Cloudron Support stats..."
# clear file
rm -rf $OUT
echo -e $LINE"cloudron.conf"$LINE >> $OUT
cat /etc/cloudron/cloudron.conf &>> $OUT
echo -e $LINE"PROVIDER"$LINE >> $OUT
cat /etc/cloudron/PROVIDER &>> $OUT || true
echo -e $LINE"Docker container"$LINE >> $OUT
if ! timeout --kill-after 10s 15s docker ps -a &>> $OUT 2>&1; then
@@ -107,7 +107,7 @@ if [[ "${enableSSH}" == "true" ]]; then
permit_root_login=$(grep -q ^PermitRootLogin.*yes /etc/ssh/sshd_config && echo "yes" || echo "no")
# support.js uses similar logic
if $(grep -q "ec2\|lightsail\|ami" /etc/cloudron/cloudron.conf); then
if $(grep -q "ec2\|lightsail\|ami" /etc/cloudron/PROVIDER); then
ssh_user="ubuntu"
keys_file="/home/ubuntu/.ssh/authorized_keys"
else
+3 -3
View File
@@ -108,8 +108,6 @@ systemctl restart unbound
# ensure cloudron-syslog runs
systemctl restart cloudron-syslog
$json -f /etc/cloudron/cloudron.conf -I -e "delete this.edition" # can be removed after 4.0
echo "==> Configuring sudoers"
rm -f /etc/sudoers.d/${USER}
cp "${script_dir}/start/sudoers" /etc/sudoers.d/${USER}
@@ -124,8 +122,8 @@ 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
rm -f "${PLATFORM_DATA_DIR}/logrotate.d/"*
cp "${script_dir}/start/logrotate/"* "${PLATFORM_DATA_DIR}/logrotate.d/"
rm -f "${PLATFORM_DATA_DIR}/logrotate.d/box-logrotate" "${PLATFORM_DATA_DIR}/logrotate.d/app-logrotate" # remove pre 3.6 config files
# logrotate files have to be owned by root, this is here to fixup existing installations where we were resetting the owner to yellowtent
chown root:root "${PLATFORM_DATA_DIR}/logrotate.d/"
@@ -173,6 +171,8 @@ mysql -u root -p${mysql_root_password} -e 'CREATE DATABASE IF NOT EXISTS box'
echo "==> Migrating data"
(cd "${BOX_SRC_DIR}" && BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@127.0.0.1/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up)
rm -f /etc/cloudron/cloudron.conf
if [[ ! -f "${BOX_DATA_DIR}/dhparams.pem" ]]; then
echo "==> Generating dhparams (takes forever)"
openssl dhparam -out "${BOX_DATA_DIR}/dhparams.pem" 2048
+17 -2
View File
@@ -240,8 +240,23 @@ LoadPlugin write_graphite
Interactive false
Import "df"
# <Module df>
# </Module>
Import "du"
<Module du>
<Path>
Instance maildata
Dir "/home/yellowtent/boxdata/mail"
</Path>
<Path>
Instance boxdata
Dir "/home/yellowtent/boxdata"
Exclude "mail"
</Path>
<Path>
Instance platformdata
Dir "/home/yellowtent/platformdata"
</Path>
</Module>
</Plugin>
<Plugin write_graphite>
+1
View File
@@ -21,6 +21,7 @@ def read():
except:
continue
# type comes from https://github.com/collectd/collectd/blob/master/src/types.db
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
+79
View File
@@ -0,0 +1,79 @@
import collectd,os,subprocess,sys,re,time
# https://www.programcreek.com/python/example/106897/collectd.register_read
PATHS = [] # { name, dir, exclude }
INTERVAL = 60 * 60 * 12 # twice a day. change values in docker-graphite if you change this
def du(pathinfo):
cmd = 'timeout 1800 du -Dsb "{}"'.format(pathinfo['dir'])
if pathinfo['exclude'] != '':
cmd += ' --exclude "{}"'.format(pathinfo['exclude'])
collectd.info('computing size with command: %s' % cmd);
try:
size = subprocess.check_output(cmd, shell=True).split()[0].decode('utf-8')
collectd.info('\tsize of %s is %s (time: %i)' % (pathinfo['dir'], size, int(time.time())))
return size
except Exception as e:
collectd.info('\terror getting the size of %s: %s' % (pathinfo['dir'], str(e)))
return 0
def parseSize(size):
units = {"B": 1, "KB": 10**3, "MB": 10**6, "GB": 10**9, "TB": 10**12}
number, unit, _ = re.split('([a-zA-Z]+)', size.upper())
return int(float(number)*units[unit])
def dockerSize():
# use --format '{{json .}}' to dump the string. '{{if eq .Type "Images"}}{{.Size}}{{end}}' still creates newlines
cmd = 'timeout 1800 docker system df --format "{{.Size}}" | head -n1'
try:
size = subprocess.check_output(cmd, shell=True).strip().decode('utf-8')
collectd.info('size of docker images is %s (%s) (time: %i)' % (size, parseSize(size), int(time.time())))
return parseSize(size)
except Exception as e:
collectd.info('error getting docker images size : %s' % str(e))
return 0
# configure is called for each module block. this is called before init
def configure(config):
global PATHS
for child in config.children:
if child.key != 'Path':
collectd.info('du plugin: Unknown config key "%s"' % key)
continue
pathinfo = { 'name': '', 'dir': '', 'exclude': '' }
for node in child.children:
if node.key == 'Instance':
pathinfo['name'] = node.values[0]
elif node.key == 'Dir':
pathinfo['dir'] = node.values[0]
elif node.key == 'Exclude':
pathinfo['exclude'] = node.values[0]
PATHS.append(pathinfo);
collectd.info('du plugin: monitoring %s' % pathinfo['dir']);
def init():
global PATHS
collectd.info('custom du plugin initialized with %s %s' % (PATHS, sys.version))
def read():
for pathinfo in PATHS:
size = du(pathinfo)
# type comes from https://github.com/collectd/collectd/blob/master/src/types.db
val = collectd.Values(type='capacity', plugin='du', plugin_instance=pathinfo['name'])
val.dispatch(values=[size], type_instance='usage')
size = dockerSize()
val = collectd.Values(type='capacity', plugin='du', plugin_instance='docker')
val.dispatch(values=[size], type_instance='usage')
collectd.register_init(init)
collectd.register_config(configure)
collectd.register_read(read, INTERVAL)
-18
View File
@@ -1,18 +0,0 @@
# logrotate config for app, crash, addon and task logs
# man 7 glob
/home/yellowtent/platformdata/logs/[!t][!a][!s][!k][!s]/*.log {
# only keep one rotated file, we currently do not send that over the api
rotate 1
size 10M
# we never compress so we can simply tail the files
nocompress
copytruncate
}
/home/yellowtent/platformdata/logs/tasks/*.log {
monthly
rotate 0
missingok
}
+2 -1
View File
@@ -1,7 +1,8 @@
# logrotate config for box logs
# keep upto 5 logs of size 10M each
/home/yellowtent/platformdata/logs/box.log {
rotate 10
rotate 5
size 10M
# we never compress so we can simply tail the files
nocompress
+31
View File
@@ -0,0 +1,31 @@
# logrotate config for app, crash, addon and task logs
# man 7 glob
/home/yellowtent/platformdata/logs/graphite/*.log
/home/yellowtent/platformdata/logs/mail/*.log
/home/yellowtent/platformdata/logs/mysql/*.log
/home/yellowtent/platformdata/logs/mongodb/*.log
/home/yellowtent/platformdata/logs/postgresql/*.log
/home/yellowtent/platformdata/logs/sftp/*.log
/home/yellowtent/platformdata/logs/redis-*/*.log
/home/yellowtent/platformdata/logs/crash/*.log
/home/yellowtent/platformdata/logs/updater/*.log {
# only keep one rotated file, we currently do not send that over the api
rotate 1
size 10M
missingok
# we never compress so we can simply tail the files
nocompress
copytruncate
}
# keep task logs for a week. the 'nocreate' option ensures empty log files are not
# created post rotation
/home/yellowtent/platformdata/logs/tasks/*.log {
minage 7
daily
rotate 0
missingok
nocreate
}
+2 -1
View File
@@ -27,7 +27,6 @@ exports = module.exports = {
};
var assert = require('assert'),
config = require('./config.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:accesscontrol'),
tokendb = require('./tokendb.js'),
@@ -130,6 +129,8 @@ function validateToken(accessToken, callback) {
if (error && error.reason === UsersError.NOT_FOUND) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401
if (error) return callback(error);
if (!user.active) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401
scopesForUser(user, function (error, userScopes) {
if (error) return callback(error);
+34 -10
View File
@@ -37,7 +37,7 @@ var accesscontrol = require('./accesscontrol.js'),
assert = require('assert'),
async = require('async'),
clients = require('./clients.js'),
config = require('./config.js'),
constants = require('./constants.js'),
ClientsError = clients.ClientsError,
crypto = require('crypto'),
DatabaseError = require('./databaseerror.js'),
@@ -46,6 +46,7 @@ var accesscontrol = require('./accesscontrol.js'),
dockerConnection = docker.connection,
DockerError = docker.DockerError,
fs = require('fs'),
graphs = require('./graphs.js'),
hat = require('./hat.js'),
infra = require('./infra_version.js'),
mail = require('./mail.js'),
@@ -57,6 +58,7 @@ var accesscontrol = require('./accesscontrol.js'),
safe = require('safetydance'),
semver = require('semver'),
settings = require('./settings.js'),
sftp = require('./sftp.js'),
shell = require('./shell.js'),
spawn = require('child_process').spawn,
split = require('split'),
@@ -269,6 +271,10 @@ function restartContainer(serviceName, callback) {
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
docker.startContainer(serviceName, function (error) {
if (error && error.reason === DockerError.NOT_FOUND) {
callback(null); // callback early since rebuilding takes long
return rebuildService(serviceName, function (error) { if (error) console.error(`Unable to rebuild service ${serviceName}`, error); });
}
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
callback(null);
@@ -276,6 +282,24 @@ function restartContainer(serviceName, callback) {
});
}
function rebuildService(serviceName, callback) {
assert.strictEqual(typeof serviceName, 'string');
assert.strictEqual(typeof callback, 'function');
assert(KNOWN_SERVICES[serviceName], `Unknown service ${serviceName}`);
// this attempts to recreate the service docker container if they don't exist but platform infra version is unchanged
// passing an infra version of 'none' will not attempt to purge existing data, not sure if this is good or bad
if (serviceName === 'mongodb') return startMongodb({ version: 'none' }, callback);
if (serviceName === 'postgresql') return startPostgresql({ version: 'none' }, callback);
if (serviceName === 'mysql') return startMysql({ version: 'none' }, callback);
if (serviceName === 'sftp') return sftp.startSftp({ version: 'none' }, callback);
if (serviceName === 'graphite') return graphs.startGraphite({ version: 'none' }, callback);
// nothing to rebuild for now
callback();
}
function getServiceDetails(containerName, tokenEnvName, callback) {
assert.strictEqual(typeof containerName, 'string');
assert.strictEqual(typeof tokenEnvName, 'string');
@@ -364,7 +388,7 @@ function getService(serviceName, callback) {
}
KNOWN_SERVICES[serviceName].status(function (error, result) {
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
if (error) return callback(error);
tmp.status = result.status;
tmp.memoryUsed = result.memoryUsed;
@@ -620,7 +644,7 @@ function importDatabase(addon, callback) {
if (!error) return iteratorCallback();
debug(`importDatabase: Error importing ${addon} of app ${app.id}. Marking as errored`, error);
appdb.update(app.id, { installationState: appdb.ISTATE_ERROR, installationProgress: error.message }, iteratorCallback);
appdb.update(app.id, { installationState: apps.ISTATE_ERROR, error: { message: error.message } }, iteratorCallback);
});
}, callback);
});
@@ -685,7 +709,7 @@ function getEnvironment(app, callback) {
appdb.getAddonConfigByAppId(app.id, function (error, result) {
if (error) return callback(error);
if (app.manifest.addons['docker']) result.push({ name: 'DOCKER_HOST', value: `tcp://172.18.0.1:${config.get('dockerProxyPort')}` });
if (app.manifest.addons['docker']) result.push({ name: 'DOCKER_HOST', value: `tcp://172.18.0.1:${constants.DOCKER_PROXY_PORT}` });
return callback(null, result.map(function (e) { return e.name + '=' + e.value; }));
});
@@ -800,7 +824,7 @@ function setupOauth(app, options, callback) {
var env = [
{ name: `${envPrefix}OAUTH_CLIENT_ID`, value: result.id },
{ name: `${envPrefix}OAUTH_CLIENT_SECRET`, value: result.clientSecret },
{ name: `${envPrefix}OAUTH_ORIGIN`, value: config.adminOrigin() }
{ name: `${envPrefix}OAUTH_ORIGIN`, value: settings.adminOrigin() }
];
debugApp(app, 'Setting oauth addon config to %j', env);
@@ -876,8 +900,8 @@ function setupLdap(app, options, callback) {
var env = [
{ name: `${envPrefix}LDAP_SERVER`, value: '172.18.0.1' },
{ name: `${envPrefix}LDAP_PORT`, value: '' + config.get('ldapPort') },
{ name: `${envPrefix}LDAP_URL`, value: 'ldap://172.18.0.1:' + config.get('ldapPort') },
{ name: `${envPrefix}LDAP_PORT`, value: '' + constants.LDAP_PORT },
{ name: `${envPrefix}LDAP_URL`, value: 'ldap://172.18.0.1:' + constants.LDAP_PORT },
{ name: `${envPrefix}LDAP_USERS_BASE_DN`, value: 'ou=users,dc=cloudron' },
{ name: `${envPrefix}LDAP_GROUPS_BASE_DN`, value: 'ou=groups,dc=cloudron' },
{ name: `${envPrefix}LDAP_BIND_DN`, value: 'cn='+ app.id + ',ou=apps,dc=cloudron' },
@@ -1779,7 +1803,7 @@ function statusSftp(callback) {
assert.strictEqual(typeof callback, 'function');
docker.inspect('sftp', function (error, container) {
if (error && error.reason === DockerError.NOT_FOUND) return callback(new AddonsError(AddonsError.NOT_ACTIVE, error));
if (error && error.reason === DockerError.NOT_FOUND) return callback(null, { status: exports.SERVICE_STATUS_STOPPED });
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
docker.memoryUsage('sftp', function (error, result) {
@@ -1800,10 +1824,10 @@ function statusGraphite(callback) {
assert.strictEqual(typeof callback, 'function');
docker.inspect('graphite', function (error, container) {
if (error && error.reason === DockerError.NOT_FOUND) return callback(new AddonsError(AddonsError.NOT_ACTIVE, error));
if (error && error.reason === DockerError.NOT_FOUND) return callback(null, { status: exports.SERVICE_STATUS_STOPPED });
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
request.get('http://127.0.0.1:8417', { timeout: 3000 }, function (error, response) {
request.get('http://127.0.0.1:8417/graphite-web/dashboard', { timeout: 3000 }, function (error, response) {
if (error) return callback(null, { status: exports.SERVICE_STATUS_STARTING, error: `Error waiting for graphite: ${error.message}` });
if (response.statusCode !== 200) return callback(null, { status: exports.SERVICE_STATUS_STARTING, error: `Error waiting for graphite. Status code: ${response.statusCode} message: ${response.body.message}` });
+3 -3
View File
@@ -96,7 +96,7 @@ server {
<% if ( endpoint === 'admin' ) { -%>
# CSP headers for the admin/dashboard resources
add_header Content-Security-Policy "default-src 'none'; connect-src wss: https: 'self' *.cloudron.io; script-src https: 'self' 'unsafe-inline' 'unsafe-eval'; img-src * data:; style-src https: 'unsafe-inline'; object-src 'none'; font-src https: 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self';";
add_header Content-Security-Policy "default-src 'none'; frame-src 'self' cloudron.io *.cloudron.io; connect-src wss: https: 'self' *.cloudron.io; script-src https: 'self' 'unsafe-inline' 'unsafe-eval'; img-src * data:; style-src https: 'unsafe-inline'; object-src 'none'; font-src https: 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self';";
<% } -%>
proxy_http_version 1.1;
@@ -163,9 +163,9 @@ server {
client_max_body_size 0;
}
# graphite paths (uncomment block below and visit /graphite/index.html)
# graphite paths (uncomment block below and visit /graphite-web/dashboard)
# remember to comment out the CSP policy as well to access the graphite dashboard
# location ~ ^/(graphite|content|metrics|dashboard|render|browser|composer)/ {
# location ~ ^/graphite-web/ {
# proxy_pass http://127.0.0.1:8417;
# client_max_body_size 1m;
# }
+18 -89
View File
@@ -21,33 +21,9 @@ exports = module.exports = {
getAppIdByAddonConfigValue: getAppIdByAddonConfigValue,
setHealth: setHealth,
setInstallationCommand: setInstallationCommand,
setRunCommand: setRunCommand,
setTask: setTask,
getAppStoreIds: getAppStoreIds,
// installation codes (keep in sync in UI)
ISTATE_PENDING_INSTALL: 'pending_install', // installs and fresh reinstalls
ISTATE_PENDING_CLONE: 'pending_clone', // clone
ISTATE_PENDING_CONFIGURE: 'pending_configure', // config (location, port) changes and on infra update
ISTATE_PENDING_UNINSTALL: 'pending_uninstall', // uninstallation
ISTATE_PENDING_RESTORE: 'pending_restore', // restore to previous backup or on upgrade
ISTATE_PENDING_UPDATE: 'pending_update', // update from installed state preserving data
ISTATE_PENDING_FORCE_UPDATE: 'pending_force_update', // update from any state preserving data
ISTATE_PENDING_BACKUP: 'pending_backup', // backup the app
ISTATE_ERROR: 'error', // error executing last pending_* command
ISTATE_INSTALLED: 'installed', // app is installed
RSTATE_RUNNING: 'running',
RSTATE_PENDING_START: 'pending_start',
RSTATE_PENDING_STOP: 'pending_stop',
RSTATE_STOPPED: 'stopped', // app stopped by us
// run codes (keep in sync in UI)
HEALTH_HEALTHY: 'healthy',
HEALTH_UNHEALTHY: 'unhealthy',
HEALTH_ERROR: 'error',
HEALTH_DEAD: 'dead',
// subdomain table types
SUBDOMAIN_TYPE_PRIMARY: 'primary',
SUBDOMAIN_TYPE_REDIRECT: 'redirect',
@@ -62,10 +38,10 @@ var assert = require('assert'),
safe = require('safetydance'),
util = require('util');
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.installationProgress', 'apps.runState',
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.errorJson', 'apps.runState',
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'subdomains.subdomain AS location', 'subdomains.domain',
'apps.accessRestrictionJson', 'apps.restoreConfigJson', 'apps.oldConfigJson', 'apps.updateConfigJson', 'apps.memoryLimit',
'apps.label', 'apps.tagsJson',
'apps.accessRestrictionJson', 'apps.memoryLimit',
'apps.label', 'apps.tagsJson', 'apps.taskId',
'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
'apps.creationTime', 'apps.updateTime', 'apps.mailboxName', 'apps.enableAutomaticUpdate',
'apps.dataDir', 'apps.ts', 'apps.healthTime' ].join(',');
@@ -81,18 +57,6 @@ function postProcess(result) {
result.manifest = safe.JSON.parse(result.manifestJson);
delete result.manifestJson;
assert(result.oldConfigJson === null || typeof result.oldConfigJson === 'string');
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.tagsJson === null || typeof result.tagsJson === 'string');
result.tags = safe.JSON.parse(result.tagsJson) || [];
delete result.tagsJson;
@@ -142,6 +106,11 @@ function postProcess(result) {
// in the db, we store dataDir as unique/nullable
result.dataDir = result.dataDir || '';
result.error = safe.JSON.parse(result.errorJson);
delete result.errorJson;
result.taskId = result.taskId ? String(result.taskId) : null;
}
function get(id, callback) {
@@ -272,8 +241,7 @@ function add(id, appStoreId, manifest, location, domain, portBindings, data, cal
const accessRestriction = data.accessRestriction || null;
const accessRestrictionJson = JSON.stringify(accessRestriction);
const memoryLimit = data.memoryLimit || 0;
const installationState = data.installationState || exports.ISTATE_PENDING_INSTALL;
const restoreConfigJson = data.restoreConfig ? JSON.stringify(data.restoreConfig) : null; // used when cloning
const installationState = data.installationState || 'pending_install'; // FIXME
const sso = 'sso' in data ? data.sso : null;
const robotsTxt = 'robotsTxt' in data ? data.robotsTxt : null;
const debugModeJson = data.debugMode ? JSON.stringify(data.debugMode) : null;
@@ -286,9 +254,9 @@ function add(id, appStoreId, manifest, location, domain, portBindings, data, cal
queries.push({
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, '
+ 'restoreConfigJson, sso, debugModeJson, robotsTxt, mailboxName, label, tagsJson) '
+ ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, restoreConfigJson,
+ 'sso, debugModeJson, robotsTxt, mailboxName, label, tagsJson) '
+ ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit,
sso, debugModeJson, robotsTxt, mailboxName, label, tagsJson ]
});
@@ -454,7 +422,7 @@ function updateWithConstraints(id, app, constraints, callback) {
var fields = [ ], values = [ ];
for (var p in app) {
if (p === 'manifest' || p === 'oldConfig' || p === 'updateConfig' || p === 'restoreConfig' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode') {
if (p === 'manifest' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode' || p === 'error') {
fields.push(`${p}Json = ?`);
values.push(JSON.stringify(app[p]));
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains' && p !== 'env') {
@@ -477,7 +445,6 @@ function updateWithConstraints(id, app, constraints, callback) {
});
}
// not sure if health should influence runState
function setHealth(appId, health, healthTime, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof health, 'string');
@@ -486,53 +453,15 @@ function setHealth(appId, health, healthTime, callback) {
var values = { health, healthTime };
var constraints = 'AND runState NOT LIKE "pending_%" AND installationState = "installed"';
updateWithConstraints(appId, values, constraints, callback);
updateWithConstraints(appId, values, '', callback);
}
function setInstallationCommand(appId, installationState, values, callback) {
function setTask(appId, values, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof installationState, 'string');
if (typeof values === 'function') {
callback = values;
values = { };
} else {
assert.strictEqual(typeof values, 'object');
assert.strictEqual(typeof callback, 'function');
}
values.installationState = installationState;
values.installationProgress = '';
// 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 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" 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) {
updateWithConstraints(appId, values, 'AND (installationState = "installed" OR installationState = "pending_configure" OR installationState = "error")', callback);
} else {
callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, 'invalid installationState'));
}
}
function setRunCommand(appId, runState, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof runState, 'string');
assert.strictEqual(typeof values, 'object');
assert.strictEqual(typeof callback, 'function');
var values = { runState: runState };
updateWithConstraints(appId, values, 'AND runState NOT LIKE "pending_%" AND installationState = "installed"', callback);
updateWithConstraints(appId, values, 'AND taskId IS NULL', callback);
}
function getAppStoreIds(callback) {
+12 -12
View File
@@ -36,16 +36,16 @@ function setHealth(app, health, callback) {
let now = new Date(), healthTime = app.healthTime, curHealth = app.health;
if (health === appdb.HEALTH_HEALTHY) {
if (health === apps.HEALTH_HEALTHY) {
healthTime = now;
if (curHealth && curHealth !== appdb.HEALTH_HEALTHY) { // app starts out with null health
if (curHealth && curHealth !== apps.HEALTH_HEALTHY) { // app starts out with null health
debugApp(app, 'app switched from %s to healthy', curHealth);
// do not send mails for dev apps
if (!app.debugMode) eventlog.add(eventlog.ACTION_APP_UP, auditSource.HEALTH_MONITOR, { app: app });
}
} else if (Math.abs(now - healthTime) > UNHEALTHY_THRESHOLD) {
if (curHealth === appdb.HEALTH_HEALTHY) {
if (curHealth === apps.HEALTH_HEALTHY) {
debugApp(app, 'marking as unhealthy since not seen for more than %s minutes', UNHEALTHY_THRESHOLD/(60 * 1000));
// do not send mails for dev apps
@@ -72,7 +72,7 @@ function checkAppHealth(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (app.installationState !== appdb.ISTATE_INSTALLED || app.runState !== appdb.RSTATE_RUNNING) {
if (app.installationState !== apps.ISTATE_INSTALLED || app.runState !== apps.RSTATE_RUNNING) {
debugApp(app, 'skipped. istate:%s rstate:%s', app.installationState, app.runState);
return callback(null);
}
@@ -82,34 +82,34 @@ function checkAppHealth(app, callback) {
docker.inspect(app.containerId, function (error, data) {
if (error || !data || !data.State) {
debugApp(app, 'Error inspecting container');
return setHealth(app, appdb.HEALTH_ERROR, callback);
return setHealth(app, apps.HEALTH_ERROR, callback);
}
if (data.State.Running !== true) {
debugApp(app, 'exited');
return setHealth(app, appdb.HEALTH_DEAD, callback);
return setHealth(app, apps.HEALTH_DEAD, callback);
}
// non-appstore apps may not have healthCheckPath
if (!manifest.healthCheckPath) return setHealth(app, appdb.HEALTH_HEALTHY, callback);
if (!manifest.healthCheckPath) return setHealth(app, apps.HEALTH_HEALTHY, callback);
// poll through docker network instead of nginx to bypass any potential oauth proxy
var healthCheckUrl = 'http://127.0.0.1:' + app.httpPort + manifest.healthCheckPath;
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)
.set('User-Agent', 'Mozilla (CloudronHealth)') // 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);
setHealth(app, apps.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);
setHealth(app, apps.HEALTH_UNHEALTHY, callback);
} else {
setHealth(app, appdb.HEALTH_HEALTHY, callback);
setHealth(app, apps.HEALTH_HEALTHY, callback);
}
});
});
@@ -187,7 +187,7 @@ function processApp(callback) {
if (error) console.error(error);
var alive = result
.filter(function (a) { return a.installationState === appdb.ISTATE_INSTALLED && a.runState === appdb.RSTATE_RUNNING && a.health === appdb.HEALTH_HEALTHY; })
.filter(function (a) { return a.installationState === apps.ISTATE_INSTALLED && a.runState === apps.RSTATE_RUNNING && a.health === apps.HEALTH_HEALTHY; })
.map(function (a) { return (a.location || 'naked_domain') + '|' + a.manifest.id; }).join(', ');
debug('apps alive: [%s]', alive);
+208 -143
View File
@@ -42,6 +42,7 @@ exports = module.exports = {
getAppConfig: getAppConfig,
getDataDir: getDataDir,
getIconPath: getIconPath,
downloadFile: downloadFile,
uploadFile: uploadFile,
@@ -49,20 +50,48 @@ exports = module.exports = {
PORT_TYPE_TCP: 'tcp',
PORT_TYPE_UDP: 'udp',
// error codes
ETASK_STOPPED: 'task_stopped', // user stopped a task
ETASK_CRASHED: 'task_crashed', // apptask crashed
// installation codes (keep in sync in UI)
ISTATE_PENDING_INSTALL: 'pending_install', // installs and fresh reinstalls
ISTATE_PENDING_CLONE: 'pending_clone', // clone
ISTATE_PENDING_CONFIGURE: 'pending_configure', // config (location, port) changes and on infra update
ISTATE_PENDING_UNINSTALL: 'pending_uninstall', // uninstallation
ISTATE_PENDING_RESTORE: 'pending_restore', // restore to previous backup or on upgrade
ISTATE_PENDING_UPDATE: 'pending_update', // update from installed state preserving data
ISTATE_PENDING_BACKUP: 'pending_backup', // backup the app. this is state because it blocks other operations
ISTATE_ERROR: 'error', // error executing last pending_* command
ISTATE_INSTALLED: 'installed', // app is installed
// run states
RSTATE_RUNNING: 'running',
RSTATE_PENDING_START: 'pending_start',
RSTATE_PENDING_STOP: 'pending_stop',
RSTATE_STOPPED: 'stopped', // app stopped by us
// health states (keep in sync in UI)
HEALTH_HEALTHY: 'healthy',
HEALTH_UNHEALTHY: 'unhealthy',
HEALTH_ERROR: 'error',
HEALTH_DEAD: 'dead',
// exported for testing
_validatePortBindings: validatePortBindings,
_validateAccessRestriction: validateAccessRestriction,
_translatePortBindings: translatePortBindings
_translatePortBindings: translatePortBindings,
_MOCK_GET_BY_IP_APP_ID: ''
};
var appdb = require('./appdb.js'),
appstore = require('./appstore.js'),
AppstoreError = require('./appstore.js').AppstoreError,
appTaskManager = require('./apptaskmanager.js'),
assert = require('assert'),
async = require('async'),
backups = require('./backups.js'),
BackupsError = backups.BackupsError,
config = require('./config.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:apps'),
@@ -81,10 +110,11 @@ var appdb = require('./appdb.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
semver = require('semver'),
settings = require('./settings.js'),
spawn = require('child_process').spawn,
split = require('split'),
superagent = require('superagent'),
taskmanager = require('./taskmanager.js'),
tasks = require('./tasks.js'),
TransformStream = require('stream').Transform,
updateChecker = require('./updatechecker.js'),
util = require('util'),
@@ -94,9 +124,10 @@ var appdb = require('./appdb.js'),
// http://dustinsenos.com/articles/customErrorsInNode
// http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
function AppsError(reason, errorOrMessage) {
function AppsError(reason, errorOrMessage, details) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
assert(typeof details === 'object' || typeof details === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
@@ -111,6 +142,8 @@ function AppsError(reason, errorOrMessage) {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
this.details = details || {};
}
util.inherits(AppsError, Error);
AppsError.INTERNAL_ERROR = 'Internal Error';
@@ -119,11 +152,9 @@ AppsError.ALREADY_EXISTS = 'Already Exists';
AppsError.NOT_FOUND = 'Not Found';
AppsError.BAD_FIELD = 'Bad Field';
AppsError.BAD_STATE = 'Bad State';
AppsError.PORT_RESERVED = 'Port Reserved';
AppsError.PORT_CONFLICT = 'Port Conflict';
AppsError.PLAN_LIMIT = 'Plan Limit';
AppsError.ACCESS_DENIED = 'Access denied';
AppsError.BAD_CERTIFICATE = 'Invalid certificate';
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
// validate the port bindings
function validatePortBindings(portBindings, manifest) {
@@ -149,10 +180,10 @@ function validatePortBindings(portBindings, manifest) {
2004, /* graphite (lo) */
2020, /* mail server */
2514, /* cloudron-syslog (lo) */
config.get('port'), /* app server (lo) */
config.get('sysadminPort'), /* sysadmin app server (lo) */
config.get('smtpPort'), /* internal smtp port (lo) */
config.get('ldapPort'), /* ldap server (lo) */
constants.PORT, /* app server (lo) */
constants.SYSADMIN_PORT, /* sysadmin app server (lo) */
constants.INTERNAL_SMTP_PORT, /* internal smtp port (lo) */
constants.LDAP_PORT,
3306, /* mysql (lo) */
4190, /* managesieve */
8000, /* ESXi monitoring */
@@ -162,13 +193,12 @@ function validatePortBindings(portBindings, manifest) {
if (!portBindings) return null;
for (let portName in portBindings) {
if (!/^[a-zA-Z0-9_]+$/.test(portName)) return new AppsError(AppsError.BAD_FIELD, `${portName} is not a valid environment variable`);
if (!/^[a-zA-Z0-9_]+$/.test(portName)) return new AppsError(AppsError.BAD_FIELD, `${portName} is not a valid environment variable`, { field: 'portBindings', portName: portName });
const hostPort = portBindings[portName];
if (!Number.isInteger(hostPort)) return new AppsError(AppsError.BAD_FIELD, `${hostPort} is not an integer`);
if (RESERVED_PORTS.indexOf(hostPort) !== -1) return new AppsError(AppsError.PORT_RESERVED, String(hostPort));
if (hostPort <= 1023 || hostPort > 65535) return new AppsError(AppsError.BAD_FIELD, `${hostPort} is not in permitted range`);
if (!Number.isInteger(hostPort)) return new AppsError(AppsError.BAD_FIELD, `${hostPort} is not an integer`, { field: 'portBindings', portName: portName });
if (RESERVED_PORTS.indexOf(hostPort) !== -1) return new AppsError(AppsError.BAD_FIELD, `Port ${hostPort} is reserved.`, { field: 'portBindings', portName: portName });
if (hostPort <= 1023 || hostPort > 65535) return new AppsError(AppsError.BAD_FIELD, `${hostPort} is not in permitted range`, { field: 'portBindings', portName: portName });
}
// it is OK if there is no 1-1 mapping between values in manifest.tcpPorts and portBindings. missing values implies
@@ -176,7 +206,7 @@ function validatePortBindings(portBindings, manifest) {
const tcpPorts = manifest.tcpPorts || { };
const udpPorts = manifest.udpPorts || { };
for (let portName in portBindings) {
if (!(portName in tcpPorts) && !(portName in udpPorts)) return new AppsError(AppsError.BAD_FIELD, `Invalid portBindings ${portName}`);
if (!(portName in tcpPorts) && !(portName in udpPorts)) return new AppsError(AppsError.BAD_FIELD, `Invalid portBindings ${portName}`, { field: 'portBindings', portName: portName });
}
return null;
@@ -251,7 +281,7 @@ function validateRobotsTxt(robotsTxt) {
if (robotsTxt === null) return null;
// this is the nginx limit on inline strings. if we really hit this, we have to generate a file
if (robotsTxt.length > 4096) return new AppsError(AppsError.BAD_FIELD, 'robotsTxt must be less than 4096');
if (robotsTxt.length > 4096) return new AppsError(AppsError.BAD_FIELD, 'robotsTxt must be less than 4096', { field: 'robotsTxt' });
// TODO: validate the robots file? we escape the string when templating the nginx config right now
@@ -269,26 +299,26 @@ function validateBackupFormat(format) {
function validateLabel(label) {
if (label === null) return null;
if (label.length > 128) return new AppsError(AppsError.BAD_FIELD, 'label must be less than 128');
if (label.length > 128) return new AppsError(AppsError.BAD_FIELD, 'label must be less than 128', { field: 'label' });
return null;
}
function validateTags(tags) {
if (!Array.isArray(tags)) return new AppsError(AppsError.BAD_FIELD, 'tags must be an array of strings');
if (tags.length > 64) return new AppsError(AppsError.BAD_FIELD, 'Can only set up to 64 tags');
if (!Array.isArray(tags)) return new AppsError(AppsError.BAD_FIELD, 'tags must be an array of strings', { field: 'tags' });
if (tags.length > 64) return new AppsError(AppsError.BAD_FIELD, 'Can only set up to 64 tags', { field: 'tags' });
if (tags.some(tag => (!tag || typeof tag !== 'string'))) return new AppsError(AppsError.BAD_FIELD, 'tags must be an array of non-empty strings');
if (tags.some(tag => tag.length > 128)) return new AppsError(AppsError.BAD_FIELD, 'tag must be less than 128');
if (tags.some(tag => (!tag || typeof tag !== 'string'))) return new AppsError(AppsError.BAD_FIELD, 'tags must be an array of non-empty strings', { field: 'tags' });
if (tags.some(tag => tag.length > 128)) return new AppsError(AppsError.BAD_FIELD, 'tag must be less than 128', { field: 'tags' });
return null;
}
function validateEnv(env) {
for (let key in env) {
if (key.length > 512) return new AppsError(AppsError.BAD_FIELD, 'Max env var key length is 512');
if (key.length > 512) return new AppsError(AppsError.BAD_FIELD, 'Max env var key length is 512', { field: 'env', env: env });
// http://pubs.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap08.html
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) return new AppsError(AppsError.BAD_FIELD, `"${key}" is not a valid environment variable`);
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) return new AppsError(AppsError.BAD_FIELD, `"${key}" is not a valid environment variable`, { field: 'env', env: env });
}
return null;
@@ -297,56 +327,61 @@ function validateEnv(env) {
function validateDataDir(dataDir) {
if (dataDir === '') return null; // revert back to default dataDir
if (path.resolve(dataDir) !== dataDir) return new AppsError(AppsError.BAD_FIELD, 'dataDir must be an absolute path');
if (path.resolve(dataDir) !== dataDir) return new AppsError(AppsError.BAD_FIELD, 'dataDir must be an absolute path', { field: 'dataDir' });
// nfs shares will have the directory mounted already
let stat = safe.fs.lstatSync(dataDir);
if (stat) {
if (!stat.isDirectory()) return new AppsError(AppsError.BAD_FIELD, `dataDir ${dataDir} is not a directory`);
if (!stat.isDirectory()) return new AppsError(AppsError.BAD_FIELD, `dataDir ${dataDir} is not a directory`, { field: 'dataDir' });
let entries = safe.fs.readdirSync(dataDir);
if (!entries) return new AppsError(AppsError.BAD_FIELD, `dataDir ${dataDir} could not be listed`);
if (entries.length !== 0) return new AppsError(AppsError.BAD_FIELD, `dataDir ${dataDir} is not empty`);
if (!entries) return new AppsError(AppsError.BAD_FIELD, `dataDir ${dataDir} could not be listed`, { field: 'dataDir' });
if (entries.length !== 0) return new AppsError(AppsError.BAD_FIELD, `dataDir ${dataDir} is not empty`, { field: 'dataDir' });
}
// backup logic relies on paths not overlapping (because it recurses)
if (dataDir.startsWith(paths.APPS_DATA_DIR)) return new AppsError(AppsError.BAD_FIELD, `dataDir ${dataDir} cannot be inside apps data`);
if (dataDir.startsWith(paths.APPS_DATA_DIR)) return new AppsError(AppsError.BAD_FIELD, `dataDir ${dataDir} cannot be inside apps data`, { field: 'dataDir' });
// if we made it this far, it cannot start with any of these realistically
const fhs = [ '/bin', '/boot', '/etc', '/lib', '/lib32', '/lib64', '/proc', '/run', '/sbin', '/tmp', '/usr' ];
if (fhs.some((p) => dataDir.startsWith(p))) return new AppsError(AppsError.BAD_FIELD, `dataDir ${dataDir} cannot be placed inside this location`);
if (fhs.some((p) => dataDir.startsWith(p))) return new AppsError(AppsError.BAD_FIELD, `dataDir ${dataDir} cannot be placed inside this location`, { field: 'dataDir' });
return null;
}
function getDuplicateErrorDetails(error, location, domainObject, portBindings, alternateDomains) {
assert.strictEqual(error.reason, DatabaseError.ALREADY_EXISTS);
function getDuplicateErrorDetails(errorMessage, location, domainObject, portBindings, alternateDomains) {
assert.strictEqual(typeof errorMessage, 'string');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof portBindings, 'object');
assert(Array.isArray(alternateDomains));
var match = error.message.match(/ER_DUP_ENTRY: Duplicate entry '(.*)' for key '(.*)'/);
var match = errorMessage.match(/ER_DUP_ENTRY: Duplicate entry '(.*)' for key '(.*)'/);
if (!match) {
debug('Unexpected SQL error message.', error);
return new AppsError(AppsError.INTERNAL_ERROR, error);
debug('Unexpected SQL error message.', errorMessage);
return new AppsError(AppsError.INTERNAL_ERROR, new Error(errorMessage));
}
// check if the location or alternateDomains conflicts
if (match[2] === 'subdomain') {
// mysql reports a unique conflict with a dash: eg. domain:example.com subdomain:test => test-example.com
if (match[1] === `${location}-${domainObject.domain}`) return new AppsError(AppsError.ALREADY_EXISTS, `Domain '${domains.fqdn(location, domainObject)}' is in use`);
if (match[1] === `${location}-${domainObject.domain}`) {
return new AppsError(AppsError.ALREADY_EXISTS, `Domain '${domains.fqdn(location, domainObject)}' is in use`, { location: location, domain: domainObject.domain });
}
// check alternateDomains
let tmp = alternateDomains.filter(function (d) {
return match[1] === `${d.subdomain}-${d.domain}`;
});
for (let d of alternateDomains) {
if (match[1] !== `${d.subdomain}-${d.domain}`) continue;
if (tmp.length > 0) return new AppsError(AppsError.ALREADY_EXISTS, `Alternate domain '${tmp[0].subdomain}.${tmp[0].domain}' is in use`);
return new AppsError(AppsError.ALREADY_EXISTS, `Alternate domain '${d.subdomain}.${d.domain}' is in use`, { location: d.subdomain, domain: d.domain });
}
}
// check if any of the port bindings conflict
for (let portName in portBindings) {
if (portBindings[portName] === parseInt(match[1])) return new AppsError(AppsError.PORT_CONFLICT, match[1]);
if (portBindings[portName] === parseInt(match[1])) return new AppsError(AppsError.ALREADY_EXISTS, `Port ${match[1]} is reserved`, { portName });
}
if (match[2] === 'dataDir') {
return new AppsError(AppsError.BAD_FIELD, `Data directory ${match[1]} is in use`, { field: 'dataDir' });
}
return new AppsError(AppsError.ALREADY_EXISTS, `${match[2]} '${match[1]}' is in use`);
@@ -377,7 +412,7 @@ function getDataDir(app, dataDir) {
function removeInternalFields(app) {
return _.pick(app,
'id', 'appStoreId', 'installationState', 'installationProgress', 'runState', 'health',
'id', 'appStoreId', 'installationState', 'error', 'runState', 'health', 'taskId',
'location', 'domain', 'fqdn', 'mailboxName',
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit',
'sso', 'debugMode', 'robotsTxt', 'enableBackup', 'creationTime', 'updateTime', 'ts', 'tags',
@@ -387,7 +422,7 @@ function removeInternalFields(app) {
// non-admins can only see these
function removeRestrictedFields(app) {
return _.pick(app,
'id', 'appStoreId', 'installationState', 'installationProgress', 'runState', 'health',
'id', 'appStoreId', 'installationState', 'error', 'runState', 'health', 'taskId',
'location', 'domain', 'fqdn', 'manifest', 'portBindings', 'iconUrl', 'creationTime', 'ts', 'tags', 'label');
}
@@ -403,6 +438,22 @@ function getIconUrlSync(app) {
return null;
}
function getIconPath(appId, options, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
if (!options.original) {
const userIconPath = `${paths.APP_ICONS_DIR}/${appId}.user.png`;
if (safe.fs.existsSync(userIconPath)) return callback(null, userIconPath);
}
const appstoreIconPath = `${paths.APP_ICONS_DIR}/${appId}.png`;
if (safe.fs.existsSync(appstoreIconPath)) return callback(null, appstoreIconPath);
callback(new AppsError(AppsError.NOT_FOUND, 'No icon'));
}
function postProcess(app, domainObjectMap) {
let result = {};
for (let portName in app.portBindings) {
@@ -488,6 +539,9 @@ function getByIpAddress(ip, callback) {
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
// this is only used by the ldap test. the apps tests still uses proper docker
if (constants.TEST && exports._MOCK_GET_BY_IP_APP_ID) return get(exports._MOCK_GET_BY_IP_APP_ID, callback);
docker.getContainerIdByIp(ip, function (error, containerId) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
@@ -552,7 +606,7 @@ function downloadManifest(appStoreId, manifest, callback) {
var parts = appStoreId.split('@');
var url = config.apiServerOrigin() + '/api/v1/apps/' + parts[0] + (parts[1] ? '/versions/' + parts[1] : '');
var url = settings.apiServerOrigin() + '/api/v1/apps/' + parts[0] + (parts[1] ? '/versions/' + parts[1] : '');
debug('downloading manifest from %s', url);
@@ -569,6 +623,41 @@ function mailboxNameForLocation(location, manifest) {
return (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app';
}
function scheduleTask(appId, args, values, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof args, 'object');
assert.strictEqual(typeof values, 'object');
assert.strictEqual(typeof callback, 'function');
assert(values.installationState || values.runState);
tasks.add(tasks.TASK_APP, [ appId, args ], function (error, taskId) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
values.error = null;
values.taskId = taskId;
appdb.setTask(appId, values, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new AppsError(AppsError.ALREADY_EXISTS, error.message));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE, 'Another task is scheduled for this app')); // could be because app went away OR a taskId exists
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
appTaskManager.scheduleTask(appId, taskId, function (error) {
debug(`scheduleTask: task ${taskId} of $${appId} completed`);
if (error && (error.code === tasks.ECRASHED || error.code === tasks.ESTOPPED)) { // if task crashed, update the error
debug(`Apptask crashed/stopped: ${error.message}`);
const code = error.crashed ? exports.ETASK_CRASHED : exports.ETASK_STOPPED;
appdb.update(appId, { installationState: exports.ISTATE_ERROR, error: { code: code, message: error.message }, taskId: null }, NOOP_CALLBACK);
} else if (values.installationState !== exports.ISTATE_PENDING_UNINSTALL) { // clear out taskId since it's done
appdb.update(appId, { taskId: null }, NOOP_CALLBACK);
}
});
callback(null, { taskId });
});
});
}
function install(data, user, auditSource, callback) {
assert(data && typeof data === 'object');
assert(user && typeof user === 'object');
@@ -640,7 +729,7 @@ function install(data, user, auditSource, callback) {
if (mailboxName) {
error = mail.validateName(mailboxName);
if (error) return callback(error);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message, { field: 'mailboxName' }));
} else {
mailboxName = mailboxNameForLocation(location, manifest);
}
@@ -648,7 +737,7 @@ function install(data, user, auditSource, callback) {
var appId = uuid.v4();
if (icon) {
if (!validator.isBase64(icon)) return callback(new AppsError(AppsError.BAD_FIELD, 'icon is not base64'));
if (!validator.isBase64(icon)) return callback(new AppsError(AppsError.BAD_FIELD, 'icon is not base64', { field: 'icon' }));
if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.png'), Buffer.from(icon, 'base64'))) {
return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving icon:' + safe.error.message));
@@ -660,11 +749,11 @@ function install(data, user, auditSource, callback) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
error = domains.validateHostname(location, domainObject);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Bad location: ' + error.message));
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Bad location: ' + error.message, { field: 'location' }));
if (cert && key) {
error = reverseProxy.validateCertificate(location, domainObject, { cert, key });
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message, { field: 'cert' }));
}
debug('Will install app with id : ' + appId);
@@ -675,7 +764,6 @@ function install(data, user, auditSource, callback) {
sso: sso,
debugMode: debugMode,
mailboxName: mailboxName,
restoreConfig: backupId ? { backupId: backupId, backupFormat: backupFormat } : null,
enableBackup: enableBackup,
enableAutomaticUpdate: enableAutomaticUpdate,
robotsTxt: robotsTxt,
@@ -684,7 +772,7 @@ function install(data, user, auditSource, callback) {
};
appdb.add(appId, appStoreId, manifest, location, domain, translatePortBindings(portBindings, manifest), data, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error, location, domainObject, portBindings, data.alternateDomains));
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error.message, location, domainObject, portBindings, data.alternateDomains));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, error.message));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
@@ -697,15 +785,14 @@ function install(data, user, auditSource, callback) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error setting cert: ' + error.message));
}
taskmanager.restartAppTask(appId);
const restoreConfig = backupId ? { backupId: backupId, backupFormat: backupFormat } : null;
// fetch fresh app object for eventlog
get(appId, function (error, result) {
scheduleTask(appId, { restoreConfig }, { installationState: exports.ISTATE_PENDING_INSTALL }, function (error, result) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId: appId, app: result });
callback(null, { id : appId });
callback(null, { id : appId, taskId: result.taskId });
});
});
});
@@ -722,8 +809,10 @@ function configure(appId, data, user, auditSource, callback) {
get(appId, function (error, app) {
if (error) return callback(error);
if (app.taskId) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState} / ${app.runState}`));
let domain, location, portBindings, values = { installationState: exports.ISTATE_PENDING_CONFIGURE };
let domain, location, portBindings, values = { };
if ('location' in data && 'domain' in data) {
location = values.location = data.location.toLowerCase();
domain = values.domain = data.domain.toLowerCase();
@@ -768,7 +857,7 @@ function configure(appId, data, user, auditSource, callback) {
if ('mailboxName' in data) {
if (data.mailboxName) {
error = mail.validateName(data.mailboxName);
if (error) return callback(error);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message, { field: 'mailboxName' }));
values.mailboxName = data.mailboxName;
} else {
values.mailboxName = mailboxNameForLocation(location, app.manifest);
@@ -808,7 +897,7 @@ function configure(appId, data, user, auditSource, callback) {
if ('icon' in data) {
if (data.icon) {
if (!validator.isBase64(data.icon)) return callback(new AppsError(AppsError.BAD_FIELD, 'icon is not base64'));
if (!validator.isBase64(data.icon)) return callback(new AppsError(AppsError.BAD_FIELD, 'icon is not base64', { field: 'icon' }));
if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.user.png'), Buffer.from(data.icon, 'base64'))) {
return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving icon:' + safe.error.message));
@@ -823,13 +912,13 @@ function configure(appId, data, user, auditSource, callback) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
error = domains.validateHostname(location, domainObject);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Bad location: ' + error.message));
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Bad location: ' + error.message, { field: 'location' }));
// save cert to boxdata/certs. TODO: move this to apptask when we have a real task queue
if ('cert' in data && 'key' in data) {
if (data.cert && data.key) {
error = reverseProxy.validateCertificate(location, domainObject, { cert: data.cert, key: data.key });
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message, { field: 'cert' }));
}
error = reverseProxy.setAppCertificateSync(location, domainObject, { cert: data.cert, key: data.key });
@@ -839,25 +928,15 @@ function configure(appId, data, user, auditSource, callback) {
if ('enableBackup' in data) values.enableBackup = data.enableBackup;
if ('enableAutomaticUpdate' in data) values.enableAutomaticUpdate = data.enableAutomaticUpdate;
values.oldConfig = getAppConfig(app);
debug(`configure: id:${appId}`);
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_CONFIGURE, values, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error, location, domainObject, portBindings, data.alternateDomains));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
scheduleTask(appId, { oldConfig: getAppConfig(app) }, values, function (error, result) {
if (error && error.reason === AppsError.ALREADY_EXISTS) error = getDuplicateErrorDetails(error.message, location, domainObject, portBindings, data.alternateDomains);
if (error) return callback(error);
taskmanager.restartAppTask(appId);
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: result });
// fetch fresh app object for eventlog
get(appId, function (error, result) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: result });
callback(null);
});
callback(null, { taskId: result.taskId });
});
});
});
@@ -873,11 +952,15 @@ function update(appId, data, auditSource, callback) {
get(appId, function (error, app) {
if (error) return callback(error);
if (app.taskId) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState} / ${app.runState}`));
downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) {
if (error) return callback(error);
var updateConfig = { };
var updateConfig = {
skipNotification: data.force,
skipBackup: data.force
};
error = manifestFormat.parse(manifest);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest error:' + error.message));
@@ -904,7 +987,7 @@ function update(appId, data, auditSource, callback) {
if ('icon' in data) {
if (data.icon) {
if (!validator.isBase64(data.icon)) return callback(new AppsError(AppsError.BAD_FIELD, 'icon is not base64'));
if (!validator.isBase64(data.icon)) return callback(new AppsError(AppsError.BAD_FIELD, 'icon is not base64', { field: 'icon' }));
if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.user.png'), Buffer.from(data.icon, 'base64'))) {
return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving icon:' + safe.error.message));
@@ -923,18 +1006,15 @@ function update(appId, data, auditSource, callback) {
updateConfig.memoryLimit = updateConfig.manifest.memoryLimit;
}
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) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
taskmanager.restartAppTask(appId);
scheduleTask(appId, { updateConfig: updateConfig }, { installationState: exports.ISTATE_PENDING_UPDATE }, function (error, result) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId: appId, toManifest: manifest, fromManifest: app.manifest, force: data.force, app: app });
// clear update indicator, if update fails, it will come back through the update checker
updateChecker.resetAppUpdateInfo(appId);
callback(null);
callback(null, { taskId: result.taskId });
});
});
});
@@ -1004,6 +1084,7 @@ function restore(appId, data, auditSource, callback) {
get(appId, function (error, app) {
if (error) return callback(error);
if (app.taskId) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState} / ${app.runState}`));
// for empty or null backupId, use existing manifest to mimic a reinstall
var func = data.backupId ? backups.get.bind(null, data.backupId) : function (next) { return next(null, { manifest: app.manifest }); };
@@ -1019,22 +1100,18 @@ function restore(appId, data, auditSource, callback) {
error = checkManifestConstraints(backupInfo.manifest);
if (error) return callback(error);
var values = {
restoreConfig: data.backupId ? { backupId: data.backupId, backupFormat: backupInfo.format } : null, // when null, apptask simply reinstalls
manifest: backupInfo.manifest,
oldConfig: getAppConfig(app)
let values = {
installationState: exports.ISTATE_PENDING_RESTORE,
manifest: backupInfo.manifest
};
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_RESTORE, values, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
taskmanager.restartAppTask(appId);
const restoreConfig = data.backupId ? { backupId: data.backupId, backupFormat: backupInfo.format, oldManifest: app.manifest } : null; // when null, apptask simply reinstalls
scheduleTask(appId, { restoreConfig }, values, function (error, result) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { app: app, backupId: backupInfo.id, fromManifest: app.manifest, toManifest: backupInfo.manifest });
callback(null);
callback(null, { taskId: result.taskId });
});
});
});
@@ -1104,7 +1181,7 @@ function clone(appId, data, user, auditSource, callback) {
if (mailboxName) {
error = mail.validateName(mailboxName);
if (error) return callback(error);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message, { field: 'mailboxName' }));
} else {
mailboxName = mailboxNameForLocation(location, manifest);
}
@@ -1119,10 +1196,9 @@ function clone(appId, data, user, auditSource, callback) {
var newAppId = uuid.v4();
var data = {
installationState: appdb.ISTATE_PENDING_CLONE,
installationState: exports.ISTATE_PENDING_CLONE,
memoryLimit: app.memoryLimit,
accessRestriction: app.accessRestriction,
restoreConfig: { backupId: backupId, backupFormat: backupInfo.format },
sso: !!app.sso,
mailboxName: mailboxName,
enableBackup: app.enableBackup,
@@ -1131,21 +1207,20 @@ function clone(appId, data, user, auditSource, callback) {
};
appdb.add(newAppId, app.appStoreId, manifest, location, domain, translatePortBindings(portBindings, manifest), data, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error, location, domainObject, portBindings, []));
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error.message, location, domainObject, portBindings, []));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
purchaseApp({ appId: newAppId, appstoreId: app.appStoreId, manifestId: manifest.id }, function (error) {
if (error) return callback(error);
taskmanager.restartAppTask(newAppId);
const restoreConfig = { backupId: backupId, backupFormat: backupInfo.format };
// fetch fresh app object for eventlog
get(appId, function (error, result) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
scheduleTask(newAppId, { restoreConfig }, { installationState: exports.ISTATE_PENDING_CLONE }, function (error, result) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_APP_CLONE, auditSource, { appId: newAppId, oldAppId: appId, backupId: backupId, oldApp: app, newApp: result });
callback(null, { id : newAppId });
callback(null, { id: newAppId, taskId: result.taskId });
});
});
});
@@ -1159,10 +1234,11 @@ function uninstall(appId, auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug('Will uninstall app with id:%s', appId);
debug(`Will uninstall app with id : ${appId}`);
get(appId, function (error, app) {
if (error) return callback(error);
if (app.taskId) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState} / ${app.runState}`));
appstore.unpurchaseApp(appId, { appstoreId: app.appStoreId, manifestId: app.manifest.id }, function (error) {
if (error && error.reason === AppstoreError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
@@ -1171,15 +1247,12 @@ function uninstall(appId, auditSource, callback) {
if (error && error.reason === AppstoreError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
taskmanager.stopAppTask(appId, function () {
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_UNINSTALL, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
scheduleTask(appId, { /* args */ }, { installationState: exports.ISTATE_PENDING_UNINSTALL }, function (error, result) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_APP_UNINSTALL, auditSource, { appId: appId, app: app });
eventlog.add(eventlog.ACTION_APP_UNINSTALL, auditSource, { appId: appId, app: result });
taskmanager.startAppTask(appId, callback);
});
callback(null, { taskId: result.taskId });
});
});
});
@@ -1191,13 +1264,11 @@ function start(appId, callback) {
debug('Will start app with id:%s', appId);
appdb.setRunCommand(appId, appdb.RSTATE_PENDING_START, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
get(appId, function (error, app) {
if (error) return callback(error);
if (app.taskId) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState} / ${app.runState}`));
taskmanager.restartAppTask(appId);
callback(null);
scheduleTask(appId, { /* args */ }, { runState: exports.RSTATE_PENDING_START }, callback);
});
}
@@ -1207,13 +1278,11 @@ function stop(appId, callback) {
debug('Will stop app with id:%s', appId);
appdb.setRunCommand(appId, appdb.RSTATE_PENDING_STOP, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
get(appId, function (error, app) {
if (error) return callback(error);
if (app.taskId) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState} / ${app.runState}`));
taskmanager.restartAppTask(appId);
callback(null);
scheduleTask(appId, { /* args */ }, { runState: exports.RSTATE_PENDING_STOP }, callback);
});
}
@@ -1224,11 +1293,11 @@ function checkManifestConstraints(manifest) {
if (!manifest.dockerImage) return new AppsError(AppsError.BAD_FIELD, 'Missing dockerImage'); // dockerImage is optional in manifest
if (semver.valid(manifest.maxBoxVersion) && semver.gt(config.version(), manifest.maxBoxVersion)) {
if (semver.valid(manifest.maxBoxVersion) && semver.gt(constants.VERSION, manifest.maxBoxVersion)) {
return new AppsError(AppsError.BAD_FIELD, 'Box version exceeds Apps maxBoxVersion');
}
if (semver.valid(manifest.minBoxVersion) && semver.gt(manifest.minBoxVersion, config.version())) {
if (semver.valid(manifest.minBoxVersion) && semver.gt(manifest.minBoxVersion, constants.VERSION)) {
return new AppsError(AppsError.BAD_FIELD, 'App version requires a new platform version');
}
@@ -1246,7 +1315,7 @@ function exec(appId, options, callback) {
get(appId, function (error, app) {
if (error) return callback(error);
if (app.installationState !== appdb.ISTATE_INSTALLED || app.runState !== appdb.RSTATE_RUNNING) {
if (app.installationState !== exports.ISTATE_INSTALLED || app.runState !== exports.RSTATE_RUNNING) {
return callback(new AppsError(AppsError.BAD_STATE, 'App not installed or running'));
}
@@ -1353,22 +1422,18 @@ function backup(appId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
appdb.exists(appId, function (error, exists) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (!exists) return callback(new AppsError(AppsError.NOT_FOUND));
get(appId, function (error, app) {
if (error) return callback(error);
if (app.taskId) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState} / ${app.runState}`));
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_BACKUP, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
scheduleTask(appId, { /* args */ }, { installationState: exports.ISTATE_PENDING_BACKUP }, (error, result) => {
if (error) return callback(error);
taskmanager.restartAppTask(appId);
callback(null);
callback(null, { taskId: result.taskId });
});
});
}
function listBackups(page, perPage, appId, callback) {
assert(typeof page === 'number' && page > 0);
assert(typeof perPage === 'number' && perPage > 0);
@@ -1396,14 +1461,14 @@ function restoreInstalledApps(callback) {
async.map(apps, function (app, iteratorDone) {
backups.getByAppIdPaged(1, 1, app.id, function (error, results) {
var restoreConfig = !error && results.length ? { backupId: results[0].id, backupFormat: results[0].format } : null;
const restoreConfig = !error && results.length ? { backupId: results[0].id, backupFormat: results[0].format, oldManifest: app.manifest } : null;
debug(`marking ${app.fqdn} for restore using restore config ${JSON.stringify(restoreConfig)}`);
appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_RESTORE, { restoreConfig: restoreConfig, oldConfig: getAppConfig(app) }, function (error) {
appdb.update(app.id, { taskId: null }, function (error) { // clear any stale taskId
if (error) debug(`Error marking ${app.fqdn} for restore: ${JSON.stringify(error)}`);
iteratorDone(); // always succeed
scheduleTask(app.id, { restoreConfig }, { installationState: exports.ISTATE_PENDING_RESTORE }, () => iteratorDone()); // always succeed
});
});
}, callback);
@@ -1419,10 +1484,10 @@ function configureInstalledApps(callback) {
async.map(apps, function (app, iteratorDone) {
debug(`marking ${app.fqdn} for reconfigure`);
appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_CONFIGURE, { oldConfig: null }, function (error) {
appdb.update(app.id, { taskId: null }, function (error) { // clear any stale taskId
if (error) debug(`Error marking ${app.fqdn} for reconfigure: ${JSON.stringify(error)}`);
iteratorDone(); // always succeed
scheduleTask(app.id, { oldConfig: getAppConfig(app) }, { installationState: exports.ISTATE_PENDING_CONFIGURE }, () => iteratorDone()); // always succeed
});
}, callback);
});
+22 -21
View File
@@ -27,7 +27,7 @@ exports = module.exports = {
var apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
config = require('./config.js'),
constants = require('./constants.js'),
custom = require('./custom.js'),
debug = require('debug')('box:appstore'),
domains = require('./domains.js'),
@@ -39,6 +39,7 @@ var apps = require('./apps.js'),
semver = require('semver'),
settings = require('./settings.js'),
superagent = require('superagent'),
sysinfo = require('./sysinfo.js'),
users = require('./users.js'),
util = require('util');
@@ -97,7 +98,7 @@ function login(email, password, totpToken, callback) {
totpToken: totpToken
};
const url = config.apiServerOrigin() + '/api/v1/login';
const url = settings.apiServerOrigin() + '/api/v1/login';
superagent.post(url).send(data).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
if (result.statusCode === 401) return callback(new AppstoreError(AppstoreError.ACCESS_DENIED));
@@ -117,7 +118,7 @@ function registerUser(email, password, callback) {
password: password,
};
const url = config.apiServerOrigin() + '/api/v1/register_user';
const url = settings.apiServerOrigin() + '/api/v1/register_user';
superagent.post(url).send(data).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
if (result.statusCode === 409) return callback(new AppstoreError(AppstoreError.ALREADY_EXISTS));
@@ -133,7 +134,7 @@ function getSubscription(callback) {
getCloudronToken(function (error, token) {
if (error) return callback(error);
const url = config.apiServerOrigin() + '/api/v1/subscription';
const url = settings.apiServerOrigin() + '/api/v1/subscription';
superagent.get(url).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
if (result.statusCode === 401) return callback(new AppstoreError(AppstoreError.INVALID_TOKEN));
@@ -160,7 +161,7 @@ function purchaseApp(data, callback) {
getCloudronToken(function (error, token) {
if (error) return callback(error);
const url = `${config.apiServerOrigin()}/api/v1/cloudronapps`;
const url = `${settings.apiServerOrigin()}/api/v1/cloudronapps`;
superagent.post(url).send(data).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
@@ -185,7 +186,7 @@ function unpurchaseApp(appId, data, callback) {
getCloudronToken(function (error, token) {
if (error) return callback(error);
const url = `${config.apiServerOrigin()}/api/v1/cloudronapps/${appId}`;
const url = `${settings.apiServerOrigin()}/api/v1/cloudronapps/${appId}`;
superagent.get(url).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
@@ -279,9 +280,9 @@ function sendAliveStatus(callback) {
};
var data = {
version: config.version(),
adminFqdn: config.adminFqdn(),
provider: config.provider(),
version: constants.VERSION,
adminFqdn: settings.adminFqdn(),
provider: sysinfo.provider(),
backendSettings: backendSettings,
machine: {
cpus: os.cpus(),
@@ -295,7 +296,7 @@ function sendAliveStatus(callback) {
getCloudronToken(function (error, token) {
if (error) return callback(error);
const url = `${config.apiServerOrigin()}/api/v1/alive`;
const url = `${settings.apiServerOrigin()}/api/v1/alive`;
superagent.post(url).send(data).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
@@ -315,9 +316,9 @@ function getBoxUpdate(callback) {
getCloudronToken(function (error, token) {
if (error) return callback(error);
const url = `${config.apiServerOrigin()}/api/v1/boxupdate`;
const url = `${settings.apiServerOrigin()}/api/v1/boxupdate`;
superagent.get(url).query({ accessToken: token, boxVersion: config.version() }).timeout(10 * 1000).end(function (error, result) {
superagent.get(url).query({ accessToken: token, boxVersion: constants.VERSION }).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
if (result.statusCode === 401) return callback(new AppstoreError(AppstoreError.INVALID_TOKEN));
if (result.statusCode === 422) return callback(new AppstoreError(AppstoreError.LICENSE_ERROR, result.body.message));
@@ -326,7 +327,7 @@ function getBoxUpdate(callback) {
var updateInfo = result.body;
if (!semver.valid(updateInfo.version) || semver.gt(config.version(), updateInfo.version)) {
if (!semver.valid(updateInfo.version) || semver.gt(constants.VERSION, updateInfo.version)) {
return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Invalid update version: %s %s', result.statusCode, result.text)));
}
@@ -350,9 +351,9 @@ function getAppUpdate(app, callback) {
getCloudronToken(function (error, token) {
if (error) return callback(error);
const url = `${config.apiServerOrigin()}/api/v1/appupdate`;
const url = `${settings.apiServerOrigin()}/api/v1/appupdate`;
superagent.get(url).query({ accessToken: token, boxVersion: config.version(), appId: app.appStoreId, appVersion: app.manifest.version }).timeout(10 * 1000).end(function (error, result) {
superagent.get(url).query({ accessToken: token, boxVersion: constants.VERSION, appId: app.appStoreId, appVersion: app.manifest.version }).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode === 401) return callback(new AppstoreError(AppstoreError.INVALID_TOKEN));
if (result.statusCode === 422) return callback(new AppstoreError(AppstoreError.LICENSE_ERROR, result.body.message));
@@ -380,7 +381,7 @@ function registerCloudron(data, callback) {
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
const url = `${config.apiServerOrigin()}/api/v1/register_cloudron`;
const url = `${settings.apiServerOrigin()}/api/v1/register_cloudron`;
superagent.post(url).send(data).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
@@ -436,7 +437,7 @@ function registerWithLoginCredentials(options, callback) {
login(options.email, options.password, options.totpToken || '', function (error, result) {
if (error) return callback(error);
registerCloudron({ domain: config.adminDomain(), accessToken: result.accessToken }, callback);
registerCloudron({ domain: settings.adminDomain(), accessToken: result.accessToken }, callback);
});
});
});
@@ -463,7 +464,7 @@ function createTicket(info, callback) {
if (error) console.error('Unable to get app info', error);
if (result) info.app = result;
let url = config.apiServerOrigin() + '/api/v1/ticket';
let url = settings.apiServerOrigin() + '/api/v1/ticket';
info.supportEmail = custom.spec().support.email; // destination address for tickets
@@ -487,8 +488,8 @@ function getApps(callback) {
settings.getUnstableAppsConfig(function (error, unstable) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
const url = `${config.apiServerOrigin()}/api/v1/apps`;
superagent.get(url).query({ accessToken: token, boxVersion: config.version(), unstable: unstable }).timeout(10 * 1000).end(function (error, result) {
const url = `${settings.apiServerOrigin()}/api/v1/apps`;
superagent.get(url).query({ accessToken: token, boxVersion: constants.VERSION, unstable: unstable }).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.INVALID_TOKEN));
if (result.statusCode === 422) return callback(new AppstoreError(AppstoreError.LICENSE_ERROR, result.body.message));
@@ -509,7 +510,7 @@ function getAppVersion(appId, version, callback) {
getCloudronToken(function (error, token) {
if (error) return callback(error);
let url = `${config.apiServerOrigin()}/api/v1/apps/${appId}`;
let url = `${settings.apiServerOrigin()}/api/v1/apps/${appId}`;
if (version !== 'latest') url += `/versions/${version}`;
superagent.get(url).query({ accessToken: token }).timeout(10 * 1000).end(function (error, result) {
+194 -293
View File
@@ -3,8 +3,7 @@
'use strict';
exports = module.exports = {
initialize: initialize,
startTask: startTask,
run: run,
// exported for testing
_reserveHttpPort: reserveHttpPort,
@@ -13,8 +12,8 @@ exports = module.exports = {
_createAppDir: createAppDir,
_deleteAppDir: deleteAppDir,
_verifyManifest: verifyManifest,
_registerSubdomain: registerSubdomain,
_unregisterSubdomain: unregisterSubdomain,
_registerSubdomains: registerSubdomains,
_unregisterSubdomains: unregisterSubdomains,
_waitForDnsPropagation: waitForDnsPropagation
};
@@ -27,10 +26,10 @@ var addons = require('./addons.js'),
async = require('async'),
auditsource = require('./auditsource.js'),
backups = require('./backups.js'),
config = require('./config.js'),
database = require('./database.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:apptask'),
df = require('@sindresorhus/df'),
docker = require('./docker.js'),
domains = require('./domains.js'),
DomainsError = domains.DomainsError,
@@ -46,6 +45,7 @@ var addons = require('./addons.js'),
reverseProxy = require('./reverseproxy.js'),
rimraf = require('rimraf'),
safe = require('safetydance'),
settings = require('./settings.js'),
shell = require('./shell.js'),
superagent = require('superagent'),
sysinfo = require('./sysinfo.js'),
@@ -58,14 +58,6 @@ var COLLECTD_CONFIG_EJS = fs.readFileSync(__dirname + '/collectd.config.ejs', {
LOGROTATE_CONFIG_EJS = fs.readFileSync(__dirname + '/logrotate.ejs', { encoding: 'utf8' }),
CONFIGURE_LOGROTATE_CMD = path.join(__dirname, 'scripts/configurelogrotate.sh');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
database.initialize(callback);
}
function debugApp(app) {
assert.strictEqual(typeof app, 'object');
@@ -120,7 +112,6 @@ function unconfigureReverseProxy(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
// TODO: maybe revoke the cert
reverseProxy.unconfigureApp(app, callback);
}
@@ -200,7 +191,7 @@ function addCollectdProfile(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
var collectdConf = ejs.render(COLLECTD_CONFIG_EJS, { appId: app.id, containerId: app.containerId });
var collectdConf = ejs.render(COLLECTD_CONFIG_EJS, { appId: app.id, containerId: app.containerId, appDataDir: apps.getDataDir(app, app.dataDir) });
fs.writeFile(path.join(paths.COLLECTD_APPCONFIG_DIR, app.id + '.conf'), collectdConf, function (error) {
if (error) return callback(error);
shell.sudo('addCollectdProfile', [ CONFIGURE_COLLECTD_CMD, 'add', app.id ], {}, callback);
@@ -228,7 +219,7 @@ function addLogrotateConfig(app, callback) {
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 logrotateConf = ejs.render(LOGROTATE_CONFIG_EJS, { volumePath: runVolume.Source, appId: app.id });
var tmpFilePath = path.join(os.tmpdir(), app.id + '.logrotate');
fs.writeFile(tmpFilePath, logrotateConf, function (error) {
if (error) return callback(error);
@@ -266,7 +257,7 @@ function downloadIcon(app, callback) {
debugApp(app, 'Downloading icon of %s@%s', app.appStoreId, app.manifest.version);
var iconUrl = config.apiServerOrigin() + '/api/v1/apps/' + app.appStoreId + '/versions/' + app.manifest.version + '/icon';
var iconUrl = settings.apiServerOrigin() + '/api/v1/apps/' + app.appStoreId + '/versions/' + app.manifest.version + '/icon';
async.retry({ times: 10, interval: 5000 }, function (retryCallback) {
superagent
@@ -274,7 +265,7 @@ function downloadIcon(app, callback) {
.buffer(true)
.timeout(30 * 1000)
.end(function (error, res) {
if (error && !error.response) return retryCallback(new Error('Network error downloading icon:' + error.message));
if (error && !error.response) return retryCallback(new Error('Network error downloading icon : ' + error.message));
if (res.statusCode !== 200) return retryCallback(null); // ignore error. this can also happen for apps installed with cloudron-cli
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));
@@ -284,7 +275,7 @@ function downloadIcon(app, callback) {
}, callback);
}
function registerSubdomain(app, overwrite, callback) {
function registerSubdomains(app, overwrite, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof overwrite, 'boolean');
assert.strictEqual(typeof callback, 'function');
@@ -292,73 +283,17 @@ function registerSubdomain(app, overwrite, callback) {
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Registering subdomain location [%s] overwrite: %s', app.fqdn, overwrite);
const allDomains = [ { subdomain: app.location, domain: app.domain }].concat(app.alternateDomains);
// get the current record before updating it
domains.getDnsRecords(app.location, app.domain, 'A', function (error, values) {
if (error && error.reason === DomainsError.EXTERNAL_ERROR) return retryCallback(error); // try again
if (error) return retryCallback(null, error); // give up for access and other errors
// refuse to update any existing DNS record for custom domains that we did not create
if (values.length !== 0 && !overwrite) return retryCallback(null, new Error('DNS Record already exists'));
domains.upsertDnsRecords(app.location, app.domain, 'A', [ ip ], function (error) {
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) {
debug('Upsert error. Will retry.', error.message);
return retryCallback(error); // try again
}
retryCallback(null, error);
});
});
}, function (error, result) {
if (error || result instanceof Error) return callback(error || result);
callback(null);
});
});
}
function unregisterSubdomain(app, location, domain, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
async.retry({ times: 30, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Unregistering subdomain: %s', app.fqdn);
domains.removeDnsRecords(location, domain, 'A', [ ip ], function (error) {
if (error && error.reason === DomainsError.NOT_FOUND) return retryCallback(null, null); // domain can be not found if oldConfig.domain or restoreConfig.domain was removed
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) return retryCallback(error); // try again
retryCallback(null, error);
});
}, function (error, result) {
if (error || result instanceof Error) return callback(error || result);
callback(null);
});
});
}
function registerAlternateDomains(app, overwrite, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof overwrite, 'boolean');
assert.strictEqual(typeof callback, 'function');
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
async.eachSeries(app.alternateDomains, function (domain, callback) {
console.dir(allDomains);
async.eachSeries(allDomains, function (domain, iteratorDone) {
async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Registering alternate subdomain [%s] overwrite: %s', (domain.subdomain ? (domain.subdomain + '.') : '') + domain.domain, overwrite);
debugApp(app, 'Registering subdomain: %s%s', domain.subdomain ? (domain.subdomain + '.') : '', domain.domain);
// get the current record before updating it
domains.getDnsRecords(domain.subdomain, domain.domain, 'A', function (error, values) {
if (error) return retryCallback(error);
if (error && error.reason === DomainsError.EXTERNAL_ERROR) return retryCallback(error); // try again
if (error) return retryCallback(null, error); // give up for access and other errors
// refuse to update any existing DNS record for custom domains that we did not create
if (values.length !== 0 && !overwrite) return retryCallback(null, new Error('DNS Record already exists'));
@@ -368,37 +303,28 @@ function registerAlternateDomains(app, overwrite, callback) {
debug('Upsert error. Will retry.', error.message);
return retryCallback(error); // try again
}
retryCallback(null, error);
});
});
}, function (error, result) {
if (error || result instanceof Error) return callback(error || result);
callback();
if (error || result instanceof Error) return iteratorDone(error || result);
iteratorDone(null);
});
}, callback);
});
}
function unregisterAlternateDomains(app, all, callback) {
function unregisterSubdomains(app, allDomains, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof all, 'boolean');
assert(Array.isArray(allDomains));
assert.strictEqual(typeof callback, 'function');
let obsoleteDomains = [];
if (all) {
obsoleteDomains = app.alternateDomains;
} else if (app.oldConfig) { // oldConfig can be null during an infra update
obsoleteDomains = app.oldConfig.alternateDomains.filter(function (o) {
return !app.alternateDomains.some(function (n) { return n.subdomain === o.subdomain && n.domain === o.domain; });
});
}
if (obsoleteDomains.length === 0) return callback();
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
async.eachSeries(obsoleteDomains, function (domain, callback) {
async.eachSeries(allDomains, function (domain, iteratorDone) {
async.retry({ times: 30, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Unregistering subdomain: %s%s', domain.subdomain ? (domain.subdomain + '.') : '', domain.domain);
@@ -409,8 +335,9 @@ function unregisterAlternateDomains(app, all, callback) {
retryCallback(null, error);
});
}, function (error, result) {
if (error || result instanceof Error) return callback(error || result);
callback();
if (error || result instanceof Error) return iteratorDone(error || result);
iteratorDone();
});
}, callback);
});
@@ -447,7 +374,7 @@ function waitForDnsPropagation(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (!config.CLOUDRON) {
if (!constants.CLOUDRON) {
debugApp(app, 'Skipping dns propagation check for development');
return callback(null);
}
@@ -479,6 +406,23 @@ function migrateDataDir(app, sourceDir, callback) {
shell.sudo('migrateDataDir', [ MV_VOLUME_CMD, resolvedSourceDir, resolvedTargetDir ], {}, callback);
}
function downloadImage(manifest, callback) {
assert.strictEqual(typeof manifest, 'object');
assert.strictEqual(typeof callback, 'function');
docker.info(function (error, info) {
if (error) return callback(error);
const dfAsync = util.callbackify(df.file);
dfAsync(info.DockerRootDir, function (error, diskUsage) {
if (error) return callback(error);
if (diskUsage.available < (1024*1024*1024)) return callback(new Error('Not enough disk space to pull docker image. See https://cloudron.io/documentation/storage/#docker-image-location'));
docker.downloadImage(manifest, callback);
});
});
}
// Ordering is based on the following rationale:
// - configure nginx, icon, oauth
// - register subdomain.
@@ -490,11 +434,13 @@ function migrateDataDir(app, sourceDir, callback) {
// - 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) {
function install(app, restoreConfig, progressCallback, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof restoreConfig, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
const restoreConfig = app.restoreConfig, isRestoring = app.installationState === appdb.ISTATE_PENDING_RESTORE;
const isInstalling = app.installationState !== apps.ISTATE_PENDING_RESTORE; // install or clone
async.series([
// this protects against the theoretical possibility of an app being marked for install/restore from
@@ -502,15 +448,15 @@ function install(app, callback) {
verifyManifest.bind(null, app.manifest),
// teardown for re-installs
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
progressCallback.bind(null, { percent: 10, message: 'Cleaning up old install' }),
unconfigureReverseProxy.bind(null, app),
removeCollectdProfile.bind(null, app),
removeLogrotateConfig.bind(null, app),
stopApp.bind(null, app),
stopApp.bind(null, app, progressCallback),
deleteContainers.bind(null, app, { managedOnly: true }),
function teardownAddons(next) {
// when restoring, app does not require these addons anymore. remove carefully to preserve the db passwords
var addonsToRemove = !isRestoring ? app.manifest.addons : _.omit(app.oldConfig.manifest.addons, Object.keys(app.manifest.addons));
var addonsToRemove = isInstalling ? app.manifest.addons : _.omit(restoreConfig.oldManifest.addons, Object.keys(app.manifest.addons));
addons.teardownAddons(app, addonsToRemove, next);
},
@@ -518,205 +464,205 @@ function install(app, callback) {
// for restore case
function deleteImageIfChanged(done) {
if (!app.oldConfig || (app.oldConfig.manifest.dockerImage === app.manifest.dockerImage)) return done();
if (isInstalling) return done();
docker.deleteImage(app.oldConfig.manifest, done);
if (restoreConfig.oldManifest.dockerImage === app.manifest.dockerImage) return done();
docker.deleteImage(restoreConfig.oldManifest, done);
},
reserveHttpPort.bind(null, app),
updateApp.bind(null, app, { installationProgress: '20, Downloading icon' }),
progressCallback.bind(null, { percent: 20, message: 'Downloading icon' }),
downloadIcon.bind(null, app),
updateApp.bind(null, app, { installationProgress: '30, Registering subdomain' }),
registerSubdomain.bind(null, app, isRestoring /* overwrite */),
progressCallback.bind(null, { percent: 30, message: 'Registering subdomains' }),
registerSubdomains.bind(null, app, !isInstalling /* overwrite */),
updateApp.bind(null, app, { installationProgress: '35, Registering alternate domains'}),
registerAlternateDomains.bind(null, app, isRestoring /* overwrite */),
progressCallback.bind(null, { percent: 40, message: 'Downloading image' }),
downloadImage.bind(null, app.manifest),
updateApp.bind(null, app, { installationProgress: '40, Downloading image' }),
docker.downloadImage.bind(null, app.manifest),
updateApp.bind(null, app, { installationProgress: '50, Creating app data directory' }),
progressCallback.bind(null, { percent: 50, message: 'Creating app data directory' }),
createAppDir.bind(null, app),
function restoreFromBackup(next) {
if (!restoreConfig) {
if (!restoreConfig.backupId) {
async.series([
updateApp.bind(null, app, { installationProgress: '60, Setting up addons' }),
progressCallback.bind(null, { percent: 60, message: 'Setting up addons' }),
addons.setupAddons.bind(null, app, app.manifest.addons),
], next);
} else {
async.series([
updateApp.bind(null, app, { installationProgress: '65, Download backup and restoring addons' }),
progressCallback.bind(null, { percent: 65, message: 'Download backup and restoring addons' }),
addons.setupAddons.bind(null, app, app.manifest.addons),
addons.clearAddons.bind(null, app, app.manifest.addons),
backups.restoreApp.bind(null, app, app.manifest.addons, restoreConfig, (progress) => updateApp(app, { installationProgress: `65, Restore - ${progress.message}` }, NOOP_CALLBACK))
backups.restoreApp.bind(null, app, app.manifest.addons, restoreConfig, (progress) => {
progressCallback({ percent: 65, message: `Restore - ${progress.message}` });
})
], next);
}
},
updateApp.bind(null, app, { installationProgress: '70, Creating container' }),
progressCallback.bind(null, { percent: 70, message: 'Creating container' }),
createContainer.bind(null, app),
updateApp.bind(null, app, { installationProgress: '75, Setting up logrotate config' }),
progressCallback.bind(null, { percent: 75, message: 'Setting up logrotate config' }),
addLogrotateConfig.bind(null, app),
updateApp.bind(null, app, { installationProgress: '80, Setting up collectd profile' }),
progressCallback.bind(null, { percent: 80, message: 'Setting up collectd profile' }),
addCollectdProfile.bind(null, app),
runApp.bind(null, app),
runApp.bind(null, app, progressCallback),
updateApp.bind(null, app, { installationProgress: '85, Waiting for DNS propagation' }),
progressCallback.bind(null, { percent: 85, message: 'Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '95, Configuring reverse proxy' }),
progressCallback.bind(null, { percent: 95, message: 'Configuring reverse proxy' }),
configureReverseProxy.bind(null, app),
// done!
function (callback) {
debugApp(app, 'installed');
updateApp(app, { installationState: appdb.ISTATE_INSTALLED, installationProgress: '', health: null }, callback);
}
progressCallback.bind(null, { percent: 100, message: 'Done' }),
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, health: null })
], function seriesDone(error) {
if (error) {
debugApp(app, 'error installing app: %s', error);
return updateApp(app, { installationState: appdb.ISTATE_ERROR, installationProgress: error.message }, callback.bind(null, error));
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: { message: error.message } }, callback.bind(null, error));
}
callback(null);
});
}
function backup(app, callback) {
function backup(app, progressCallback, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
async.series([
updateApp.bind(null, app, { installationProgress: '10, Backing up' }),
backups.backupApp.bind(null, app, { /* options */ }, (progress) => updateApp(app, { installationProgress: `30, ${progress.message}` }, NOOP_CALLBACK)),
progressCallback.bind(null, { percent: 10, message: 'Backing up' }),
backups.backupApp.bind(null, app, { /* options */ }, (progress) => {
progressCallback({ percent: 30, message: progress.message });
}),
// done!
function (callback) {
debugApp(app, 'installed');
updateApp(app, { installationState: appdb.ISTATE_INSTALLED, installationProgress: '' }, callback);
}
progressCallback.bind(null, { percent: 100, message: 'Done' }),
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null })
], function seriesDone(error) {
if (error) {
debugApp(app, 'error backing up app: %s', error);
return updateApp(app, { installationState: appdb.ISTATE_INSTALLED, installationProgress: error.message }, callback.bind(null, error)); // return to installed state intentionally
return updateApp(app, { installationState: apps.ISTATE_INSTALLED, error: { message: error.message } }, callback.bind(null, error)); // return to installed state intentionally
}
callback(null);
});
}
// note that configure is called after an infra update as well
function configure(app, callback) {
function configure(app, oldConfig, progressCallback, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof oldConfig, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
// oldConfig can be null during an infra update
const locationChanged = app.oldConfig && (app.oldConfig.fqdn !== app.fqdn);
const dataDirChanged = app.oldConfig && (app.oldConfig.dataDir !== app.dataDir);
const locationChanged = oldConfig.fqdn !== app.fqdn;
const dataDirChanged = oldConfig.dataDir !== app.dataDir;
async.series([
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
progressCallback.bind(null, { percent: 10, message: 'Cleaning up old install' }),
unconfigureReverseProxy.bind(null, app),
removeCollectdProfile.bind(null, app),
removeLogrotateConfig.bind(null, app),
stopApp.bind(null, app),
stopApp.bind(null, app, progressCallback),
deleteContainers.bind(null, app, { managedOnly: true }),
unregisterAlternateDomains.bind(null, app, false /* all */),
function (next) {
if (!locationChanged) return next();
let obsoleteDomains = oldConfig.alternateDomains.filter(function (o) {
return !app.alternateDomains.some(function (n) { return n.subdomain === o.subdomain && n.domain === o.domain; });
});
unregisterSubdomain(app, app.oldConfig.location, app.oldConfig.domain, next);
if (locationChanged) obsoleteDomains.push({ subdomain: oldConfig.location, domain: oldConfig.domain });
if (obsoleteDomains.length === 0) return next();
unregisterSubdomains(app, obsoleteDomains, next);
},
reserveHttpPort.bind(null, app),
updateApp.bind(null, app, { installationProgress: '20, Downloading icon' }),
progressCallback.bind(null, { percent: 20, message: 'Downloading icon' }),
downloadIcon.bind(null, app),
updateApp.bind(null, app, { installationProgress: '30, Registering subdomain' }),
registerSubdomain.bind(null, app, !locationChanged /* overwrite */), // if location changed, do not overwrite to detect conflicts
progressCallback.bind(null, { percent: 30, message: 'Registering subdomains' }),
registerSubdomains.bind(null, app, !locationChanged /* overwrite */), // if location changed, do not overwrite to detect conflicts
updateApp.bind(null, app, { installationProgress: '35, Registering alternate domains'}),
registerAlternateDomains.bind(null, app, true /* overwrite */), // figure out when to overwrite
progressCallback.bind(null, { percent: 40, message: 'Downloading image' }),
downloadImage.bind(null, app.manifest),
updateApp.bind(null, app, { installationProgress: '40, Downloading image' }),
docker.downloadImage.bind(null, app.manifest),
updateApp.bind(null, app, { installationProgress: '45, Ensuring app data directory' }),
progressCallback.bind(null, { percent: 45, message: 'Ensuring app data directory' }),
createAppDir.bind(null, app),
// re-setup addons since they rely on the app's fqdn (e.g oauth)
updateApp.bind(null, app, { installationProgress: '50, Setting up addons' }),
progressCallback.bind(null, { percent: 50, message: 'Setting up addons' }),
addons.setupAddons.bind(null, app, app.manifest.addons),
// migrate dataDir
function (next) {
if (!dataDirChanged) return next();
migrateDataDir(app, app.oldConfig.dataDir, next);
migrateDataDir(app, oldConfig.dataDir, next);
},
updateApp.bind(null, app, { installationProgress: '60, Creating container' }),
progressCallback.bind(null, { percent: 60, message: 'Creating container' }),
createContainer.bind(null, app),
updateApp.bind(null, app, { installationProgress: '65, Setting up logrotate config' }),
progressCallback.bind(null, { percent: 65, message: 'Setting up logrotate config' }),
addLogrotateConfig.bind(null, app),
updateApp.bind(null, app, { installationProgress: '70, Add collectd profile' }),
progressCallback.bind(null, { percent: 70, message: 'Add collectd profile' }),
addCollectdProfile.bind(null, app),
runApp.bind(null, app),
runApp.bind(null, app, progressCallback),
updateApp.bind(null, app, { installationProgress: '80, Waiting for DNS propagation' }),
progressCallback.bind(null, { percent: 80, message: 'Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Configuring reverse proxy' }),
progressCallback.bind(null, { percent: 90, message: 'Configuring reverse proxy' }),
configureReverseProxy.bind(null, app),
// done!
function (callback) {
debugApp(app, 'configured');
updateApp(app, { installationState: appdb.ISTATE_INSTALLED, installationProgress: '', health: null }, callback);
}
progressCallback.bind(null, { percent: 100, message: 'Done' }),
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
], function seriesDone(error) {
if (error) {
debugApp(app, 'error reconfiguring : %s', error);
return updateApp(app, { installationState: appdb.ISTATE_ERROR, installationProgress: error.message }, callback.bind(null, error));
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: { message: error.message }}, callback.bind(null, error));
}
callback(null);
});
}
// nginx configuration is skipped because app.httpPort is expected to be available
function update(app, callback) {
function update(app, updateConfig, progressCallback, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof updateConfig, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
debugApp(app, `Updating to ${app.updateConfig.manifest.version}`);
debugApp(app, `Updating to ${updateConfig.manifest.version}`);
// app does not want these addons anymore
// FIXME: this does not handle option changes (like multipleDatabases)
var unusedAddons = _.omit(app.manifest.addons, Object.keys(app.updateConfig.manifest.addons));
const FORCED_UPDATE = (app.installationState === appdb.ISTATE_PENDING_FORCE_UPDATE);
var unusedAddons = _.omit(app.manifest.addons, Object.keys(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.updateConfig.manifest),
progressCallback.bind(null, { percent: 0, message: 'Verify manifest' }),
verifyManifest.bind(null, updateConfig.manifest),
function (next) {
if (FORCED_UPDATE) return next(null);
if (updateConfig.skipBackup) return next(null);
async.series([
updateApp.bind(null, app, { installationProgress: '15, Backing up app' }),
progressCallback.bind(null, { percent: 15, message: 'Backing up app' }),
// preserve update backups for 3 weeks
backups.backupApp.bind(null, app, { preserveSecs: 3*7*24*60*60 }, (progress) => updateApp(app, { installationProgress: `15, Backup - ${progress.message}` }, NOOP_CALLBACK))
backups.backupApp.bind(null, app, { preserveSecs: 3*7*24*60*60 }, (progress) => {
progressCallback({ percent: 15, message: `Backup - ${progress.message}` });
})
], function (error) {
if (error) error.backupError = true;
next(error);
@@ -725,18 +671,18 @@ function update(app, callback) {
// 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),
progressCallback.bind(null, { percent: 25, message: 'Downloading image' }),
downloadImage.bind(null, 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' }),
progressCallback.bind(null, { percent: 35, message: 'Cleaning up old install' }),
removeCollectdProfile.bind(null, app),
removeLogrotateConfig.bind(null, app),
stopApp.bind(null, app),
stopApp.bind(null, app, progressCallback),
deleteContainers.bind(null, app, { managedOnly: true }),
function deleteImageIfChanged(done) {
if (app.manifest.dockerImage === app.updateConfig.manifest.dockerImage) return done();
if (app.manifest.dockerImage === updateConfig.manifest.dockerImage) return done();
docker.deleteImage(app.manifest, done);
},
@@ -747,8 +693,8 @@ function update(app, callback) {
// free unused ports
function (next) {
const currentPorts = app.portBindings || {};
const newTcpPorts = app.updateConfig.manifest.tcpPorts || {};
const newUdpPorts = app.updateConfig.manifest.udpPorts || {};
const newTcpPorts = updateConfig.manifest.tcpPorts || {};
const newUdpPorts = updateConfig.manifest.udpPorts || {};
async.each(Object.keys(currentPorts), function (portName, callback) {
if (newTcpPorts[portName] || newUdpPorts[portName]) return callback(); // port still in use
@@ -765,141 +711,124 @@ function update(app, 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, _.pick(updateConfig, 'manifest', 'appStoreId', 'memoryLimit')), // switch over to the new config
updateApp.bind(null, app, { installationProgress: '45, Downloading icon' }),
progressCallback.bind(null, { percent: 45, message: 'Downloading icon' }),
downloadIcon.bind(null, app),
updateApp.bind(null, app, { installationProgress: '70, Updating addons' }),
addons.setupAddons.bind(null, app, app.updateConfig.manifest.addons),
progressCallback.bind(null, { percent: 70, message: 'Updating addons' }),
addons.setupAddons.bind(null, app, updateConfig.manifest.addons),
updateApp.bind(null, app, { installationProgress: '80, Creating container' }),
progressCallback.bind(null, { percent: 80, message: 'Creating container' }),
createContainer.bind(null, app),
updateApp.bind(null, app, { installationProgress: '85, Setting up logrotate config' }),
progressCallback.bind(null, { percent: 85, message: 'Setting up logrotate config' }),
addLogrotateConfig.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Add collectd profile' }),
progressCallback.bind(null, { percent: 90, message: 'Add collectd profile' }),
addCollectdProfile.bind(null, app),
runApp.bind(null, app),
runApp.bind(null, app, progressCallback),
// done!
function (callback) {
debugApp(app, 'updated');
updateApp(app, { installationState: appdb.ISTATE_INSTALLED, installationProgress: '', health: null, updateConfig: null, updateTime: new Date() }, callback);
}
progressCallback.bind(null, { percent: 100, message: 'Done' }),
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null, updateTime: new Date() })
], function seriesDone(error) {
if (error && error.backupError) {
debugApp(app, 'update aborted because backup failed', error);
updateApp(app, { installationState: appdb.ISTATE_INSTALLED, installationProgress: '', health: null, updateConfig: null }, callback.bind(null, error));
updateApp(app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null }, callback.bind(null, error));
} else if (error) {
debugApp(app, 'Error updating app: %s', error);
updateApp(app, { installationState: appdb.ISTATE_ERROR, installationProgress: error.message, updateTime: new Date() }, callback.bind(null, error));
updateApp(app, { installationState: apps.ISTATE_ERROR, error: { message: error.message }, updateTime: new Date() }, callback.bind(null, error));
} else {
// do not spam the notifcation view
if (FORCED_UPDATE) return callback();
if (updateConfig.skipNotification) return callback();
eventlog.add(eventlog.ACTION_APP_UPDATE_FINISH, auditsource.APP_TASK, { app: app, success: true }, callback);
}
});
}
function uninstall(app, callback) {
function uninstall(app, progressCallback, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'uninstalling');
async.series([
updateApp.bind(null, app, { installationProgress: '0, Remove collectd profile' }),
progressCallback.bind(null, { percent: 0, message: 'Remove collectd profile' }),
removeCollectdProfile.bind(null, app),
updateApp.bind(null, app, { installationProgress: '5, Remove logrotate config' }),
progressCallback.bind(null, { percent: 5, message: 'Remove logrotate config' }),
removeLogrotateConfig.bind(null, app),
updateApp.bind(null, app, { installationProgress: '10, Stopping app' }),
stopApp.bind(null, app),
progressCallback.bind(null, { percent: 10, message: 'Stopping app' }),
stopApp.bind(null, app, progressCallback),
updateApp.bind(null, app, { installationProgress: '20, Deleting container' }),
progressCallback.bind(null, { percent: 20, message: 'Deleting container' }),
deleteContainers.bind(null, app, {}),
updateApp.bind(null, app, { installationProgress: '30, Teardown addons' }),
progressCallback.bind(null, { percent: 30, message: 'Teardown addons' }),
addons.teardownAddons.bind(null, app, app.manifest.addons),
updateApp.bind(null, app, { installationProgress: '40, Deleting app data directory' }),
progressCallback.bind(null, { percent: 40, message: 'Deleting app data directory' }),
deleteAppDir.bind(null, app, { removeDirectory: true }),
updateApp.bind(null, app, { installationProgress: '50, Deleting image' }),
progressCallback.bind(null, { percent: 50, message: 'Deleting image' }),
docker.deleteImage.bind(null, app.manifest),
updateApp.bind(null, app, { installationProgress: '60, Unregistering domains' }),
unregisterAlternateDomains.bind(null, app, true /* all */),
unregisterSubdomain.bind(null, app, app.location, app.domain),
progressCallback.bind(null, { percent: 60, message: 'Unregistering domains' }),
unregisterSubdomains.bind(null, app, [ { subdomain: app.location, domain: app.domain } ].concat(app.alternateDomains)),
updateApp.bind(null, app, { installationProgress: '70, Cleanup icon' }),
progressCallback.bind(null, { percent: 70, message: 'Cleanup icon' }),
removeIcon.bind(null, app),
updateApp.bind(null, app, { installationProgress: '80, Unconfiguring reverse proxy' }),
progressCallback.bind(null, { percent: 80, message: 'Unconfiguring reverse proxy' }),
unconfigureReverseProxy.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Cleanup logs' }),
progressCallback.bind(null, { percent: 90, message: 'Cleanup logs' }),
cleanupLogs.bind(null, app),
updateApp.bind(null, app, { installationProgress: '95, Remove app from database' }),
progressCallback.bind(null, { percent: 95, message: 'Remove app from database' }),
appdb.del.bind(null, app.id)
], function seriesDone(error) {
if (error) {
debugApp(app, 'error uninstalling app: %s', error);
return updateApp(app, { installationState: appdb.ISTATE_ERROR, installationProgress: error.message }, callback.bind(null, error));
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: { message: error.message } }, callback.bind(null, error));
}
callback(null);
});
}
function runApp(app, callback) {
function runApp(app, progressCallback, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
progressCallback({ message: 'Starting app' });
docker.startContainer(app.containerId, function (error) {
if (error) return callback(error);
updateApp(app, { runState: appdb.RSTATE_RUNNING }, callback);
updateApp(app, { runState: apps.RSTATE_RUNNING }, callback);
});
}
function stopApp(app, callback) {
function stopApp(app, progressCallback, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
progressCallback({ message: 'Stopping app' });
docker.stopContainers(app.id, function (error) {
if (error) return callback(error);
updateApp(app, { runState: appdb.RSTATE_STOPPED, health: null }, callback);
updateApp(app, { runState: apps.RSTATE_STOPPED, health: null }, callback);
});
}
function handleRunCommand(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (app.runState === appdb.RSTATE_PENDING_STOP) {
return stopApp(app, callback);
}
if (app.runState === appdb.RSTATE_PENDING_START || app.runState === appdb.RSTATE_RUNNING) {
debugApp(app, 'Resuming app with state : %s', app.runState);
return runApp(app, callback);
}
debugApp(app, 'handleRunCommand - doing nothing: %s', app.runState);
return callback(null);
}
function startTask(appId, callback) {
function run(appId, args, progressCallback, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof args, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
// determine what to do
@@ -909,51 +838,23 @@ function startTask(appId, callback) {
debugApp(app, 'startTask installationState: %s runState: %s', app.installationState, app.runState);
switch (app.installationState) {
case appdb.ISTATE_PENDING_UNINSTALL: return uninstall(app, callback);
case appdb.ISTATE_PENDING_CONFIGURE: return configure(app, callback);
case apps.ISTATE_PENDING_INSTALL: return install(app, args.restoreConfig || {}, progressCallback, callback);
case apps.ISTATE_PENDING_CONFIGURE: return configure(app, args.oldConfig, progressCallback, callback);
case apps.ISTATE_PENDING_UNINSTALL: return uninstall(app, progressCallback, callback);
case apps.ISTATE_PENDING_CLONE: return install(app, args.restoreConfig || {}, progressCallback, callback);
case apps.ISTATE_PENDING_RESTORE: return install(app, args.restoreConfig || {}, progressCallback, callback);
case apps.ISTATE_PENDING_UPDATE: return update(app, args.updateConfig, progressCallback, callback);
case apps.ISTATE_PENDING_BACKUP: return backup(app, progressCallback, callback);
case apps.ISTATE_INSTALLED:
switch (app.runState) {
case apps.RSTATE_PENDING_STOP: return stopApp(app, progressCallback, callback);
case apps.RSTATE_PENDING_START: return runApp(app, progressCallback, callback);
default: return callback(new Error('Unknown run command in apptask:' + app.runState));
}
case appdb.ISTATE_PENDING_UPDATE: return update(app, callback);
case appdb.ISTATE_PENDING_FORCE_UPDATE: return update(app, callback);
case appdb.ISTATE_PENDING_INSTALL: return install(app, callback);
case appdb.ISTATE_PENDING_CLONE: return install(app, callback);
case appdb.ISTATE_PENDING_RESTORE: return install(app, callback);
case appdb.ISTATE_PENDING_BACKUP: return backup(app, callback);
case appdb.ISTATE_INSTALLED: return handleRunCommand(app, callback);
case appdb.ISTATE_ERROR:
debugApp(app, 'Internal error. apptask launched with error status.');
return callback(null);
default:
debugApp(app, 'apptask launched with invalid command');
return callback(new Error('Unknown command in apptask:' + app.installationState));
return callback(new Error('Unknown install command in apptask:' + app.installationState));
}
});
}
if (require.main === module) {
assert.strictEqual(process.argv.length, 3, 'Pass the appid as argument');
// add a separator for the log file
debug('------------------------------------------------------------');
debug('Apptask for %s', process.argv[2]);
process.on('SIGTERM', function () {
debug('taskmanager sent SIGTERM since it got a new task for this app');
process.exit(0);
});
initialize(function (error) {
if (error) throw error;
startTask(process.argv[2], function (error) {
if (error) debug('Apptask completed with error', error);
debug('Apptask completed for %s', process.argv[2]);
// 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);
});
});
}
+103
View File
@@ -0,0 +1,103 @@
'use strict';
exports = module.exports = {
resumeTasks: resumeTasks,
scheduleTask: scheduleTask
};
let apps = require('./apps.js'),
assert = require('assert'),
debug = require('debug')('box:taskmanager'),
fs = require('fs'),
locker = require('./locker.js'),
safe = require('safetydance'),
path = require('path'),
paths = require('./paths.js'),
tasks = require('./tasks.js');
let gActiveTasks = { }; // indexed by app id
let gPendingTasks = [ ];
const TASK_CONCURRENCY = 3;
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
function waitText(lockOperation) {
if (lockOperation === locker.OP_BOX_UPDATE) return 'Waiting for Cloudron to finish updating. See the Settings view';
if (lockOperation === locker.OP_PLATFORM_START) return 'Waiting for Cloudron to initialize';
if (lockOperation === locker.OP_FULL_BACKUP) return 'Wait for Cloudron to finish backup. See the Backups view';
return ''; // cannot happen
}
// callback is called when task is finished
function scheduleTask(appId, taskId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof taskId, 'string');
assert.strictEqual(typeof callback, 'function');
if (appId in gActiveTasks) {
return callback(new Error(`Task for %s is already active: ${appId}`));
}
if (Object.keys(gActiveTasks).length >= TASK_CONCURRENCY) {
debug(`Reached concurrency limit, queueing task id ${taskId}`);
tasks.update(taskId, { percent: 0, message: 'Waiting for other app tasks to complete' }, NOOP_CALLBACK);
gPendingTasks.push({ appId, taskId, callback });
return;
}
var lockError = locker.recursiveLock(locker.OP_APPTASK);
if (lockError) {
debug(`Could not get lock. ${lockError.message}, queueing task id ${taskId}`);
tasks.update(taskId, { percent: 0, message: waitText(lockError.operation) }, NOOP_CALLBACK);
gPendingTasks.push({ appId, taskId, callback });
return;
}
gActiveTasks[appId] = {};
const logFile = path.join(paths.LOG_DIR, appId, 'apptask.log');
if (!fs.existsSync(path.dirname(logFile))) safe.fs.mkdirSync(path.dirname(logFile)); // ensure directory
tasks.startTask(taskId, { logFile }, function (error, result) {
callback(error, result);
delete gActiveTasks[appId];
locker.unlock(locker.OP_APPTASK); // unlock event will trigger next task
});
}
function startNextTask() {
if (gPendingTasks.length === 0) return;
assert(Object.keys(gActiveTasks).length < TASK_CONCURRENCY);
const t = gPendingTasks.shift();
scheduleTask(t.appId, t.taskId, t.callback);
}
// resume app tasks when platform is ready or after a crash
function resumeTasks(callback) {
assert.strictEqual(typeof callback, 'function');
debug('resuming tasks');
locker.on('unlocked', startNextTask);
apps.getAll(function (error, result) {
if (error) return callback(error);
result.forEach(function (app) {
if (app.installationState === apps.ISTATE_INSTALLED && app.runState === apps.RSTATE_RUNNING) return;
if (app.installationState === apps.ISTATE_ERROR) return;
debug(`resumeTask: schedule task for ${app.fqdn} ${app.id}: state=${app.installationState},taskId=${app.taskId}`);
scheduleTask(app.id, app.taskId, NOOP_CALLBACK);
});
callback(null);
});
}
+1 -1
View File
@@ -4,8 +4,8 @@ exports = module.exports = {
CRON: { userId: null, username: 'cron' },
HEALTH_MONITOR: { userId: null, username: 'healthmonitor' },
SYSADMIN: { userId: null, username: 'sysadmin' },
TASK_MANAGER: { userId: null, username: 'taskmanager' },
APP_TASK: { userId: null, username: 'apptask' },
EXTERNAL_LDAP_TASK: { userId: null, username: 'externalldap' },
fromRequest: fromRequest
};
+92 -50
View File
@@ -40,18 +40,18 @@ exports = module.exports = {
};
var addons = require('./addons.js'),
appdb = require('./appdb.js'),
apps = require('./apps.js'),
AppsError = require('./apps.js').AppsError,
async = require('async'),
assert = require('assert'),
backupdb = require('./backupdb.js'),
config = require('./config.js'),
constants = require('./constants.js'),
crypto = require('crypto'),
database = require('./database.js'),
DatabaseError = require('./databaseerror.js'),
DataLayout = require('./datalayout.js'),
debug = require('debug')('box:backups'),
df = require('@sindresorhus/df'),
eventlog = require('./eventlog.js'),
fs = require('fs'),
locker = require('./locker.js'),
@@ -60,6 +60,7 @@ var addons = require('./addons.js'),
path = require('path'),
paths = require('./paths.js'),
progressStream = require('progress-stream'),
prettyBytes = require('pretty-bytes'),
safe = require('safetydance'),
shell = require('./shell.js'),
settings = require('./settings.js'),
@@ -112,6 +113,7 @@ function api(provider) {
case 's3-v4-compat': return require('./storage/s3.js');
case 'digitalocean-spaces': return require('./storage/s3.js');
case 'exoscale-sos': return require('./storage/s3.js');
case 'wasabi': return require('./storage/s3.js');
case 'scaleway-objectstorage': return require('./storage/s3.js');
case 'noop': return require('./storage/noop.js');
default: return null;
@@ -291,6 +293,9 @@ function tarPack(dataLayout, key, callback) {
},
map: function(header) {
header.name = dataLayout.toRemotePath(header.name);
// the tar pax format allows us to encode filenames > 100 and size > 8GB (see #640)
// https://www.systutorials.com/docs/linux/man/5-star/
if (header.size > 8589934590 || header.name > 99) header.pax = { size: header.size };
return header;
},
strict: false // do not error for unknown types (skip fifo, char/block devices)
@@ -408,6 +413,34 @@ function saveFsMetadata(dataLayout, metadataFile, callback) {
callback();
}
// the du call in the function below requires root
function checkFreeDiskSpace(backupConfig, dataLayout, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
assert.strictEqual(typeof callback, 'function');
if (backupConfig.provider !== 'filesystem') return callback();
let used = 0;
for (let localPath of dataLayout.localPaths()) {
debug(`checkFreeDiskSpace: getting disk usage of ${localPath}`);
let result = safe.child_process.execSync(`du -Dsb ${localPath}`, { encoding: 'utf8' });
if (!result) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, safe.error));
used += parseInt(result, 10);
}
debug(`checkFreeDiskSpace: ${used} bytes`);
df.file(backupConfig.backupFolder).then(function (diskUsage) {
const needed = used + (1024 * 1024 * 1024); // check if there is atleast 1GB left afterwards
if (diskUsage.available <= needed) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, `Not enough disk space for backup. Needed: ${prettyBytes(needed)} Available: ${prettyBytes(diskUsage.available)}`));
callback(null);
}).catch(function (error) {
callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
});
}
// this function is called via backupupload (since it needs root to traverse app's directory)
function upload(backupId, format, dataLayoutString, progressCallback, callback) {
assert.strictEqual(typeof backupId, 'string');
@@ -423,29 +456,33 @@ function upload(backupId, format, dataLayoutString, progressCallback, callback)
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
if (format === 'tgz') {
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
retryCallback = once(retryCallback); // protect again upload() erroring much later after tar stream error
checkFreeDiskSpace(backupConfig, dataLayout, function (error) {
if (error) return callback(error);
tarPack(dataLayout, backupConfig.key || null, function (error, tarStream) {
if (error) return retryCallback(error);
if (format === 'tgz') {
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
retryCallback = once(retryCallback); // protect again upload() erroring much later after tar stream error
tarStream.on('progress', function(progress) {
const transferred = Math.round(progress.transferred/1024/1024), speed = Math.round(progress.speed/1024/1024);
if (!transferred && !speed) return progressCallback({ message: 'Uploading backup' }); // 0M@0Mbps looks wrong
progressCallback({ message: `Uploading backup ${transferred}M@${speed}Mbps` });
tarPack(dataLayout, backupConfig.key || null, function (error, tarStream) {
if (error) return retryCallback(error);
tarStream.on('progress', function(progress) {
const transferred = Math.round(progress.transferred/1024/1024), speed = Math.round(progress.speed/1024/1024);
if (!transferred && !speed) return progressCallback({ message: 'Uploading backup' }); // 0M@0Mbps looks wrong
progressCallback({ message: `Uploading backup ${transferred}M@${speed}Mbps` });
});
tarStream.on('error', retryCallback); // already returns BackupsError
api(backupConfig.provider).upload(backupConfig, getBackupFilePath(backupConfig, backupId, format), tarStream, retryCallback);
});
tarStream.on('error', retryCallback); // already returns BackupsError
api(backupConfig.provider).upload(backupConfig, getBackupFilePath(backupConfig, backupId, format), tarStream, retryCallback);
});
}, callback);
} else {
async.series([
saveFsMetadata.bind(null, dataLayout, `${dataLayout.localRoot()}/fsmetadata.json`),
sync.bind(null, backupConfig, backupId, dataLayout, progressCallback)
], callback);
}
}, callback);
} else {
async.series([
saveFsMetadata.bind(null, dataLayout, `${dataLayout.localRoot()}/fsmetadata.json`),
sync.bind(null, backupConfig, backupId, dataLayout, progressCallback)
], callback);
}
});
});
}
@@ -638,7 +675,7 @@ function restore(backupConfig, backupId, progressCallback, callback) {
debug('restore: database imported');
callback();
settings.initCache(callback);
});
});
}
@@ -689,7 +726,7 @@ function runBackupUpload(backupId, format, dataLayout, progressCallback, callbac
callback();
}).on('message', function (message) {
if (!message.result) return progressCallback(message);
debug(`runBackupUpload: result - ${message}`);
debug(`runBackupUpload: result - ${JSON.stringify(message)}`);
result = message.result;
});
}
@@ -764,12 +801,12 @@ function rotateBoxBackup(backupConfig, tag, appBackupIds, progressCallback, call
if (!snapshotInfo) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, 'Snapshot info missing or corrupt'));
const snapshotTime = snapshotInfo.timestamp.replace(/[T.]/g, '-').replace(/[:Z]/g,''); // add this to filename to make it unique, so it's easy to download them
const backupId = util.format('%s/box_%s_v%s', tag, snapshotTime, config.version());
const backupId = util.format('%s/box_%s_v%s', tag, snapshotTime, constants.VERSION);
const format = backupConfig.format;
debug(`Rotating box backup to id ${backupId}`);
backupdb.add(backupId, { version: config.version(), type: backupdb.BACKUP_TYPE_BOX, dependsOn: appBackupIds, manifest: null, format: format }, function (error) {
backupdb.add(backupId, { version: constants.VERSION, type: backupdb.BACKUP_TYPE_BOX, dependsOn: appBackupIds, manifest: null, format: format }, function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
var copy = api(backupConfig.provider).copy(backupConfig, getBackupFilePath(backupConfig, 'snapshot/box', format), getBackupFilePath(backupConfig, backupId, format));
@@ -809,10 +846,10 @@ function backupBoxWithAppBackupIds(appBackupIds, tag, progressCallback, callback
function canBackupApp(app) {
// only backup apps that are installed or pending configure or called from apptask. Rest of them are in some
// state not good for consistent backup (i.e addons may not have been setup completely)
return (app.installationState === appdb.ISTATE_INSTALLED && app.health === appdb.HEALTH_HEALTHY) ||
app.installationState === appdb.ISTATE_PENDING_CONFIGURE ||
app.installationState === appdb.ISTATE_PENDING_BACKUP || // called from apptask
app.installationState === appdb.ISTATE_PENDING_UPDATE; // called from apptask
return (app.installationState === apps.ISTATE_INSTALLED && app.health === apps.HEALTH_HEALTHY) ||
app.installationState === apps.ISTATE_PENDING_CONFIGURE ||
app.installationState === apps.ISTATE_PENDING_BACKUP || // called from apptask
app.installationState === apps.ISTATE_PENDING_UPDATE; // called from apptask
}
function snapshotApp(app, progressCallback, callback) {
@@ -982,19 +1019,21 @@ function startBackupTask(auditSource, callback) {
let error = locker.lock(locker.OP_FULL_BACKUP);
if (error) return callback(new BackupsError(BackupsError.BAD_STATE, `Cannot backup now: ${error.message}`));
let task = tasks.startTask(tasks.TASK_BACKUP, []);
task.on('error', (error) => callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)));
task.on('start', (taskId) => {
tasks.add(tasks.TASK_BACKUP, [ ], function (error, taskId) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_BACKUP_START, auditSource, { taskId });
tasks.startTask(taskId, {}, function (error, result) {
locker.unlock(locker.OP_FULL_BACKUP);
const errorMessage = error ? error.message : '';
eventlog.add(eventlog.ACTION_BACKUP_FINISH, auditSource, { taskId: taskId, errorMessage: errorMessage, backupId: result });
});
callback(null, taskId);
});
task.on('finish', (error, result) => {
locker.unlock(locker.OP_FULL_BACKUP);
const errorMessage = error ? error.message : '';
eventlog.add(eventlog.ACTION_BACKUP_FINISH, auditSource, { taskId: task.id, errorMessage: errorMessage, backupId: result });
});
}
function ensureBackup(auditSource, callback) {
@@ -1218,18 +1257,21 @@ function cleanup(auditSource, progressCallback, callback) {
}
function startCleanupTask(auditSource, callback) {
let task = tasks.startTask(tasks.TASK_CLEAN_BACKUPS, [ auditSource ]);
task.on('error', (error) => callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)));
task.on('start', (taskId) => {
tasks.add(tasks.TASK_CLEAN_BACKUPS, [ auditSource ], function (error, taskId) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_BACKUP_CLEANUP_START, auditSource, { taskId });
callback(null, taskId);
});
task.on('finish', (error, result) => { // result is { removedBoxBackups, removedAppBackups }
eventlog.add(eventlog.ACTION_BACKUP_CLEANUP_FINISH, auditSource, {
errorMessage: error ? error.message : null,
removedBoxBackups: result ? result.removedBoxBackups : [],
removedAppBackups: result ? result.removedAppBackups : []
tasks.startTask(taskId, {}, (error, result) => { // result is { removedBoxBackups, removedAppBackups }
eventlog.add(eventlog.ACTION_BACKUP_CLEANUP_FINISH, auditSource, {
errorMessage: error ? error.message : null,
removedBoxBackups: result ? result.removedBoxBackups : [],
removedAppBackups: result ? result.removedAppBackups : []
});
});
callback(null, taskId);
});
}
+38
View File
@@ -0,0 +1,38 @@
'use strict';
let assert = require('assert'),
fs = require('fs'),
path = require('path');
exports = module.exports = {
getChanges: getChanges
};
function getChanges(version) {
assert.strictEqual(typeof version, 'string');
let changelog = [ ];
const lines = fs.readFileSync(path.join(__dirname, '../CHANGES'), 'utf8').split('\n');
version = version.replace(/[+-].*/, ''); // strip prerelease
let i;
for (i = 0; i < lines.length; i++) {
if (lines[i] === '[' + version + ']') break;
}
for (i = i + 1; i < lines.length; i++) {
if (lines[i] === '') continue;
if (lines[i][0] === '[') break;
lines[i] = lines[i].trim();
// detect and remove list style - and * in changelog lines
if (lines[i].indexOf('-') === 0) lines[i] = lines[i].slice(1).trim();
if (lines[i].indexOf('*') === 0) lines[i] = lines[i].slice(1).trim();
changelog.push(lines[i]);
}
return changelog;
}
+76 -92
View File
@@ -6,7 +6,6 @@ exports = module.exports = {
initialize: initialize,
uninitialize: uninitialize,
getConfig: getConfig,
getDisks: getDisks,
getLogs: getLogs,
reboot: reboot,
@@ -19,24 +18,22 @@ exports = module.exports = {
setDashboardAndMailDomain: setDashboardAndMailDomain,
renewCerts: renewCerts,
runSystemChecks: runSystemChecks,
setupDashboard: setupDashboard,
// exposed for testing
_checkDiskSpace: checkDiskSpace
runSystemChecks: runSystemChecks,
};
var apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
auditSource = require('./auditsource.js'),
backups = require('./backups.js'),
clients = require('./clients.js'),
config = require('./config.js'),
constants = require('./constants.js'),
cron = require('./cron.js'),
debug = require('debug')('box:cloudron'),
domains = require('./domains.js'),
DomainsError = require('./domains.js').DomainsError,
df = require('@sindresorhus/df'),
eventlog = require('./eventlog.js'),
custom = require('./custom.js'),
fs = require('fs'),
@@ -47,10 +44,12 @@ var apps = require('./apps.js'),
paths = require('./paths.js'),
platform = require('./platform.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
shell = require('./shell.js'),
spawn = require('child_process').spawn,
split = require('split'),
sysinfo = require('./sysinfo.js'),
tasks = require('./tasks.js'),
users = require('./users.js'),
util = require('util');
@@ -89,7 +88,7 @@ function initialize(callback) {
runStartupTasks();
callback();
notifyUpdate(callback);
}
function uninitialize(callback) {
@@ -113,13 +112,45 @@ function onActivated(callback) {
], callback);
}
function setUpdateSuccess(callback) {
tasks.listByTypePaged(tasks.TASK_UPDATE, 1, 1, function (error, results) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
if (results.length !== 1) return callback(); // when hotfixing
tasks.update(results[0].id, { percent: 100, error: null }, function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
callback();
});
});
}
function notifyUpdate(callback) {
assert.strictEqual(typeof callback, 'function');
const version = safe.fs.readFileSync(paths.VERSION_FILE, 'utf8');
if (version === constants.VERSION) return callback();
eventlog.add(eventlog.ACTION_UPDATE_FINISH, auditSource.CRON, { oldVersion: version || 'dev', newVersion: constants.VERSION }, function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
setUpdateSuccess(function (error) {
if (error) return callback(error);
safe.fs.writeFileSync(paths.VERSION_FILE, constants.VERSION, 'utf8');
callback();
});
});
}
// each of these tasks can fail. we will add some routes to fix/re-run them
function runStartupTasks() {
// configure nginx to be reachable by IP
reverseProxy.configureDefaultServer(NOOP_CALLBACK);
// always generate webadmin config since we have no versioning mechanism for the ejs
if (config.adminDomain()) reverseProxy.writeAdminConfig(config.adminDomain(), NOOP_CALLBACK);
if (settings.adminDomain()) reverseProxy.writeAdminConfig(settings.adminDomain(), NOOP_CALLBACK);
// check activation state and start the platform
users.isActivated(function (error, activated) {
@@ -130,32 +161,6 @@ function runStartupTasks() {
});
}
function getDisks(callback) {
assert.strictEqual(typeof callback, 'function');
var disks = {
boxDataDisk: null,
platformDataDisk: null,
appsDataDisk: null
};
df.file(paths.BOX_DATA_DIR).then(function (result) {
disks.boxDataDisk = result.filesystem;
return df.file(paths.PLATFORM_DATA_DIR);
}).then(function (result) {
disks.platformDataDisk = result.filesystem;
return df.file(paths.APPS_DATA_DIR);
}).then(function (result) {
disks.appsDataDisk = result.filesystem;
callback(null, disks);
}).catch(function (error) {
callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
});
}
function getConfig(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -164,15 +169,15 @@ function getConfig(callback) {
// be picky about what we send out here since this is sent for 'normal' users as well
callback(null, {
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin(),
adminDomain: config.adminDomain(),
adminFqdn: config.adminFqdn(),
mailFqdn: config.mailFqdn(),
version: config.version(),
isDemo: config.isDemo(),
apiServerOrigin: settings.apiServerOrigin(),
webServerOrigin: settings.webServerOrigin(),
adminDomain: settings.adminDomain(),
adminFqdn: settings.adminFqdn(),
mailFqdn: settings.mailFqdn(),
version: constants.VERSION,
isDemo: settings.isDemo(),
memory: os.totalmem(),
provider: config.provider(),
provider: sysinfo.provider(),
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
uiSpec: custom.uiSpec()
});
@@ -194,7 +199,6 @@ function isRebootRequired(callback) {
function runSystemChecks() {
async.parallel([
checkBackupConfiguration,
checkDiskSpace,
checkMailStatus,
checkRebootRequired
], function (error) {
@@ -214,45 +218,6 @@ function checkBackupConfiguration(callback) {
});
}
function checkDiskSpace(callback) {
assert.strictEqual(typeof callback, 'function');
debug('Checking disk space');
getDisks(function (error, disks) {
if (error) {
debug('df error %s', error.message);
return callback();
}
df().then(function (entries) {
/*
[{
filesystem: '/dev/disk1',
size: 499046809600,
used: 443222245376,
available: 55562420224,
capacity: 0.89,
mountpoint: '/'
}, ...]
*/
var oos = entries.some(function (entry) {
// ignore other filesystems but where box, app and platform data is
if (entry.filesystem !== disks.boxDataDisk && entry.filesystem !== disks.platformDataDisk && entry.filesystem !== disks.appsDataDisk) return false;
return (entry.available <= (1.25 * 1024 * 1024 * 1024)); // 1.5G
});
debug('Disk space checked. ok: %s', !oos);
notifications.alert(notifications.ALERT_DISK_SPACE, 'Server is running out of disk space', oos ? JSON.stringify(entries, null, 4) : '', callback);
}).catch(function (error) {
if (error) console.error(error);
callback();
});
});
}
function checkMailStatus(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -342,9 +307,13 @@ function prepareDashboardDomain(domain, auditSource, callback) {
const conflict = result.filter(app => app.fqdn === fqdn);
if (conflict.length) return callback(new CloudronError(CloudronError.BAD_STATE, 'Dashboard location conflicts with an existing app'));
let task = tasks.startTask(tasks.TASK_PREPARE_DASHBOARD_DOMAIN, [ domain, auditSource ]);
task.on('error', (error) => callback(new CloudronError(CloudronError.INTERNAL_ERROR, error)));
task.on('start', (taskId) => callback(null, taskId));
tasks.add(tasks.TASK_PREPARE_DASHBOARD_DOMAIN, [ domain, auditSource ], function (error, taskId) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
tasks.startTask(taskId, {}, NOOP_CALLBACK);
callback(null, taskId);
});
});
});
}
@@ -366,10 +335,10 @@ function setDashboardDomain(domain, auditSource, callback) {
const fqdn = domains.fqdn(constants.ADMIN_LOCATION, domainObject);
config.setAdminDomain(domain);
config.setAdminFqdn(fqdn);
clients.addDefaultClients(config.adminOrigin(), function (error) {
async.series([
(done) => settings.setAdmin(domain, fqdn, done),
(done) => clients.addDefaultClients(settings.adminOrigin(), done)
], function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_DASHBOARD_DOMAIN_UPDATE, auditSource, { domain: domain, fqdn: fqdn });
@@ -397,12 +366,27 @@ function setDashboardAndMailDomain(domain, auditSource, callback) {
});
}
function setupDashboard(auditSource, progressCallback, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
async.series([
domains.prepareDashboardDomain.bind(null, settings.adminDomain(), auditSource, progressCallback),
setDashboardDomain.bind(null, settings.adminDomain(), auditSource)
], callback);
}
function renewCerts(options, auditSource, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
let task = tasks.startTask(tasks.TASK_RENEW_CERTS, [ options, auditSource ]);
task.on('error', (error) => callback(new CloudronError(CloudronError.INTERNAL_ERROR, error)));
task.on('start', (taskId) => callback(null, taskId));
tasks.add(tasks.TASK_RENEW_CERTS, [ options, auditSource ], function (error, taskId) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
tasks.startTask(taskId, {}, NOOP_CALLBACK);
callback(null, taskId);
});
}
+10
View File
@@ -30,3 +30,13 @@ LoadPlugin "table"
</Result>
</Table>
</Plugin>
<Plugin python>
<Module du>
<Path>
Instance "<%= appId %>"
Dir "<%= appDataDir %>"
</Path>
</Module>
</Plugin>
-203
View File
@@ -1,203 +0,0 @@
'use strict';
exports = module.exports = {
baseDir: baseDir,
// values set here will be lost after a upgrade/update. use the sqlite database
// for persistent values that need to be backed up
get: get,
set: set,
// ifdefs to check environment
CLOUDRON: process.env.BOX_ENV === 'cloudron',
TEST: process.env.BOX_ENV === 'test',
// convenience getters
provider: provider,
apiServerOrigin: apiServerOrigin,
webServerOrigin: webServerOrigin,
adminDomain: adminDomain,
setFqdn: setAdminDomain,
setAdminDomain: setAdminDomain,
setAdminFqdn: setAdminFqdn,
version: version,
database: database,
// these values are derived
adminOrigin: adminOrigin,
internalAdminOrigin: internalAdminOrigin,
sysadminOrigin: sysadminOrigin, // localhost routes
adminFqdn: adminFqdn,
mailFqdn: mailFqdn,
hasIPv6: hasIPv6,
isDemo: isDemo,
// for testing resets to defaults
_reset: _reset
};
var assert = require('assert'),
fs = require('fs'),
path = require('path'),
safe = require('safetydance'),
_ = require('underscore');
// assert on unknown environment can't proceed
assert(exports.CLOUDRON || exports.TEST, 'Unknown environment. This should not happen!');
var data = { };
function baseDir() {
const homeDir = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
if (exports.CLOUDRON) return homeDir;
if (exports.TEST) return path.join(homeDir, '.cloudron_test');
// cannot reach
}
const cloudronConfigFileName = exports.CLOUDRON ? '/etc/cloudron/cloudron.conf' : path.join(baseDir(), 'cloudron.conf');
function saveSync() {
// only save values we want to have in the cloudron.conf, see start.sh
var conf = {
apiServerOrigin: data.apiServerOrigin,
webServerOrigin: data.webServerOrigin,
adminDomain: data.adminDomain,
adminFqdn: data.adminFqdn,
provider: data.provider,
isDemo: data.isDemo
};
fs.writeFileSync(cloudronConfigFileName, JSON.stringify(conf, null, 4)); // functions are ignored by JSON.stringify
}
function _reset(callback) {
safe.fs.unlinkSync(cloudronConfigFileName);
initConfig();
if (callback) callback();
}
function initConfig() {
// setup defaults
data.adminFqdn = '';
data.adminDomain = '';
data.port = 3000;
data.apiServerOrigin = null;
data.webServerOrigin = null;
data.provider = 'generic';
data.smtpPort = 2525; // this value comes from mail container
data.sysadminPort = 3001;
data.ldapPort = 3002;
data.dockerProxyPort = 3003;
// 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
// see setupTest script how the mysql-server is run
data.database.hostname = require('child_process').execSync('docker inspect -f "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" mysql-server').toString().trim();
}
// overwrite defaults with saved config
var existingData = safe.JSON.parse(safe.fs.readFileSync(cloudronConfigFileName, 'utf8'));
_.extend(data, existingData);
}
initConfig();
// set(obj) or set(key, value)
function set(key, value) {
if (typeof key === 'object') {
var obj = key;
for (var k in obj) {
assert(k in data, 'config.js is missing key "' + k + '"');
data[k] = obj[k];
}
} else {
data = safe.set(data, key, value);
}
saveSync();
}
function get(key) {
assert.strictEqual(typeof key, 'string');
return safe.query(data, key);
}
function apiServerOrigin() {
return get('apiServerOrigin');
}
function webServerOrigin() {
return get('webServerOrigin');
}
function setAdminDomain(domain) {
set('adminDomain', domain);
}
function adminDomain() {
return get('adminDomain');
}
function setAdminFqdn(adminFqdn) {
set('adminFqdn', adminFqdn);
}
function adminFqdn() {
return get('adminFqdn');
}
function mailFqdn() {
return adminFqdn();
}
function adminOrigin() {
return 'https://' + adminFqdn();
}
function internalAdminOrigin() {
return 'http://127.0.0.1:' + get('port');
}
function sysadminOrigin() {
return 'http://127.0.0.1:' + get('sysadminPort');
}
function version() {
if (exports.TEST) return '3.0.0-test';
return fs.readFileSync(path.join(__dirname, '../VERSION'), 'utf8').trim();
}
function database() {
return get('database');
}
function isDemo() {
return get('isDemo') === true;
}
function provider() {
return get('provider');
}
function hasIPv6() {
const IPV6_PROC_FILE = '/proc/net/if_inet6';
// on contabo, /proc/net/if_inet6 is an empty file. so just exists is not enough
return fs.existsSync(IPV6_PROC_FILE) && fs.readFileSync(IPV6_PROC_FILE, 'utf8').trim().length !== 0;
}
+18 -1
View File
@@ -1,5 +1,11 @@
'use strict';
let fs = require('fs'),
path = require('path');
const CLOUDRON = process.env.BOX_ENV === 'cloudron',
TEST = process.env.BOX_ENV === 'test';
exports = module.exports = {
SMTP_LOCATION: 'smtp',
IMAP_LOCATION: 'imap',
@@ -18,6 +24,12 @@ exports = module.exports = {
ADMIN_LOCATION: 'my',
PORT: CLOUDRON ? 3000 : 5454,
INTERNAL_SMTP_PORT: 2525, // this value comes from the mail container
SYSADMIN_PORT: 3001,
LDAP_PORT: 3002,
DOCKER_PROXY_PORT: 3003,
NGINX_DEFAULT_CONFIG_FILE_NAME: 'default.conf',
GHOST_USER_FILE: '/tmp/cloudron_ghost.json',
@@ -30,6 +42,11 @@ exports = module.exports = {
AUTOUPDATE_PATTERN_NEVER: 'never',
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8)
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8),
CLOUDRON: CLOUDRON,
TEST: TEST,
VERSION: process.env.BOX_ENV === 'cloudron' ? fs.readFileSync(path.join(__dirname, '../VERSION'), 'utf8').trim() : '4.2.0-test'
};
+13 -3
View File
@@ -15,10 +15,10 @@ var appHealthMonitor = require('./apphealthmonitor.js'),
auditSource = require('./auditsource.js'),
backups = require('./backups.js'),
cloudron = require('./cloudron.js'),
config = require('./config.js'),
constants = require('./constants.js'),
CronJob = require('cron').CronJob,
debug = require('debug')('box:cron'),
disks = require('./disks.js'),
dyndns = require('./dyndns.js'),
eventlog = require('./eventlog.js'),
janitor = require('./janitor.js'),
@@ -35,6 +35,7 @@ var gJobs = {
backup: null,
boxUpdateChecker: null,
systemChecks: null,
diskSpaceChecker: null,
certificateRenew: null,
cleanupBackups: null,
cleanupEventlog: null,
@@ -112,6 +113,15 @@ function recreateJobs(tz) {
timeZone: tz
});
if (gJobs.diskSpaceChecker) gJobs.diskSpaceChecker.stop();
gJobs.diskSpaceChecker = new CronJob({
cronTime: '00 30 * * * *', // every 30 minutes. if you change this interval, change the notification messages with correct duration
onTick: () => disks.checkDiskSpace(NOOP_CALLBACK),
start: true,
runOnInit: true, // run system check immediately
timeZone: tz
});
// randomized pattern per cloudron every hour
var randomMinute = Math.floor(60*Math.random());
@@ -165,7 +175,7 @@ function recreateJobs(tz) {
if (gJobs.schedulerSync) gJobs.schedulerSync.stop();
gJobs.schedulerSync = new CronJob({
cronTime: config.TEST ? '*/10 * * * * *' : '00 */1 * * * *', // every minute
cronTime: constants.TEST ? '*/10 * * * * *' : '00 */1 * * * *', // every minute
onTick: scheduler.sync,
start: true,
timeZone: tz
@@ -248,7 +258,7 @@ function dynamicDnsChanged(enabled) {
if (enabled) {
gJobs.dynamicDns = new CronJob({
cronTime: '00 */10 * * * *',
cronTime: '5 * * * * *', // we only update the records if the ip has changed.
onTick: dyndns.sync.bind(null, auditSource.CRON, NOOP_CALLBACK),
start: true,
timeZone: gJobs.boxUpdateCheckerJob.cronTime.zone // hack
+3 -4
View File
@@ -1,7 +1,6 @@
'use strict';
let config = require('./config.js'),
debug = require('debug')('box:features'),
let debug = require('debug')('box:custom'),
lodash = require('lodash'),
paths = require('./paths.js'),
safe = require('safetydance'),
@@ -32,8 +31,8 @@ const DEFAULT_SPEC = {
remoteSupport: true,
ticketFormBody:
'Use this form to open support tickets. You can also write directly to [support@cloudron.io](mailto:support@cloudron.io).\n\n'
+ `* [Knowledge Base & App Docs](${config.webServerOrigin()}/documentation/apps/?support_view)\n`
+ `* [Custom App Packaging & API](${config.webServerOrigin()}/developer/packaging/?support_view)\n`
+ '* [Knowledge Base & App Docs](https://cloudron.io/documentation/apps/?support_view)\n'
+ '* [Custom App Packaging & API](https://cloudron.io/developer/packaging/?support_view)\n'
+ '* [Forum](https://forum.cloudron.io/)\n\n',
submitTickets: true
},
+24 -11
View File
@@ -15,7 +15,7 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
child_process = require('child_process'),
config = require('./config.js'),
constants = require('./constants.js'),
mysql = require('mysql'),
once = require('once'),
util = require('util');
@@ -23,25 +23,38 @@ var assert = require('assert'),
var gConnectionPool = null,
gDefaultConnection = null;
const gDatabase = {
hostname: '127.0.0.1',
username: 'root',
password: 'password',
port: 3306,
name: 'box'
};
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
if (gConnectionPool !== null) return callback(null);
if (constants.TEST) {
// see setupTest script how the mysql-server is run
gDatabase.hostname = require('child_process').execSync('docker inspect -f "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" mysql-server').toString().trim();
}
gConnectionPool = mysql.createPool({
connectionLimit: 5, // this has to be > 1 since we store one connection as 'default'. the rest for transactions
host: config.database().hostname,
user: config.database().username,
password: config.database().password,
port: config.database().port,
database: config.database().name,
host: gDatabase.hostname,
user: gDatabase.username,
password: gDatabase.password,
port: gDatabase.port,
database: gDatabase.name,
multipleStatements: false,
ssl: false,
timezone: 'Z' // mysql follows the SYSTEM timezone. on Cloudron, this is UTC
});
gConnectionPool.on('connection', function (connection) {
connection.query('USE ' + config.database().name);
connection.query('USE ' + gDatabase.name);
connection.query('SET SESSION sql_mode = \'strict_all_tables\'');
});
@@ -87,8 +100,8 @@ function clear(callback) {
assert.strictEqual(typeof callback, 'function');
var cmd = util.format('mysql --host="%s" --user="%s" --password="%s" -Nse "SHOW TABLES" %s | grep -v "^migrations$" | while read table; do mysql --host="%s" --user="%s" --password="%s" -e "SET FOREIGN_KEY_CHECKS = 0; TRUNCATE TABLE $table" %s; done',
config.database().hostname, config.database().username, config.database().password, config.database().name,
config.database().hostname, config.database().username, config.database().password, config.database().name);
gDatabase.hostname, gDatabase.username, gDatabase.password, gDatabase.name,
gDatabase.hostname, gDatabase.username, gDatabase.password, gDatabase.name);
async.series([
child_process.exec.bind(null, cmd),
@@ -178,7 +191,7 @@ function importFromFile(file, callback) {
assert.strictEqual(typeof file, 'string');
assert.strictEqual(typeof callback, 'function');
var cmd = `/usr/bin/mysql -h "${config.database().hostname}" -u ${config.database().username} -p${config.database().password} ${config.database().name} < ${file}`;
var cmd = `/usr/bin/mysql -h "${gDatabase.hostname}" -u ${gDatabase.username} -p${gDatabase.password} ${gDatabase.name} < ${file}`;
async.series([
query.bind(null, 'CREATE DATABASE IF NOT EXISTS box'),
@@ -190,7 +203,7 @@ function exportToFile(file, callback) {
assert.strictEqual(typeof file, 'string');
assert.strictEqual(typeof callback, 'function');
var cmd = `/usr/bin/mysqldump -h "${config.database().hostname}" -u root -p${config.database().password} --single-transaction --routines --triggers ${config.database().name} > "${file}"`;
var cmd = `/usr/bin/mysqldump -h "${gDatabase.hostname}" -u root -p${gDatabase.password} --single-transaction --routines --triggers ${gDatabase.name} > "${file}"`;
child_process.exec(cmd, callback);
}
+118
View File
@@ -0,0 +1,118 @@
'use strict';
exports = module.exports = {
getDisks: getDisks,
checkDiskSpace: checkDiskSpace
};
const apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
debug = require('debug')('box:disks'),
df = require('@sindresorhus/df'),
docker = require('./docker.js'),
notifications = require('./notifications.js'),
paths = require('./paths.js'),
util = require('util');
function DisksError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
}
util.inherits(DisksError, Error);
DisksError.INTERNAL_ERROR = 'Internal Error';
DisksError.EXTERNAL_ERROR = 'External Error';
function getDisks(callback) {
assert.strictEqual(typeof callback, 'function');
const dfAsync = async.asyncify(df), dfFileAsync = async.asyncify(df.file);
docker.info(function (error, info) {
if (error) return callback(new DisksError(DisksError.INTERNAL_ERROR, error));
async.series([
dfAsync,
dfFileAsync.bind(null, paths.BOX_DATA_DIR),
dfFileAsync.bind(null, paths.PLATFORM_DATA_DIR),
dfFileAsync.bind(null, paths.APPS_DATA_DIR),
dfFileAsync.bind(null, info.DockerRootDir)
], function (error, values) {
if (error) return callback(new DisksError(DisksError.INTERNAL_ERROR, error));
// filter by ext4 and then sort to make sure root disk is first
const ext4Disks = values[0].filter((r) => r.type === 'ext4').sort((a, b) => a.mountpoint.localeCompare(b.mountpoint));
const disks = {
disks: ext4Disks, // root disk is first
boxDataDisk: values[1].filesystem,
mailDataDisk: values[1].filesystem,
platformDataDisk: values[2].filesystem,
appsDataDisk: values[3].filesystem,
dockerDataDisk: values[4].filesystem,
apps: {}
};
apps.getAll(function (error, allApps) {
if (error) return callback(new DisksError(DisksError.INTERNAL_ERROR, error));
async.eachSeries(allApps, function (app, iteratorDone) {
if (!app.dataDir) {
disks.apps[app.id] = disks.appsDataDisk;
return iteratorDone();
}
dfFileAsync(app.dataDir, function (error, result) {
disks.apps[app.id] = error ? disks.appsDataDisk : result.filesystem; // ignore any errors
iteratorDone();
});
}, function (error) {
if (error) return callback(new DisksError(DisksError.INTERNAL_ERROR, error));
callback(null, disks);
});
});
});
});
}
function checkDiskSpace(callback) {
assert.strictEqual(typeof callback, 'function');
debug('Checking disk space');
getDisks(function (error, disks) {
if (error) {
debug('checkDiskSpace: error getting disks %s', error.message);
return callback();
}
var oos = disks.disks.some(function (entry) {
// ignore other filesystems but where box, app and platform data is
if (entry.filesystem !== disks.boxDataDisk
&& entry.filesystem !== disks.platformDataDisk
&& entry.filesystem !== disks.appsDataDisk
&& entry.filesystem !== disks.dockerDataDisk) return false;
return (entry.available <= (1.25 * 1024 * 1024 * 1024)); // 1.5G
});
debug('checkDiskSpace: disk space checked. ok: %s', !oos);
notifications.alert(notifications.ALERT_DISK_SPACE, 'Server is running out of disk space', oos ? JSON.stringify(disks.disks, null, 4) : '', callback);
});
}
+4 -4
View File
@@ -11,10 +11,10 @@ exports = module.exports = {
};
var assert = require('assert'),
config = require('../config.js'),
debug = require('debug')('box:dns/caas'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
settings = require('../settings.js'),
superagent = require('superagent'),
util = require('util'),
waitForDns = require('./waitfordns.js');
@@ -58,7 +58,7 @@ function upsert(domainObject, location, type, values, callback) {
};
superagent
.post(config.apiServerOrigin() + '/api/v1/caas/domains/' + fqdn)
.post(settings.apiServerOrigin() + '/api/v1/caas/domains/' + fqdn)
.query({ token: dnsConfig.token })
.send(data)
.timeout(30 * 1000)
@@ -84,7 +84,7 @@ function get(domainObject, location, type, callback) {
debug('get: zoneName: %s subdomain: %s type: %s fqdn: %s', domainObject.domain, location, type, fqdn);
superagent
.get(config.apiServerOrigin() + '/api/v1/caas/domains/' + fqdn)
.get(settings.apiServerOrigin() + '/api/v1/caas/domains/' + fqdn)
.query({ token: dnsConfig.token, type: type })
.timeout(30 * 1000)
.end(function (error, result) {
@@ -111,7 +111,7 @@ function del(domainObject, location, type, values, callback) {
};
superagent
.del(config.apiServerOrigin() + '/api/v1/caas/domains/' + getFqdn(location, domainObject.domain))
.del(settings.apiServerOrigin() + '/api/v1/caas/domains/' + getFqdn(location, domainObject.domain))
.query({ token: dnsConfig.token })
.send(data)
.timeout(30 * 1000)
+43 -28
View File
@@ -8,6 +8,7 @@ exports = module.exports = {
ping: ping,
info: info,
downloadImage: downloadImage,
createContainer: createContainer,
startContainer: startContainer,
@@ -33,18 +34,7 @@ exports = module.exports = {
// timeout is optional
function connectionInstance(timeout) {
var Docker = require('dockerode');
var docker;
if (process.env.BOX_ENV === 'test') {
// test code runs a docker proxy on this port
docker = new Docker({ host: 'http://localhost', port: 5687, timeout: timeout });
// proxy code uses this to route to the real docker
docker.options = { socketPath: '/var/run/docker.sock' };
} else {
docker = new Docker({ socketPath: '/var/run/docker.sock', timeout: timeout });
}
var docker = new Docker({ socketPath: '/var/run/docker.sock', timeout: timeout });
return docker;
}
@@ -52,12 +42,13 @@ var addons = require('./addons.js'),
async = require('async'),
assert = require('assert'),
child_process = require('child_process'),
config = require('./config.js'),
constants = require('./constants.js'),
debug = require('debug')('box:docker.js'),
once = require('once'),
path = require('path'),
settings = require('./settings.js'),
shell = require('./shell.js'),
safe = require('safetydance'),
spawn = child_process.spawn,
util = require('util'),
_ = require('underscore');
@@ -85,10 +76,11 @@ function DockerError(reason, errorOrMessage) {
}
util.inherits(DockerError, Error);
DockerError.INTERNAL_ERROR = 'Internal Error';
DockerError.EXTERNAL_ERROR = 'External Error';
DockerError.NOT_FOUND = 'Not found';
DockerError.BAD_FIELD = 'Bad field';
function debugApp(app, args) {
function debugApp(app) {
assert(typeof app === 'object');
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
@@ -129,23 +121,32 @@ function pullImage(manifest, callback) {
// Use docker CLI here to support downloading of private repos. for dockerode, we have to use
// https://github.com/apocas/dockerode#pull-from-private-repos
shell.spawn('pullImage', '/usr/bin/docker', [ 'pull', manifest.dockerImage ], {}, function (error) {
if (error) {
debug(`pullImage: Error pulling image ${manifest.dockerImage} of ${manifest.id}: ${error.message}`);
return callback(new Error('Failed to pull image'));
}
docker.pull(manifest.dockerImage, function (error, stream) {
if (error) return callback(new DockerError(DockerError.EXTERNAL_ERROR, 'Unable to pull image. statusCode: ' + error.statusCode));
var image = docker.getImage(manifest.dockerImage);
// https://github.com/dotcloud/docker/issues/1074 says each status message
// is emitted as a chunk
stream.on('data', function (chunk) {
var data = safe.JSON.parse(chunk) || { };
debug('pullImage %s: %j', manifest.id, data);
image.inspect(function (err, data) {
if (err) return callback(new Error('Error inspecting image:' + err.message));
if (!data || !data.Config) return callback(new Error('Missing Config in image:' + JSON.stringify(data, null, 4)));
if (!data.Config.Entrypoint && !data.Config.Cmd) return callback(new Error('Only images with entry point are allowed'));
// The data.status here is useless because this is per layer as opposed to per image
if (!data.status && data.error) {
debug('pullImage error %s: %s', manifest.id, data.errorDetail.message);
}
});
if (data.Config.ExposedPorts) debug('This image of %s exposes ports: %j', manifest.id, data.Config.ExposedPorts);
stream.on('end', function () {
debug('downloaded image %s of %s successfully', manifest.dockerImage, manifest.id);
callback(null);
});
stream.on('error', function (error) {
debug('error pulling image %s of %s: %j', manifest.dockerImage, manifest.id, error);
callback(new DockerError(DockerError.EXTERNAL_ERROR, error.message));
});
});
}
@@ -189,8 +190,9 @@ function createSubcontainer(app, name, cmd, options, callback) {
'CLOUDRON=1',
'CLOUDRON_PROXY_IP=172.18.0.1',
`CLOUDRON_APP_HOSTNAME=${app.id}`,
`${envPrefix}WEBADMIN_ORIGIN=${config.adminOrigin()}`,
`${envPrefix}API_ORIGIN=${config.adminOrigin()}`,
`CLOUDRON_ADMIN_EMAIL=${app.adminEmail}`,
`${envPrefix}WEBADMIN_ORIGIN=${settings.adminOrigin()}`,
`${envPrefix}API_ORIGIN=${settings.adminOrigin()}`,
`${envPrefix}APP_ORIGIN=https://${domain}`,
`${envPrefix}APP_DOMAIN=${domain}`
];
@@ -320,7 +322,8 @@ function startContainer(containerId, callback) {
debug('Starting container %s', containerId);
container.start(function (error) {
if (error && error.statusCode !== 304) return callback(new Error('Error starting container :' + error));
if (error && error.statusCode === 404) return callback(new DockerError(DockerError.NOT_FOUND));
if (error && error.statusCode !== 304) return callback(new DockerError(DockerError.INTERNAL_ERROR, error));
return callback(null);
});
@@ -610,3 +613,15 @@ function removeVolume(app, name, callback) {
callback();
});
}
function info(callback) {
assert.strictEqual(typeof callback, 'function');
let docker = exports.connection;
docker.info(function (error, result) {
if (error) return callback(new DockerError(DockerError.EXTERNAL_ERROR, 'Error connecting to docker. statusCode: ' + error.statusCode));
callback(null, result);
});
}
+6 -6
View File
@@ -8,7 +8,7 @@ exports = module.exports = {
var apps = require('./apps.js'),
AppsError = apps.AppsError,
assert = require('assert'),
config = require('./config.js'),
constants = require('./constants.js'),
express = require('express'),
debug = require('debug')('box:dockerproxy'),
http = require('http'),
@@ -29,7 +29,7 @@ function authorizeApp(req, res, next) {
// - only allow managing and inspection of containers belonging to the app
// make the tests pass for now
if (config.TEST) {
if (constants.TEST) {
req.app = { id: 'testappid' };
return next();
}
@@ -120,7 +120,7 @@ function start(callback) {
let proxyServer = express();
if (config.TEST) {
if (constants.TEST) {
proxyServer.use(function (req, res, next) {
debug('proxying: ' + req.method, req.url);
next();
@@ -135,9 +135,9 @@ function start(callback) {
.use(middleware.lastMile());
gHttpServer = http.createServer(proxyServer);
gHttpServer.listen(config.get('dockerProxyPort'), '0.0.0.0', callback);
gHttpServer.listen(constants.DOCKER_PROXY_PORT, '0.0.0.0', callback);
debug(`startDockerProxy: started proxy on port ${config.get('dockerProxyPort')}`);
debug(`startDockerProxy: started proxy on port ${constants.DOCKER_PROXY_PORT}`);
gHttpServer.on('upgrade', function (req, client, head) {
// Create a new tcp connection to the TCP server
@@ -150,7 +150,7 @@ function start(callback) {
if (req.headers['content-type'] === 'application/json') {
// TODO we have to parse the immediate upgrade request body, but I don't know how
let plainBody = '{"Detach":false,"Tty":false}\r\n';
upgradeMessage += `Content-Type: application/json\r\n`;
upgradeMessage += 'Content-Type: application/json\r\n';
upgradeMessage += `Content-Length: ${Buffer.byteLength(plainBody)}\r\n`;
upgradeMessage += '\r\n';
upgradeMessage += plainBody;
+8 -6
View File
@@ -35,7 +35,6 @@ module.exports = exports = {
var assert = require('assert'),
async = require('async'),
config = require('./config.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:domains'),
@@ -44,6 +43,7 @@ var assert = require('assert'),
reverseProxy = require('./reverseproxy.js'),
ReverseProxyError = reverseProxy.ReverseProxyError,
safe = require('safetydance'),
settings = require('./settings.js'),
sysinfo = require('./sysinfo.js'),
tld = require('tldjs'),
util = require('util'),
@@ -150,7 +150,7 @@ function validateHostname(location, domainObject) {
];
if (RESERVED_LOCATIONS.indexOf(location) !== -1) return new DomainsError(DomainsError.BAD_FIELD, location + ' is reserved');
if (hostname === config.adminFqdn()) return new DomainsError(DomainsError.BAD_FIELD, location + ' is reserved');
if (hostname === settings.adminFqdn()) return new DomainsError(DomainsError.BAD_FIELD, location + ' is reserved');
// workaround https://github.com/oncletom/tld.js/issues/73
var tmp = hostname.replace('_', '-');
@@ -343,7 +343,7 @@ function del(domain, auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
if (domain === config.adminDomain()) return callback(new DomainsError(DomainsError.IN_USE));
if (domain === settings.adminDomain()) return callback(new DomainsError(DomainsError.IN_USE));
domaindb.del(domain, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainsError(DomainsError.NOT_FOUND));
@@ -493,15 +493,17 @@ function prepareDashboardDomain(domain, auditSource, progressCallback, callback)
get(domain, function (error, domainObject) {
if (error) return callback(error);
const adminFqdn = fqdn(constants.ADMIN_LOCATION, domainObject);
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error.message));
async.series([
(done) => { progressCallback({ percent: 10, message: 'Updating DNS' }); done(); },
(done) => { progressCallback({ percent: 10, message: `Updating DNS of ${adminFqdn}` }); done(); },
upsertDnsRecords.bind(null, constants.ADMIN_LOCATION, domain, 'A', [ ip ]),
(done) => { progressCallback({ percent: 40, message: 'Waiting for DNS' }); done(); },
(done) => { progressCallback({ percent: 40, message: `Waiting for DNS of ${adminFqdn}` }); done(); },
waitForDnsRecord.bind(null, constants.ADMIN_LOCATION, domain, 'A', ip, { interval: 30000, times: 50000 }),
(done) => { progressCallback({ percent: 70, message: 'Getting certificate' }); done(); },
(done) => { progressCallback({ percent: 70, message: `Getting certificate of ${adminFqdn}` }); done(); },
reverseProxy.ensureCertificate.bind(null, fqdn(constants.ADMIN_LOCATION, domainObject), domain, auditSource)
], function (error) {
if (error) return callback(error);
+4 -5
View File
@@ -4,17 +4,16 @@ exports = module.exports = {
sync: sync
};
var appdb = require('./appdb.js'),
apps = require('./apps.js'),
let apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
config = require('./config.js'),
constants = require('./constants.js'),
debug = require('debug')('box:dyndns'),
domains = require('./domains.js'),
eventlog = require('./eventlog.js'),
paths = require('./paths.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
sysinfo = require('./sysinfo.js');
// called for dynamic dns setups where we have to update the IP
@@ -33,7 +32,7 @@ function sync(auditSource, callback) {
debug(`refreshDNS: updating ip from ${info.ip} to ${ip}`);
domains.upsertDnsRecords(constants.ADMIN_LOCATION, config.adminDomain(), 'A', [ ip ], function (error) {
domains.upsertDnsRecords(constants.ADMIN_LOCATION, settings.adminDomain(), 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('refreshDNS: updated admin location');
@@ -43,7 +42,7 @@ function sync(auditSource, callback) {
async.each(result, function (app, callback) {
// do not change state of installing apps since apptask will error if dns record already exists
if (app.installationState !== appdb.ISTATE_INSTALLED) return callback();
if (app.installationState !== apps.ISTATE_INSTALLED) return callback();
domains.upsertDnsRecords(app.location, app.domain, 'A', [ ip ], callback);
}, function (error) {
+1 -3
View File
@@ -22,9 +22,6 @@ exports = module.exports = {
ACTION_APP_OOM: 'app.oom',
ACTION_APP_UP: 'app.up',
ACTION_APP_DOWN: 'app.down',
ACTION_APP_TASK_START: 'app.task.start',
ACTION_APP_TASK_CRASH: 'app.task.crash',
ACTION_APP_TASK_SUCCESS: 'app.task.success',
ACTION_BACKUP_FINISH: 'backup.finish',
ACTION_BACKUP_START: 'backup.start',
@@ -51,6 +48,7 @@ exports = module.exports = {
ACTION_RESTORE: 'cloudron.restore', // unused
ACTION_START: 'cloudron.start',
ACTION_UPDATE: 'cloudron.update',
ACTION_UPDATE_FINISH: 'cloudron.update.finish',
ACTION_USER_ADD: 'user.add',
ACTION_USER_LOGIN: 'user.login',
+231
View File
@@ -0,0 +1,231 @@
'use strict';
exports = module.exports = {
ExternalLdapError: ExternalLdapError,
verifyPassword: verifyPassword,
testConfig: testConfig,
startSyncer: startSyncer,
sync: sync
};
var assert = require('assert'),
async = require('async'),
auditsource = require('./auditsource.js'),
debug = require('debug')('box:externalldap'),
ldap = require('ldapjs'),
settings = require('./settings.js'),
tasks = require('./tasks.js'),
users = require('./users.js'),
UserError = users.UsersError,
util = require('util');
function ExternalLdapError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
}
util.inherits(ExternalLdapError, Error);
ExternalLdapError.EXTERNAL_ERROR = 'external error';
ExternalLdapError.INTERNAL_ERROR = 'internal error';
ExternalLdapError.INVALID_CREDENTIALS = 'invalid credentials';
ExternalLdapError.BAD_STATE = 'bad state';
ExternalLdapError.BAD_FIELD = 'bad field';
ExternalLdapError.NOT_FOUND = 'not found';
// performs service bind if required
function getClient(externalLdapConfig, callback) {
assert.strictEqual(typeof callback, 'function');
// basic validation to not crash
try { ldap.parseDN(externalLdapConfig.baseDn); } catch (e) { return callback(new ExternalLdapError(ExternalLdapError.BAD_FIELD, 'invalid baseDn')); }
try { ldap.parseFilter(externalLdapConfig.filter); } catch (e) { return callback(new ExternalLdapError(ExternalLdapError.BAD_FIELD, 'invalid filter')); }
if (externalLdapConfig.bindDn) try { ldap.parseFilter(externalLdapConfig.bindDn); } catch (e) { return callback(new ExternalLdapError(ExternalLdapError.INVALID_CREDENTIALS)); }
var client;
try {
client = ldap.createClient({ url: externalLdapConfig.url });
} catch (e) {
if (e instanceof ldap.ProtocolError) return callback(new ExternalLdapError(ExternalLdapError.BAD_FIELD, 'url protocol is invalid'));
return callback(new ExternalLdapError(ExternalLdapError.INTERNAL_ERROR, e));
}
if (!externalLdapConfig.bindDn) return callback(null, client);
client.bind(externalLdapConfig.bindDn, externalLdapConfig.bindPassword, function (error) {
if (error instanceof ldap.InvalidCredentialsError) return callback(new ExternalLdapError(ExternalLdapError.INVALID_CREDENTIALS));
if (error) return callback(new ExternalLdapError(ExternalLdapError.EXTERNAL_ERROR, error));
callback(null, client, externalLdapConfig);
});
}
function testConfig(config, callback) {
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof callback, 'function');
if (!config.enabled) return callback();
if (!config.url) return callback(new ExternalLdapError(ExternalLdapError.BAD_FIELD, 'url must not be empty'));
if (!config.baseDn) return callback(new ExternalLdapError(ExternalLdapError.BAD_FIELD, 'basedn must not be empty'));
if (!config.filter) return callback(new ExternalLdapError(ExternalLdapError.BAD_FIELD, 'filter must not be empty'));
getClient(config, function (error, client) {
if (error) return callback(error);
var opts = {
filter: config.filter,
scope: 'sub'
};
client.search(config.baseDn, opts, function (error, result) {
if (error) return callback(new ExternalLdapError(ExternalLdapError.EXTERNAL_ERROR, error));
result.on('searchEntry', function (entry) {});
result.on('error', function (error) { callback(new ExternalLdapError(ExternalLdapError.BAD_FIELD, 'Unable to search directory')); });
result.on('end', function (result) { callback(); });
});
});
}
function verifyPassword(user, password, callback) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof callback, 'function');
settings.getExternalLdapConfig(function (error, externalLdapConfig) {
if (error) return callback(new ExternalLdapError(ExternalLdapError.INTERNAL_ERROR, error));
if (!externalLdapConfig.enabled) return callback(new ExternalLdapError(ExternalLdapError.BAD_STATE, 'not enabled'));
getClient(externalLdapConfig, function (error, client) {
if (error) return callback(error);
const dn = `uid=${user.username},${externalLdapConfig.baseDn}`;
client.bind(dn, password, function (error) {
if (error instanceof ldap.InvalidCredentialsError) return callback(new ExternalLdapError(ExternalLdapError.INVALID_CREDENTIALS));
if (error) return callback(new ExternalLdapError(ExternalLdapError.EXTERNAL_ERROR, error));
callback();
});
});
});
}
function startSyncer(callback) {
assert.strictEqual(typeof callback, 'function');
settings.getExternalLdapConfig(function (error, externalLdapConfig) {
if (error) return callback(new ExternalLdapError(ExternalLdapError.INTERNAL_ERROR, error));
if (!externalLdapConfig.enabled) return callback(new ExternalLdapError(ExternalLdapError.BAD_STATE, 'not enabled'));
tasks.add(tasks.TASK_SYNC_EXTERNAL_LDAP, [], function (error, taskId) {
if (error) return callback(new ExternalLdapError(ExternalLdapError.INTERNAL_ERROR, error));
tasks.startTask(taskId, {}, function (error, result) {
debug('sync: done', error, result);
});
callback(null, taskId);
});
});
}
function sync(progressCallback, callback) {
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
debug('Start user syncing ...');
settings.getExternalLdapConfig(function (error, externalLdapConfig) {
if (error) return callback(new ExternalLdapError(ExternalLdapError.INTERNAL_ERROR, error));
if (!externalLdapConfig.enabled) return callback(new ExternalLdapError(ExternalLdapError.BAD_STATE, 'not enabled'));
getClient(externalLdapConfig, function (error, client) {
if (error) return callback(error);
var opts = {
paged: true,
filter: externalLdapConfig.filter,
scope: 'sub' // We may have to make this configurable
};
debug(`Listing users at ${externalLdapConfig.baseDn} with filter ${externalLdapConfig.filter}`);
client.search(externalLdapConfig.baseDn, opts, function (error, result) {
if (error) return callback(new ExternalLdapError(ExternalLdapError.EXTERNAL_ERROR, error));
var ldapUsers = [];
result.on('searchEntry', function (entry) {
ldapUsers.push(entry.object);
});
result.on('error', function (error) {
callback(new ExternalLdapError(ExternalLdapError.EXTERNAL_ERROR, error));
});
result.on('end', function (result) {
if (result.status !== 0) return callback(new ExternalLdapError(ExternalLdapError.EXTERNAL_ERROR, 'Server returned status ' + result.status));
debug(`Found ${ldapUsers.length} users`);
// we ignore all errors here and just log them for now
async.eachSeries(ldapUsers, function (user, callback) {
// ignore the bindDn user if any
if (user.dn === externalLdapConfig.bindDn) return callback();
users.getByUsername(user.uid, function (error, result) {
if (error && error.reason !== UserError.NOT_FOUND) {
console.error(error);
return callback();
}
if (error) {
debug('[adding user] ', user.uid, user.mail, user.cn);
users.create(user.uid, null, user.mail, user.cn, { source: 'ldap' }, auditsource.EXTERNAL_LDAP_TASK, function (error) {
if (error) console.error('Failed to create user', user, error);
callback();
});
} else if (result.source !== 'ldap') {
debug('[conflicting user]', user.uid, user.mail, user.cn);
callback();
} else if (result.email !== user.mail || result.displayName !== user.cn) {
debug('[updating user] ', user.uid, user.mail, user.cn);
users.update(result.id, { email: user.mail, fallbackEmail: user.mail, displayName: user.cn }, auditsource.EXTERNAL_LDAP_TASK, function (error) {
if (error) console.error('Failed to update user', user, error);
callback();
});
} else {
// user known and up-to-date
callback();
}
});
}, function () {
debug('User sync done.');
callback();
});
});
});
});
});
}
+1 -1
View File
@@ -20,7 +20,7 @@ exports = module.exports = {
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:2.1.0@sha256:6d1bf221cfe6124957e2c58b57c0a47214353496009296acb16adf56df1da9d5' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.0.0@sha256:8a88dd334b62b578530a014ca1a2425a54cb9df1e475f5d3a36806e5cfa22121' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.3.1@sha256:9693e3ae42a12a7ac8cf5df94d828d46f5b22b4e2e1c7d1bc614d6ee2a22c365' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.0.2@sha256:454f035d60b768153d4f31210380271b5ba1c09367c9d95c7fa37f9e39d2f59c' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.1.0@sha256:cf0e00d6fadfb5ece0f4beaa182f3c9d57d62817f5b4571077f2cea71b6992c3' },
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:0.1.0@sha256:e177c5bf5f38c84ce1dea35649c22a1b05f96eec67a54a812c5a35e585670f0f' }
}
};
+1 -1
View File
@@ -63,7 +63,7 @@ function cleanupTmpVolume(containerInfo, callback) {
assert.strictEqual(typeof containerInfo, 'object');
assert.strictEqual(typeof callback, 'function');
var cmd = 'find /tmp -mtime +10 -exec rm -rf {} +'.split(' '); // 10 days old
var cmd = 'find /tmp -type f -mtime +10 -exec rm -rf {} +'.split(' '); // 10 day old files
debug('cleanupTmpVolume %j', containerInfo.Names);
+3 -3
View File
@@ -9,7 +9,7 @@ var assert = require('assert'),
appdb = require('./appdb.js'),
apps = require('./apps.js'),
async = require('async'),
config = require('./config.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:ldap'),
eventlog = require('./eventlog.js'),
@@ -371,7 +371,7 @@ function mailingListSearch(req, res, next) {
var parts = email.split('@');
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
mailboxdb.getGroup(parts[0], parts[1], function (error, group) {
mailboxdb.getList(parts[0], parts[1], 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()));
@@ -640,7 +640,7 @@ function start(callback) {
res.end();
});
gServer.listen(config.get('ldapPort'), '0.0.0.0', callback);
gServer.listen(constants.LDAP_PORT, '0.0.0.0', callback);
}
function stop(callback) {
+5 -1
View File
@@ -22,7 +22,11 @@ Locker.prototype.OP_APPTASK = 'apptask';
Locker.prototype.lock = function (operation) {
assert.strictEqual(typeof operation, 'string');
if (this._operation !== null) return new Error('Already locked for ' + this._operation);
if (this._operation !== null) {
let error = new Error(`Locked for ${this._operation}`);
error.operation = this._operation;
return error;
}
this._operation = operation;
++this._lockDepth;
+20 -8
View File
@@ -1,11 +1,23 @@
# Generated by apptask for the /run mount
# Generated by apptask
# keep upto 7 rotated logs. rotation triggered daily or ahead of time if size is > 1M
<%= volumePath %>/*.log <%= volumePath %>/*/*.log <%= volumePath %>/*/*/*.log {
rotate 7
daily
compress
maxsize=1M
missingok
delaycompress
copytruncate
rotate 7
daily
compress
maxsize 1M
missingok
delaycompress
copytruncate
}
/home/yellowtent/platformdata/logs/<%= appId %>/*.log {
# only keep one rotated file, we currently do not send that over the api
rotate 1
size 10M
missingok
# we never compress so we can simply tail the files
nocompress
copytruncate
}
+16 -16
View File
@@ -53,7 +53,6 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
config = require('./config.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:mail'),
@@ -71,6 +70,7 @@ var assert = require('assert'),
paths = require('./paths.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
shell = require('./shell.js'),
smtpTransport = require('nodemailer-smtp-transport'),
sysinfo = require('./sysinfo.js'),
@@ -262,14 +262,14 @@ function checkSpf(domain, mailFqdn, callback) {
let txtRecord = txtRecords[i].join(''); // https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
if (txtRecord.indexOf('v=spf1 ') !== 0) continue; // not SPF
spf.value = txtRecord;
spf.status = spf.value.indexOf(' a:' + config.adminFqdn()) !== -1;
spf.status = spf.value.indexOf(' a:' + settings.adminFqdn()) !== -1;
break;
}
if (spf.status) {
spf.expected = spf.value;
} else if (i !== txtRecords.length) {
spf.expected = 'v=spf1 a:' + config.adminFqdn() + ' ' + spf.value.slice('v=spf1 '.length);
spf.expected = 'v=spf1 a:' + settings.adminFqdn() + ' ' + spf.value.slice('v=spf1 '.length);
}
callback(null, spf);
@@ -496,7 +496,7 @@ function getStatus(domain, callback) {
};
}
const mailFqdn = config.mailFqdn();
const mailFqdn = settings.mailFqdn();
getDomain(domain, function (error, mailDomain) {
if (error) return callback(error);
@@ -567,7 +567,7 @@ function checkConfiguration(callback) {
markdownMessage += '\n\n';
});
if (markdownMessage) markdownMessage += 'Email Status is checked every 30 minutes\n See the [troubleshooting docs](https://cloudron.io/documentation/troubleshooting/#mail-dns) for more information.\n';
if (markdownMessage) markdownMessage += 'Email Status is checked every 30 minutes.\n See the [troubleshooting docs](https://cloudron.io/documentation/troubleshooting/#mail-dns) for more information.\n';
callback(null, markdownMessage); // empty message means all status checks succeeded
});
@@ -690,8 +690,8 @@ function restartMail(callback) {
if (process.env.BOX_ENV === 'test' && !process.env.TEST_CREATE_INFRA) return callback();
debug(`restartMail: restarting mail container with ${config.mailFqdn()} ${config.adminDomain()}`);
configureMail(config.mailFqdn(), config.adminDomain(), callback);
debug(`restartMail: restarting mail container with ${settings.mailFqdn()} ${settings.adminDomain()}`);
configureMail(settings.mailFqdn(), settings.adminDomain(), callback);
}
function restartMailIfActivated(callback) {
@@ -882,14 +882,14 @@ function setDnsRecords(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
upsertDnsRecords(domain, config.mailFqdn(), callback);
upsertDnsRecords(domain, settings.mailFqdn(), callback);
}
function onMailFqdnChanged(callback) {
assert.strictEqual(typeof callback, 'function');
const mailFqdn = config.mailFqdn(),
mailDomain = config.adminDomain();
const mailFqdn = settings.mailFqdn(),
mailDomain = settings.adminDomain();
domains.getAll(function (error, allDomains) {
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
@@ -908,7 +908,7 @@ function addDomain(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
const dkimSelector = domain === config.adminDomain() ? 'cloudron' : ('cloudron-' + config.adminDomain().replace(/\./g, ''));
const dkimSelector = domain === settings.adminDomain() ? 'cloudron' : ('cloudron-' + settings.adminDomain().replace(/\./g, ''));
maildb.add(domain, { dkimSelector }, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new MailError(MailError.ALREADY_EXISTS, 'Domain already exists'));
@@ -916,7 +916,7 @@ function addDomain(domain, callback) {
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
async.series([
upsertDnsRecords.bind(null, domain, config.mailFqdn()), // do this first to ensure DKIM keys
upsertDnsRecords.bind(null, domain, settings.mailFqdn()), // do this first to ensure DKIM keys
restartMailIfActivated
], NOOP_CALLBACK); // do these asynchronously
@@ -928,7 +928,7 @@ function removeDomain(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
if (domain === config.adminDomain()) return callback(new MailError(MailError.IN_USE));
if (domain === settings.adminDomain()) return callback(new MailError(MailError.IN_USE));
maildb.del(domain, function (error) {
if (error && error.reason === DatabaseError.IN_USE) return callback(new MailError(MailError.IN_USE));
@@ -1203,7 +1203,7 @@ function getLists(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
mailboxdb.listGroups(domain, function (error, result) {
mailboxdb.getLists(domain, function (error, result) {
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
callback(null, result);
@@ -1215,7 +1215,7 @@ function getList(domain, listName, callback) {
assert.strictEqual(typeof listName, 'string');
assert.strictEqual(typeof callback, 'function');
mailboxdb.getGroup(listName, domain, function (error, result) {
mailboxdb.getList(listName, domain, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such list'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
@@ -1242,7 +1242,7 @@ function addList(name, domain, members, auditSource, callback) {
if (error) return callback(error);
}
mailboxdb.addGroup(name, domain, members, function (error) {
mailboxdb.addList(name, domain, members, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new MailError(MailError.ALREADY_EXISTS, 'list already exits'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
+6 -6
View File
@@ -2,7 +2,7 @@
exports = module.exports = {
addMailbox: addMailbox,
addGroup: addGroup,
addList: addList,
updateMailboxOwner: updateMailboxOwner,
updateList: updateList,
@@ -10,10 +10,10 @@ exports = module.exports = {
listAliases: listAliases,
listMailboxes: listMailboxes,
listGroups: listGroups,
getLists: getLists,
getMailbox: getMailbox,
getGroup: getGroup,
getList: getList,
getAlias: getAlias,
getAliasesForName: getAliasesForName,
@@ -75,7 +75,7 @@ function updateMailboxOwner(name, domain, ownerId, callback) {
});
}
function addGroup(name, domain, members, callback) {
function addList(name, domain, members, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert(Array.isArray(members));
@@ -197,7 +197,7 @@ function listMailboxes(domain, callback) {
});
}
function listGroups(domain, callback) {
function getLists(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -211,7 +211,7 @@ function listGroups(domain, callback) {
});
}
function getGroup(name, domain, callback) {
function getList(name, domain, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
+13 -13
View File
@@ -24,7 +24,7 @@ exports = module.exports = {
};
var assert = require('assert'),
config = require('./config.js'),
constants = require('./constants.js'),
custom = require('./custom.js'),
debug = require('debug')('box:mailer'),
docker = require('./docker.js').connection,
@@ -54,7 +54,7 @@ function getMailConfig(callback) {
callback(null, {
cloudronName: cloudronName,
notificationFrom: `"${cloudronName}" <no-reply@${config.adminDomain()}>`
notificationFrom: `"${cloudronName}" <no-reply@${settings.adminDomain()}>`
});
});
}
@@ -84,9 +84,9 @@ function sendMail(mailOptions, callback) {
var transport = nodemailer.createTransport(smtpTransport({
host: mailServerIp,
port: config.get('smtpPort'),
port: constants.INTERNAL_SMTP_PORT,
auth: {
user: mailOptions.authUser || `no-reply@${config.adminDomain()}`,
user: mailOptions.authUser || `no-reply@${settings.adminDomain()}`,
pass: relayToken
}
}));
@@ -146,11 +146,11 @@ function sendInvite(user, invitor) {
var templateData = {
user: user,
webadminUrl: config.adminOrigin(),
setupLink: `${config.adminOrigin()}/api/v1/session/account/setup.html?reset_token=${user.resetToken}&email=${encodeURIComponent(user.email)}`,
webadminUrl: settings.adminOrigin(),
setupLink: `${settings.adminOrigin()}/api/v1/session/account/setup.html?reset_token=${user.resetToken}&email=${encodeURIComponent(user.email)}`,
invitor: invitor,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateDataText = JSON.parse(JSON.stringify(templateData));
@@ -183,7 +183,7 @@ function userAdded(mailTo, user) {
var templateData = {
user: user,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateDataText = JSON.parse(JSON.stringify(templateData));
@@ -233,9 +233,9 @@ function passwordReset(user) {
var templateData = {
user: user,
resetLink: `${config.adminOrigin()}/api/v1/session/password/reset.html?reset_token=${user.resetToken}&email=${encodeURIComponent(user.email)}`,
resetLink: `${settings.adminOrigin()}/api/v1/session/password/reset.html?reset_token=${user.resetToken}&email=${encodeURIComponent(user.email)}`,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateDataText = JSON.parse(JSON.stringify(templateData));
@@ -314,7 +314,7 @@ function appUpdated(mailTo, app, callback) {
changelog: app.manifest.changelog,
changelogHTML: converter.makeHtml(app.manifest.changelog),
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateDataText = JSON.parse(JSON.stringify(templateData));
@@ -350,11 +350,11 @@ function appUpdatesAvailable(mailTo, apps, hasSubscription, callback) {
});
var templateData = {
webadminUrl: config.adminOrigin(),
webadminUrl: settings.adminOrigin(),
hasSubscription: hasSubscription,
apps: apps,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateDataText = JSON.parse(JSON.stringify(templateData));
+2 -2
View File
@@ -5,7 +5,7 @@ exports = module.exports = {
};
var assert = require('assert'),
config = require('./config.js'),
constants = require('./constants.js'),
dns = require('dns'),
_ = require('underscore');
@@ -24,7 +24,7 @@ function resolve(hostname, rrtype, options, callback) {
options = _.extend({ }, DEFAULT_OPTIONS, options);
// Only use unbound on a Cloudron
if (config.CLOUDRON) resolver.setServers([ options.server ]);
if (constants.CLOUDRON) resolver.setServers([ options.server ]);
// should callback with ECANCELLED but looks like we might hit https://github.com/nodejs/node/issues/14814
const timerId = setTimeout(resolver.cancel.bind(resolver), options.timeout || 5000);
+33 -7
View File
@@ -24,13 +24,15 @@ exports = module.exports = {
let assert = require('assert'),
async = require('async'),
config = require('./config.js'),
auditsource = require('./auditsource.js'),
changelog = require('./changelog.js'),
custom = require('./custom.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:notifications'),
eventlog = require('./eventlog.js'),
mailer = require('./mailer.js'),
notificationdb = require('./notificationdb.js'),
settings = require('./settings.js'),
users = require('./users.js'),
util = require('util');
@@ -144,7 +146,7 @@ function userAdded(performedBy, eventId, user, callback) {
actionForAllAdmins([ performedBy, user.id ], function (admin, done) {
mailer.userAdded(admin.email, user);
add(admin.id, eventId, 'User added', `User ${user.fallbackEmail} was added`, done);
add(admin.id, eventId, `User '${user.displayName}' added`, `User '${user.username || user.email || user.fallbackEmail}' was added.`, done);
}, callback);
}
@@ -156,7 +158,7 @@ function userRemoved(performedBy, eventId, user, callback) {
actionForAllAdmins([ performedBy, user.id ], function (admin, done) {
mailer.userRemoved(admin.email, user);
add(admin.id, eventId, 'User removed', `User ${user.username || user.email || user.fallbackEmail} was removed`, done);
add(admin.id, eventId, `User '${user.displayName}' removed`, `User '${user.username || user.email || user.fallbackEmail}' was removed.`, done);
}, callback);
}
@@ -167,7 +169,7 @@ function adminChanged(performedBy, eventId, user, callback) {
actionForAllAdmins([ performedBy, user.id ], function (admin, done) {
mailer.adminChanged(admin.email, user, user.admin);
add(admin.id, eventId, 'Admin status change', `User ${user.username || user.email || user.fallbackEmail} ${user.admin ? 'is now an admin' : 'is no more an admin'}`, done);
add(admin.id, eventId, `User '${user.displayName} ' ${user.admin ? 'is now an admin' : 'is no more an admin'}`, `User '${user.username || user.email || user.fallbackEmail}' ${user.admin ? 'is now an admin' : 'is no more an admin'}.`, done);
}, callback);
}
@@ -236,8 +238,13 @@ function appUpdated(eventId, app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
const tmp = app.manifest.description.match(/<upstream>(.*)<\/upstream>/i);
const upstreamVersion = (tmp && tmp[1]) ? tmp[1] : '';
const title = upstreamVersion ? `${app.manifest.title} at ${app.fqdn} updated to ${upstreamVersion} (package version ${app.manifest.version})`
: `${app.manifest.title} at ${app.fqdn} updated to package version ${app.manifest.version}`;
actionForAllAdmins([], function (admin, done) {
add(admin.id, eventId, `App ${app.fqdn} updated`, `The application ${app.manifest.title} installed at https://${app.fqdn} was updated to package version ${app.manifest.version}.`, function (error) {
add(admin.id, eventId, title, `The application ${app.manifest.title} installed at https://${app.fqdn} was updated.\n\nChangelog:\n${app.manifest.changelog}\n`, function (error) {
if (error) return callback(error);
mailer.appUpdated(admin.email, app, function (error) {
@@ -248,6 +255,19 @@ function appUpdated(eventId, app, callback) {
}, callback);
}
function boxUpdated(oldVersion, newVersion, callback) {
assert.strictEqual(typeof oldVersion, 'string');
assert.strictEqual(typeof newVersion, 'string');
assert.strictEqual(typeof callback, 'function');
const changes = changelog.getChanges(newVersion);
const changelogMarkdown = changes.map((m) => `* ${m}\n`).join('');
actionForAllAdmins([], function (admin, done) {
add(admin.id, null, `Cloudron updated to v${newVersion}`, `Cloudron was updated from v${oldVersion} to v${newVersion}.\n\nChangelog:\n${changelogMarkdown}\n`, done);
}, callback);
}
function certificateRenewalError(eventId, vhost, errorMessage, callback) {
assert.strictEqual(typeof eventId, 'string');
assert.strictEqual(typeof vhost, 'string');
@@ -269,11 +289,11 @@ function backupFailed(eventId, taskId, errorMessage, callback) {
assert.strictEqual(typeof errorMessage, 'string');
assert.strictEqual(typeof callback, 'function');
if (custom.spec().alerts.email) mailer.backupFailed(custom.spec().alerts.email, errorMessage, `${config.adminOrigin()}/logs.html?taskId=${taskId}`);
if (custom.spec().alerts.email) mailer.backupFailed(custom.spec().alerts.email, errorMessage, `${settings.adminOrigin()}/logs.html?taskId=${taskId}`);
if (!custom.spec().alerts.notifyCloudronAdmins) return callback();
actionForAllAdmins([], function (admin, callback) {
mailer.backupFailed(admin.email, errorMessage, `${config.adminOrigin()}/logs.html?taskId=${taskId}`);
mailer.backupFailed(admin.email, errorMessage, `${settings.adminOrigin()}/logs.html?taskId=${taskId}`);
add(admin.id, eventId, 'Failed to backup', `Backup failed: ${errorMessage}. Logs are available [here](/logs.html?taskId=${taskId}). Will be retried in 4 hours`, callback);
}, callback);
}
@@ -326,6 +346,9 @@ function onEvent(id, action, source, data, callback) {
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
// external ldap syncer does not generate notifications - FIXME username might be an issue here
if (source.username === auditsource.EXTERNAL_LDAP_TASK.username) return callback();
switch (action) {
case eventlog.ACTION_USER_ADD:
return userAdded(source.userId, id, data.user, callback);
@@ -358,6 +381,9 @@ function onEvent(id, action, source, data, callback) {
if (!data.errorMessage || source.username !== 'cron') return callback();
return backupFailed(id, data.taskId, data.errorMessage, callback); // only notify for automated backups
case eventlog.ACTION_UPDATE_FINISH:
return boxUpdated(data.oldVersion, data.newVersion, callback);
default:
return callback();
}
+1 -1
View File
@@ -61,7 +61,7 @@ app.controller('Controller', ['$scope', function ($scope) {
<div class="control-label" ng-show="setupForm.password.$dirty && setupForm.password.$invalid">
<small ng-show="setupForm.password.$dirty && setupForm.password.$invalid">Password must be atleast 8 characters</small>
</div>
<input type="password" class="form-control" ng-model="password" name="password" ng-pattern="/^.{8,30}$/" required>
<input type="password" class="form-control" ng-model="password" name="password" ng-pattern="/^.{8,}$/" required>
</div>
<div class="form-group" ng-class="{ 'has-error': (setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)) }">
+40 -28
View File
@@ -1,45 +1,57 @@
'use strict';
var config = require('./config.js'),
var constants = require('./constants.js'),
path = require('path');
function baseDir() {
const homeDir = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
if (constants.CLOUDRON) return homeDir;
if (constants.TEST) return path.join(homeDir, '.cloudron_test');
// cannot reach
}
// keep these values in sync with start.sh
exports = module.exports = {
baseDir: baseDir,
CLOUDRON_DEFAULT_AVATAR_FILE: path.join(__dirname + '/../assets/avatar.png'),
INFRA_VERSION_FILE: path.join(config.baseDir(), 'platformdata/INFRA_VERSION'),
INFRA_VERSION_FILE: path.join(baseDir(), 'platformdata/INFRA_VERSION'),
LICENSE_FILE: '/etc/cloudron/LICENSE',
CUSTOM_FILE: '/etc/cloudron/custom.yml',
PROVIDER_FILE: '/etc/cloudron/PROVIDER',
PLATFORM_DATA_DIR: path.join(config.baseDir(), 'platformdata'),
APPS_DATA_DIR: path.join(config.baseDir(), 'appsdata'),
BOX_DATA_DIR: path.join(config.baseDir(), 'boxdata'),
PLATFORM_DATA_DIR: path.join(baseDir(), 'platformdata'),
APPS_DATA_DIR: path.join(baseDir(), 'appsdata'),
BOX_DATA_DIR: path.join(baseDir(), 'boxdata'),
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'),
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'),
UPDATE_DIR: path.join(config.baseDir(), 'platformdata/update'),
SNAPSHOT_INFO_FILE: path.join(config.baseDir(), 'platformdata/backup/snapshot-info.json'),
DYNDNS_INFO_FILE: path.join(config.baseDir(), 'platformdata/dyndns-info.json'),
CUSTOM_FILE: path.join(baseDir(), 'boxdata/custom.yml'),
ACME_CHALLENGES_DIR: path.join(baseDir(), 'platformdata/acme'),
ADDON_CONFIG_DIR: path.join(baseDir(), 'platformdata/addons'),
COLLECTD_APPCONFIG_DIR: path.join(baseDir(), 'platformdata/collectd/collectd.conf.d'),
LOGROTATE_CONFIG_DIR: path.join(baseDir(), 'platformdata/logrotate.d'),
NGINX_CONFIG_DIR: path.join(baseDir(), 'platformdata/nginx'),
NGINX_APPCONFIG_DIR: path.join(baseDir(), 'platformdata/nginx/applications'),
NGINX_CERT_DIR: path.join(baseDir(), 'platformdata/nginx/cert'),
BACKUP_INFO_DIR: path.join(baseDir(), 'platformdata/backup'),
UPDATE_DIR: path.join(baseDir(), 'platformdata/update'),
SNAPSHOT_INFO_FILE: path.join(baseDir(), 'platformdata/backup/snapshot-info.json'),
DYNDNS_INFO_FILE: path.join(baseDir(), 'platformdata/dyndns-info.json'),
VERSION_FILE: path.join(baseDir(), 'platformdata/VERSION'),
// this is not part of appdata because an icon may be set before install
APP_ICONS_DIR: path.join(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'),
UPDATE_CHECKER_FILE: path.join(config.baseDir(), 'boxdata/updatechecker.json'),
APP_ICONS_DIR: path.join(baseDir(), 'boxdata/appicons'),
MAIL_DATA_DIR: path.join(baseDir(), 'boxdata/mail'),
ACME_ACCOUNT_KEY_FILE: path.join(baseDir(), 'boxdata/acme/acme.key'),
APP_CERTS_DIR: path.join(baseDir(), 'boxdata/certs'),
CLOUDRON_AVATAR_FILE: path.join(baseDir(), 'boxdata/avatar.png'),
UPDATE_CHECKER_FILE: path.join(baseDir(), 'boxdata/updatechecker.json'),
LOG_DIR: path.join(config.baseDir(), 'platformdata/logs'),
TASKS_LOG_DIR: path.join(config.baseDir(), 'platformdata/logs/tasks'),
CRASH_LOG_DIR: path.join(config.baseDir(), 'platformdata/logs/crash'),
LOG_DIR: path.join(baseDir(), 'platformdata/logs'),
TASKS_LOG_DIR: path.join(baseDir(), 'platformdata/logs/tasks'),
CRASH_LOG_DIR: path.join(baseDir(), 'platformdata/logs/crash'),
// this pattern is for the cloudron logs API route to work
BACKUP_LOG_FILE: path.join(config.baseDir(), 'platformdata/logs/backup/app.log'),
UPDATER_LOG_FILE: path.join(config.baseDir(), 'platformdata/logs/updater/app.log')
BACKUP_LOG_FILE: path.join(baseDir(), 'platformdata/logs/backup/app.log'),
UPDATER_LOG_FILE: path.join(baseDir(), 'platformdata/logs/updater/app.log')
};
+4 -3
View File
@@ -10,6 +10,7 @@ exports = module.exports = {
var addons = require('./addons.js'),
apps = require('./apps.js'),
appTaskManager = require('./apptaskmanager.js'),
assert = require('assert'),
async = require('async'),
debug = require('debug')('box:platform'),
@@ -23,7 +24,7 @@ var addons = require('./addons.js'),
settings = require('./settings.js'),
sftp = require('./sftp.js'),
shell = require('./shell.js'),
taskmanager = require('./taskmanager.js'),
tasks = require('./tasks.js'),
_ = require('underscore');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
@@ -75,13 +76,13 @@ function start(callback) {
}
function stop(callback) {
taskmanager.pauseTasks(callback);
tasks.stopAllTasks(callback);
}
function onPlatformReady() {
debug('onPlatformReady: platform is ready');
exports._isReady = true;
taskmanager.resumeTasks();
appTaskManager.resumeTasks(NOOP_CALLBACK);
applyPlatformConfig(NOOP_CALLBACK);
pruneInfraImages(NOOP_CALLBACK);
+12 -18
View File
@@ -17,7 +17,6 @@ var appstore = require('./appstore.js'),
async = require('async'),
backups = require('./backups.js'),
BackupsError = require('./backups.js').BackupsError,
config = require('./config.js'),
constants = require('./constants.js'),
clients = require('./clients.js'),
cloudron = require('./cloudron.js'),
@@ -32,6 +31,7 @@ var appstore = require('./appstore.js'),
semver = require('semver'),
settings = require('./settings.js'),
superagent = require('superagent'),
sysinfo = require('./sysinfo.js'),
users = require('./users.js'),
UsersError = users.UsersError,
tld = require('tldjs'),
@@ -113,11 +113,9 @@ function unprovision(callback) {
debug('unprovision');
config.setAdminDomain('');
config.setAdminFqdn('');
// TODO: also cancel any existing configureWebadmin task
async.series([
settings.setAdmin.bind(null, '', ''),
mail.clearDomains,
domains.clear
], callback);
@@ -170,9 +168,8 @@ function setup(dnsConfig, backupConfig, auditSource, callback) {
async.series([
autoRegister.bind(null, domain),
domains.prepareDashboardDomain.bind(null, domain, auditSource, (progress) => setProgress('setup', progress.message, NOOP_CALLBACK)),
cloudron.setDashboardDomain.bind(null, domain, auditSource), // this sets up the config.fqdn()
mail.addDomain.bind(null, domain), // this relies on config.mailFqdn() and config.adminDomain()
setProgress.bind(null, 'setup', 'Applying auto-configuration'),
cloudron.setDashboardDomain.bind(null, domain, auditSource),
mail.addDomain.bind(null, domain), // this relies on settings.mailFqdn() and settings.adminDomain()
(next) => { if (!backupConfig) return next(); settings.setBackupConfig(backupConfig, next); },
setProgress.bind(null, 'setup', 'Done'),
eventlog.add.bind(null, eventlog.ACTION_PROVISION, auditSource, { })
@@ -252,7 +249,7 @@ function restore(backupConfig, backupId, version, auditSource, callback) {
assert.strictEqual(typeof callback, 'function');
if (!semver.valid(version)) return callback(new ProvisionError(ProvisionError.BAD_STATE, 'version is not a valid semver'));
if (semver.major(config.version()) !== semver.major(version) || semver.minor(config.version()) !== semver.minor(version)) return callback(new ProvisionError(ProvisionError.BAD_STATE, `Run cloudron-setup with --version ${version} to restore from this backup`));
if (semver.major(constants.VERSION) !== semver.major(version) || semver.minor(constants.VERSION) !== semver.minor(version)) return callback(new ProvisionError(ProvisionError.BAD_STATE, `Run cloudron-setup with --version ${version} to restore from this backup`));
if (gProvisionStatus.setup.active || gProvisionStatus.restore.active) return callback(new ProvisionError(ProvisionError.BAD_STATE, 'Already setting up or restoring'));
@@ -266,7 +263,7 @@ function restore(backupConfig, backupId, version, auditSource, callback) {
users.isActivated(function (error, activated) {
if (error) return done(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
if (activated) return done(new ProvisionError(ProvisionError.ALREADY_PROVISIONED, 'Already activated'));
if (activated) return done(new ProvisionError(ProvisionError.ALREADY_PROVISIONED, 'Already activated. Restore with a fresh Cloudron installation.'));
backups.testConfig(backupConfig, function (error) {
if (error && error.reason === BackupsError.BAD_FIELD) return done(new ProvisionError(ProvisionError.BAD_FIELD, error.message));
@@ -280,11 +277,8 @@ function restore(backupConfig, backupId, version, auditSource, callback) {
async.series([
setProgress.bind(null, 'restore', 'Downloading backup'),
backups.restore.bind(null, backupConfig, backupId, (progress) => setProgress('restore', progress.message, NOOP_CALLBACK)),
setProgress.bind(null, 'restore', 'Applying auto-configuration'),
// currently, our suggested restore flow is after a dnsSetup. The dnSetup creates DKIM keys and updates the DNS
// for this reason, we have to re-setup DNS after a restore so it has DKIm from the backup
// Once we have a 100% IP based restore, we can skip this
mail.setDnsRecords.bind(null, config.adminDomain()),
cloudron.setupDashboard.bind(null, auditSource, (progress) => setProgress('restore', progress.message, NOOP_CALLBACK)),
settings.setBackupConfig.bind(null, backupConfig), // update with the latest backupConfig
eventlog.add.bind(null, eventlog.ACTION_RESTORE, auditSource, { backupId }),
], function (error) {
gProvisionStatus.restore.active = false;
@@ -306,11 +300,11 @@ function getStatus(callback) {
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
callback(null, _.extend({
version: config.version(),
apiServerOrigin: config.apiServerOrigin(), // used by CaaS tool
provider: config.provider(),
version: constants.VERSION,
apiServerOrigin: settings.apiServerOrigin(), // used by CaaS tool
provider: sysinfo.provider(),
cloudronName: cloudronName,
adminFqdn: config.adminDomain() ? config.adminFqdn() : null,
adminFqdn: settings.adminDomain() ? settings.adminFqdn() : null,
activated: activated,
}, gProvisionStatus));
});
+12 -11
View File
@@ -36,7 +36,6 @@ var acme2 = require('./cert/acme2.js'),
assert = require('assert'),
async = require('async'),
caas = require('./cert/caas.js'),
config = require('./config.js'),
constants = require('./constants.js'),
crypto = require('crypto'),
debug = require('debug')('box:reverseproxy'),
@@ -51,7 +50,9 @@ var acme2 = require('./cert/acme2.js'),
paths = require('./paths.js'),
rimraf = require('rimraf'),
safe = require('safetydance'),
settings = require('./settings.js'),
shell = require('./shell.js'),
sysinfo = require('./sysinfo.js'),
users = require('./users.js'),
util = require('util');
@@ -90,8 +91,8 @@ function getCertApi(domainObject, callback) {
var api = domainObject.tlsConfig.provider === 'caas' ? caas : acme2;
var options = { prod: false, performHttpAuthorization: false, wildcard: false, email: '' };
if (domainObject.tlsConfig.provider !== 'caas') { // matches 'le-prod' or 'letsencrypt-prod'
options.prod = domainObject.tlsConfig.provider.match(/.*-prod/) !== null;
if (domainObject.tlsConfig.provider !== 'caas') {
options.prod = domainObject.tlsConfig.provider.match(/.*-prod/) !== null; // matches 'le-prod' or 'letsencrypt-prod'
options.performHttpAuthorization = domainObject.provider.match(/noop|manual|wildcard/) !== null;
options.wildcard = !!domainObject.tlsConfig.wildcard;
}
@@ -331,7 +332,7 @@ function notifyCertChanged(vhost, callback) {
assert.strictEqual(typeof vhost, 'string');
assert.strictEqual(typeof callback, 'function');
if (vhost !== config.mailFqdn()) return callback();
if (vhost !== settings.mailFqdn()) return callback();
mail.handleCertChanged(callback);
}
@@ -386,9 +387,9 @@ function writeAdminNginxConfig(bundle, configFileName, vhost, callback) {
var data = {
sourceDir: path.resolve(__dirname, '..'),
adminOrigin: config.adminOrigin(),
adminOrigin: settings.adminOrigin(),
vhost: vhost, // if vhost is empty it will become the default_server
hasIPv6: config.hasIPv6(),
hasIPv6: sysinfo.hasIPv6(),
endpoint: 'admin',
certFilePath: bundle.certFilePath,
keyFilePath: bundle.keyFilePath,
@@ -449,9 +450,9 @@ function writeAppNginxConfig(app, bundle, callback) {
var data = {
sourceDir: sourceDir,
adminOrigin: config.adminOrigin(),
adminOrigin: settings.adminOrigin(),
vhost: app.fqdn,
hasIPv6: config.hasIPv6(),
hasIPv6: sysinfo.hasIPv6(),
port: app.httpPort,
endpoint: endpoint,
certFilePath: bundle.certFilePath,
@@ -481,7 +482,7 @@ function writeAppRedirectNginxConfig(app, fqdn, bundle, callback) {
sourceDir: path.resolve(__dirname, '..'),
vhost: fqdn,
redirectTo: app.fqdn,
hasIPv6: config.hasIPv6(),
hasIPv6: sysinfo.hasIPv6(),
endpoint: 'redirect',
certFilePath: bundle.certFilePath,
keyFilePath: bundle.keyFilePath,
@@ -547,7 +548,7 @@ function renewCerts(options, auditSource, progressCallback, callback) {
var appDomains = [];
// add webadmin domain
appDomains.push({ domain: config.adminDomain(), fqdn: config.adminFqdn(), type: 'webadmin', nginxConfigFilename: path.join(paths.NGINX_APPCONFIG_DIR, `${config.adminFqdn()}.conf`) });
appDomains.push({ domain: settings.adminDomain(), fqdn: settings.adminFqdn(), type: 'webadmin', nginxConfigFilename: path.join(paths.NGINX_APPCONFIG_DIR, `${settings.adminFqdn()}.conf`) });
// add app main
allApps.forEach(function (app) {
@@ -577,7 +578,7 @@ function renewCerts(options, auditSource, progressCallback, callback) {
// reconfigure since the cert changed
var configureFunc;
if (appDomain.type === 'webadmin') configureFunc = writeAdminNginxConfig.bind(null, bundle, `${config.adminFqdn()}.conf`, config.adminFqdn());
if (appDomain.type === 'webadmin') configureFunc = writeAdminNginxConfig.bind(null, bundle, `${settings.adminFqdn()}.conf`, settings.adminFqdn());
else if (appDomain.type === 'main') configureFunc = writeAppNginxConfig.bind(null, appDomain.app, bundle);
else if (appDomain.type === 'alternate') configureFunc = writeAppRedirectNginxConfig.bind(null, appDomain.app, appDomain.fqdn, bundle);
else return iteratorCallback(new Error(`Unknown domain type for ${appDomain.fqdn}. This should never happen`));
+54 -92
View File
@@ -32,17 +32,34 @@ var apps = require('../apps.js'),
debug = require('debug')('box:routes/apps'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
paths = require('../paths.js'),
safe = require('safetydance'),
util = require('util'),
WebSocket = require('ws');
function toHttpError(appError) {
switch (appError.reason) {
case AppsError.NOT_FOUND:
return new HttpError(404, appError);
case AppsError.ALREADY_EXISTS:
case AppsError.BAD_STATE:
return new HttpError(409, appError);
case AppsError.BAD_FIELD:
return new HttpError(400, appError);
case AppsError.PLAN_LIMIT:
return new HttpError(402, appError);
case AppsError.EXTERNAL_ERROR:
return new HttpError(424, appError);
case AppsError.INTERNAL_ERROR:
default:
return new HttpError(500, appError);
}
}
function getApp(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
apps.get(req.params.id, function (error, app) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error) return next(new HttpError(500, error));
if (error) return next(toHttpError(error));
next(new HttpSuccess(200, apps.removeInternalFields(app)));
});
@@ -52,7 +69,7 @@ function getApps(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
apps.getAllByUser(req.user, function (error, allApps) {
if (error) return next(new HttpError(500, error));
if (error) return next(toHttpError(error));
allApps = allApps.map(apps.removeRestrictedFields);
@@ -63,15 +80,11 @@ function getApps(req, res, next) {
function getAppIcon(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
if (!req.query.original) {
const userIconPath = `${paths.APP_ICONS_DIR}/${req.params.id}.user.png`;
if (safe.fs.existsSync(userIconPath)) return res.sendFile(userIconPath);
}
apps.getIconPath(req.params.id, { original: req.query.original }, function (error, iconPath) {
if (error) return next(toHttpError(error));
const appstoreIconPath = `${paths.APP_ICONS_DIR}/${req.params.id}.png`;
if (safe.fs.existsSync(appstoreIconPath)) return res.sendFile(appstoreIconPath);
return next(new HttpError(404, 'No such icon'));
res.sendFile(iconPath);
});
}
function installApp(req, res, next) {
@@ -126,18 +139,10 @@ function installApp(req, res, next) {
debug('Installing app :%j', data);
apps.install(data, req.user, auditSource.fromRequest(req), function (error, app) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.PLAN_LIMIT) return next(new HttpError(402, error.message));
if (error && error.reason === AppsError.BAD_CERTIFICATE) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
apps.install(data, req.user, auditSource.fromRequest(req), function (error, result) {
if (error) return next(toHttpError(error));
next(new HttpSuccess(202, app));
next(new HttpSuccess(202, { id: result.id, taskId: result.taskId }));
});
}
@@ -189,17 +194,10 @@ function configureApp(req, res, next) {
debug('Configuring app id:%s data:%j', req.params.id, data);
apps.configure(req.params.id, data, req.user, auditSource.fromRequest(req), function (error) {
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
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 && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.BAD_CERTIFICATE) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
apps.configure(req.params.id, data, req.user, auditSource.fromRequest(req), function (error, result) {
if (error) return next(toHttpError(error));
next(new HttpSuccess(202, { }));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
@@ -214,14 +212,10 @@ function restoreApp(req, res, next) {
if (!('backupId' in req.body)) return next(new HttpError(400, 'backupId is required'));
if (data.backupId !== null && typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be string or null'));
apps.restore(req.params.id, data, auditSource.fromRequest(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.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
apps.restore(req.params.id, data, auditSource.fromRequest(req), function (error, result) {
if (error) return next(toHttpError(error));
next(new HttpSuccess(202, { }));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
@@ -239,18 +233,9 @@ function cloneApp(req, res, next) {
if (('portBindings' in data) && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
apps.clone(req.params.id, data, req.user, auditSource.fromRequest(req), function (error, result) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
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.PLAN_LIMIT) return next(new HttpError(402, error.message));
if (error && error.reason === AppsError.BAD_CERTIFICATE) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(toHttpError(error));
next(new HttpSuccess(201, { id: result.id }));
next(new HttpSuccess(201, { id: result.id, taskId: result.taskId }));
});
}
@@ -259,13 +244,10 @@ function backupApp(req, res, next) {
debug('Backup app id:%s', req.params.id);
apps.backup(req.params.id, function (error) {
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 && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(424, error));
if (error) return next(new HttpError(500, error));
apps.backup(req.params.id, function (error, result) {
if (error) return next(toHttpError(error));
next(new HttpSuccess(202, { }));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
@@ -274,12 +256,10 @@ function uninstallApp(req, res, next) {
debug('Uninstalling app id:%s', req.params.id);
apps.uninstall(req.params.id, auditSource.fromRequest(req), function (error) {
if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(424, error));
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error) return next(new HttpError(500, error));
apps.uninstall(req.params.id, auditSource.fromRequest(req), function (error, result) {
if (error) return next(toHttpError(error));
next(new HttpSuccess(202, { }));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
@@ -289,9 +269,7 @@ function startApp(req, res, next) {
debug('Start app id:%s', req.params.id);
apps.start(req.params.id, function (error) {
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));
if (error) return next(toHttpError(error));
next(new HttpSuccess(202, { }));
});
@@ -303,9 +281,7 @@ function stopApp(req, res, next) {
debug('Stop app id:%s', req.params.id);
apps.stop(req.params.id, function (error) {
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));
if (error) return next(toHttpError(error));
next(new HttpSuccess(202, { }));
});
@@ -326,13 +302,10 @@ function updateApp(req, res, next) {
debug('Update app id:%s to manifest:%j', req.params.id, data.manifest);
apps.update(req.params.id, req.body, auditSource.fromRequest(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) return next(new HttpError(500, error));
apps.update(req.params.id, req.body, auditSource.fromRequest(req), function (error, result) {
if (error) return next(toHttpError(error));
next(new HttpSuccess(202, { }));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
@@ -356,9 +329,7 @@ function getLogStream(req, res, next) {
};
apps.getLogs(req.params.id, options, function (error, logStream) {
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(412, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(toHttpError(error));
res.writeHead(200, {
'Content-Type': 'text/event-stream',
@@ -393,9 +364,7 @@ function getLogs(req, res, next) {
};
apps.getLogs(req.params.id, options, function (error, logStream) {
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(412, error.message));
if (error) return next(new HttpError(500, error));
if (error) return next(toHttpError(error));
res.writeHead(200, {
'Content-Type': 'application/x-logs',
@@ -449,9 +418,7 @@ function exec(req, res, next) {
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));
if (error) return next(toHttpError(error));
if (req.headers['upgrade'] !== 'tcp') return next(new HttpError(404, 'exec requires TCP upgrade'));
@@ -491,9 +458,7 @@ function execWebSocket(req, res, next) {
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));
if (error) return next(toHttpError(error));
debug('Connected to terminal');
@@ -533,8 +498,7 @@ function listBackups(req, res, next) {
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
apps.listBackups(page, perPage, req.params.id, function (error, result) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error) return next(new HttpError(500, error));
if (error) return next(toHttpError(error));
next(new HttpSuccess(200, { backups: result }));
});
@@ -549,8 +513,7 @@ function uploadFile(req, res, next) {
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));
if (error) return next(toHttpError(error));
debug('uploadFile: done');
@@ -566,8 +529,7 @@ function downloadFile(req, res, next) {
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));
if (error) return next(toHttpError(error));
var headers = {
'Content-Type': 'application/octet-stream',
+13 -2
View File
@@ -12,7 +12,8 @@ exports = module.exports = {
getLogStream: getLogStream,
setDashboardAndMailDomain: setDashboardAndMailDomain,
prepareDashboardDomain: prepareDashboardDomain,
renewCerts: renewCerts
renewCerts: renewCerts,
syncExternalLdap: syncExternalLdap
};
let assert = require('assert'),
@@ -21,6 +22,8 @@ let assert = require('assert'),
cloudron = require('../cloudron.js'),
CloudronError = cloudron.CloudronError,
custom = require('../custom.js'),
disks = require('../disks.js'),
externalldap = require('../externalldap.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
updater = require('../updater.js'),
@@ -51,7 +54,7 @@ function getConfig(req, res, next) {
}
function getDisks(req, res, next) {
cloudron.getDisks(function (error, result) {
disks.getDisks(function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, result));
});
@@ -185,3 +188,11 @@ function renewCerts(req, res, next) {
next(new HttpSuccess(202, { taskId }));
});
}
function syncExternalLdap(req, res, next) {
externalldap.startSyncer(function (error, taskId) {
if (error) return next(new HttpError(500, error.message));
next(new HttpSuccess(202, { taskId: taskId }));
});
}
+1 -1
View File
@@ -20,7 +20,7 @@ function login(req, res, next) {
if (!user.ghost && user.twoFactorAuthenticationEnabled) {
if (!req.body.totpToken) return next(new HttpError(401, 'A totpToken must be provided'));
let verified = speakeasy.totp.verify({ secret: user.twoFactorAuthenticationSecret, encoding: 'base32', token: req.body.totpToken });
let verified = speakeasy.totp.verify({ secret: user.twoFactorAuthenticationSecret, encoding: 'base32', token: req.body.totpToken, window: 2 });
if (!verified) return next(new HttpError(401, 'Invalid totpToken'));
}
+6 -2
View File
@@ -7,14 +7,18 @@ exports = module.exports = {
var middleware = require('../middleware/index.js'),
url = require('url');
var graphiteProxy = middleware.proxy(url.parse('http://127.0.0.1:8417'));
// for testing locally: curl 'http://127.0.0.1:8417/graphite-web/render?format=json&from=-1min&target=absolute(collectd.localhost.du-docker.capacity-usage)'
// the datapoint is (value, timestamp) https://buildmedia.readthedocs.org/media/pdf/graphite/0.9.16/graphite.pdf
const graphiteProxy = middleware.proxy(url.parse('http://127.0.0.1:8417'));
function getGraphs(req, res, next) {
var parsedUrl = url.parse(req.url, true /* parseQueryString */);
delete parsedUrl.query['access_token'];
delete req.headers['authorization'];
delete req.headers['cookies'];
req.url = url.format({ pathname: 'render', query: parsedUrl.query });
// 'graphite-web' is the URL_PREFIX in docker-graphite
req.url = url.format({ pathname: 'graphite-web/render', query: parsedUrl.query });
// graphs may take very long to respond so we run into headers already sent issues quite often
// nginx still has a request timeout which can deal with this then.
+6 -7
View File
@@ -24,7 +24,6 @@ var apps = require('../apps.js'),
authcodedb = require('../authcodedb.js'),
clients = require('../clients'),
ClientsError = clients.ClientsError,
config = require('../config.js'),
constants = require('../constants.js'),
DatabaseError = require('../databaseerror.js'),
debug = require('debug')('box:routes/oauth2'),
@@ -154,7 +153,7 @@ function renderTemplate(res, template, data) {
// amend template properties, for example used in the header
data.title = data.title || 'Cloudron';
data.adminOrigin = config.adminOrigin();
data.adminOrigin = settings.adminOrigin();
data.cloudronName = cloudronName;
res.render(template, data);
@@ -214,8 +213,8 @@ function loginForm(req, res) {
applicationName: applicationName,
applicationLogo: applicationLogo,
error: error,
username: config.isDemo() ? constants.DEMO_USERNAME : '',
password: config.isDemo() ? 'cloudron' : '',
username: settings.isDemo() ? constants.DEMO_USERNAME : '',
password: settings.isDemo() ? 'cloudron' : '',
title: applicationName + ' Login'
});
}
@@ -263,7 +262,7 @@ function login(req, res) {
return res.redirect('/api/v1/session/login?' + failureQuery);
}
let verified = speakeasy.totp.verify({ secret: req.user.twoFactorAuthenticationSecret, encoding: 'base32', token: req.body.totpToken });
let verified = speakeasy.totp.verify({ secret: req.user.twoFactorAuthenticationSecret, encoding: 'base32', token: req.body.totpToken, window: 2 });
if (!verified) {
let failureQuery = querystring.stringify({ error: 'The 2FA token is invalid', returnTo: returnTo });
return res.redirect('/api/v1/session/login?' + failureQuery);
@@ -373,7 +372,7 @@ function accountSetup(req, res, next) {
clients.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
if (error) return next(new HttpError(500, error));
res.redirect(`${config.adminOrigin()}?accessToken=${result.accessToken}&expiresAt=${result.expires}`);
res.redirect(`${settings.adminOrigin()}?accessToken=${result.accessToken}&expiresAt=${result.expires}`);
});
});
});
@@ -421,7 +420,7 @@ function passwordReset(req, res, next) {
clients.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
if (error) return next(new HttpError(500, error));
res.redirect(`${config.adminOrigin()}?accessToken=${result.accessToken}&expiresAt=${result.expires}`);
res.redirect(`${settings.adminOrigin()}?accessToken=${result.accessToken}&expiresAt=${result.expires}`);
});
});
});
+2 -1
View File
@@ -27,7 +27,8 @@ function get(req, res, next) {
fallbackEmail: req.user.fallbackEmail,
displayName: req.user.displayName,
twoFactorAuthenticationEnabled: req.user.twoFactorAuthenticationEnabled,
admin: req.user.admin
admin: req.user.admin,
source: req.user.source
}));
}
+2 -2
View File
@@ -10,18 +10,18 @@ exports = module.exports = {
var assert = require('assert'),
auditSource = require('../auditsource'),
config = require('../config.js'),
debug = require('debug')('box:routes/setup'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
provision = require('../provision.js'),
ProvisionError = require('../provision.js').ProvisionError,
sysinfo = require('../sysinfo.js'),
superagent = require('superagent');
function providerTokenAuth(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (config.provider() === 'ami') {
if (sysinfo.provider() === 'ami') {
if (typeof req.body.providerToken !== 'string' || !req.body.providerToken) return next(new HttpError(400, 'providerToken must be a non empty string'));
superagent.get('http://169.254.169.254/latest/meta-data/instance-id').timeout(30 * 1000).end(function (error, result) {
+32 -3
View File
@@ -128,15 +128,15 @@ function getCloudronAvatar(req, res, next) {
}
function getBackupConfig(req, res, next) {
settings.getBackupConfig(function (error, config) {
settings.getBackupConfig(function (error, backupConfig) {
if (error) return next(new HttpError(500, error));
// always send provider as it is used by the UI to figure if backups are disabled ('noop' backend)
if (!custom.spec().backups.configurable) {
return next(new HttpSuccess(200, { provider: config.provider }));
return next(new HttpSuccess(200, { provider: backupConfig.provider }));
}
next(new HttpSuccess(200, backups.removePrivateFields(config)));
next(new HttpSuccess(200, backups.removePrivateFields(backupConfig)));
});
}
@@ -196,6 +196,33 @@ function setPlatformConfig(req, res, next) {
});
}
function getExternalLdapConfig(req, res, next) {
settings.getExternalLdapConfig(function (error, config) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, config));
});
}
function setExternalLdapConfig(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled must be a boolean'));
if (typeof req.body.url !== 'string' || req.body.url === '') return next(new HttpError(400, 'url must be a non empty string'));
if (typeof req.body.baseDn !== 'string' || req.body.baseDn === '') return next(new HttpError(400, 'baseDn must be a non empty string'));
if (typeof req.body.filter !== 'string' || req.body.filter === '') return next(new HttpError(400, 'filter must be a non empty string'));
if ('bindDn' in req.body && (typeof req.body.bindDn !== 'string' || req.body.bindDn === '')) return next(new HttpError(400, 'bindDn must be a non empty string'));
if ('bindPassword' in req.body && typeof req.body.bindPassword !== 'string') return next(new HttpError(400, 'bindPassword must be a string'));
settings.setExternalLdapConfig(req.body, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === SettingsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, {}));
});
}
function getDynamicDnsConfig(req, res, next) {
settings.getDynamicDnsConfig(function (error, enabled) {
if (error) return next(new HttpError(500, error));
@@ -262,6 +289,7 @@ function get(req, res, next) {
case settings.DYNAMIC_DNS_KEY: return getDynamicDnsConfig(req, res, next);
case settings.BACKUP_CONFIG_KEY: return getBackupConfig(req, res, next);
case settings.PLATFORM_CONFIG_KEY: return getPlatformConfig(req, res, next);
case settings.EXTERNAL_LDAP_KEY: return getExternalLdapConfig(req, res, next);
case settings.UNSTABLE_APPS_KEY: return getUnstableAppsConfig(req, res, next);
case settings.APP_AUTOUPDATE_PATTERN_KEY: return getAppAutoupdatePattern(req, res, next);
@@ -282,6 +310,7 @@ function set(req, res, next) {
case settings.DYNAMIC_DNS_KEY: return setDynamicDnsConfig(req, res, next);
case settings.BACKUP_CONFIG_KEY: return setBackupConfig(req, res, next);
case settings.PLATFORM_CONFIG_KEY: return setPlatformConfig(req, res, next);
case settings.EXTERNAL_LDAP_KEY: return setExternalLdapConfig(req, res, next);
case settings.UNSTABLE_APPS_KEY: return setUnstableAppsConfig(req, res, next);
case settings.APP_AUTOUPDATE_PATTERN_KEY: return setAppAutoupdatePattern(req, res, next);
+1 -1
View File
@@ -94,4 +94,4 @@ describe('scopes middleware', function () {
done();
});
});
});
});
+51 -91
View File
@@ -6,13 +6,11 @@
/* global after:false */
/* global xit:false */
var appdb = require('../../appdb.js'),
apps = require('../../apps.js'),
let apps = require('../../apps.js'),
assert = require('assert'),
async = require('async'),
child_process = require('child_process'),
clients = require('../../clients.js'),
config = require('../../config.js'),
constants = require('../../constants.js'),
database = require('../../database.js'),
docker = require('../../docker.js').connection,
@@ -21,7 +19,6 @@ var appdb = require('../../appdb.js'),
hat = require('../../hat.js'),
hock = require('hock'),
http = require('http'),
https = require('https'),
ldap = require('../../ldap.js'),
net = require('net'),
nock = require('nock'),
@@ -33,17 +30,15 @@ var appdb = require('../../appdb.js'),
settings = require('../../settings.js'),
settingsdb = require('../../settingsdb.js'),
superagent = require('superagent'),
taskmanager = require('../../taskmanager.js'),
tokendb = require('../../tokendb.js'),
url = require('url'),
uuid = require('uuid'),
_ = require('underscore');
uuid = require('uuid');
var SERVER_URL = 'http://localhost:' + config.get('port');
var SERVER_URL = 'http://localhost:' + constants.PORT;
// Test image information
var TEST_IMAGE_REPO = 'cloudron/test';
var TEST_IMAGE_TAG = '25.15.2';
var TEST_IMAGE_TAG = '25.19.0';
var TEST_IMAGE = TEST_IMAGE_REPO + ':' + TEST_IMAGE_TAG;
const DOMAIN_0 = {
@@ -77,32 +72,6 @@ var user_1_id = null;
var token = null;
var token_1 = null;
function startDockerProxy(interceptor, callback) {
assert.strictEqual(typeof interceptor, 'function');
return http.createServer(function (req, res) {
if (interceptor(req, res)) return;
// rejectUnauthorized should not be required but it doesn't work without it
var options = _.extend({ }, docker.options, { method: req.method, path: req.url, headers: req.headers, rejectUnauthorized: false });
delete options.protocol; // https module doesn't like this key
var proto = docker.options.protocol === 'https' ? https : http;
var dockerRequest = proto.request(options, function (dockerResponse) {
res.writeHead(dockerResponse.statusCode, dockerResponse.headers);
dockerResponse.on('error', console.error);
dockerResponse.pipe(res, { end: true });
});
req.on('error', console.error);
if (!req.readable) {
dockerRequest.end();
} else {
req.pipe(dockerRequest, { end: true });
}
}).listen(5687, callback);
}
function checkAddons(appEntry, done) {
async.retry({ times: 15, interval: 3000 }, function (callback) {
// this was previously written with superagent but it was getting sporadic EPIPE
@@ -153,7 +122,6 @@ function checkRedis(containerId, done) {
});
}
var dockerProxy;
var imageDeleted;
var imageCreated;
@@ -173,8 +141,6 @@ function waitForSetup(done) {
function startBox(done) {
console.log('Starting box code...');
config._reset();
imageDeleted = false;
imageCreated = false;
@@ -188,6 +154,7 @@ function startBox(done) {
database._clear,
server.start,
ldap.start,
settings._setApiServerOrigin.bind(null, 'http://localhost:6060'),
function (callback) {
superagent.post(SERVER_URL + '/api/v1/cloudron/setup')
@@ -229,23 +196,6 @@ function startBox(done) {
});
},
function (callback) {
dockerProxy = startDockerProxy(function interceptor(req, res) {
if (req.method === 'POST' && req.url === '/images/create?fromImage=' + encodeURIComponent(TEST_IMAGE_REPO) + '&tag=' + TEST_IMAGE_TAG) {
imageCreated = true;
res.writeHead(200);
res.end();
return true;
} else if (req.method === 'DELETE' && req.url === '/images/' + TEST_IMAGE + '?force=false&noprune=false') {
imageDeleted = true;
res.writeHead(200);
res.end();
return true;
}
return false;
}, callback);
},
function (callback) {
process.stdout.write('Waiting for platform to be ready...');
async.retry({ times: 500, interval: 1000 }, function (retryCallback) {
@@ -255,6 +205,7 @@ function startBox(done) {
}, function (error) {
if (error) return callback(error);
console.log();
console.log('Platform is ready');
callback();
});
}
@@ -268,19 +219,16 @@ function stopBox(done) {
child_process.execSync('docker ps -qa --filter \'network=cloudron\' | xargs --no-run-if-empty docker rm -f');
// db is not cleaned up here since it's too late to call it after server.stop. if called before server.stop taskmanager apptasks are unhappy :/
async.series([
dockerProxy.close.bind(dockerProxy),
taskmanager._stopPendingTasks,
taskmanager._waitForPendingTasks,
appdb._clear,
database._clear,
server.stop,
ldap.stop,
config._reset,
ldap.stop
], done);
}
describe('App API', function () {
let taskId = '';
before(startBox);
after(stopBox);
@@ -436,7 +384,7 @@ describe('App API', function () {
});
it('app install fails because manifest download fails', function (done) {
var fake = nock(config.apiServerOrigin()).get('/api/v1/apps/test').reply(404, {});
var fake = nock(settings.apiServerOrigin()).get('/api/v1/apps/test').reply(404, {});
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
@@ -449,7 +397,7 @@ describe('App API', function () {
});
it('app install fails due to purchase failure', function (done) {
var fake1 = nock(config.apiServerOrigin()).get('/api/v1/apps/test').reply(200, { manifest: APP_MANIFEST });
var fake1 = nock(settings.apiServerOrigin()).get('/api/v1/apps/test').reply(200, { manifest: APP_MANIFEST });
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
@@ -462,8 +410,8 @@ describe('App API', function () {
});
it('app install succeeds with purchase', function (done) {
var fake2 = nock(config.apiServerOrigin()).get('/api/v1/apps/' + APP_STORE_ID).reply(200, { manifest: APP_MANIFEST });
var fake3 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/cloudronapps') >= 0; }, (body) => body.appstoreId === APP_STORE_ID && body.manifestId === APP_MANIFEST.id && body.appId).reply(201, { });
var fake2 = nock(settings.apiServerOrigin()).get('/api/v1/apps/' + APP_STORE_ID).reply(200, { manifest: APP_MANIFEST });
var fake3 = nock(settings.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/cloudronapps') >= 0; }, (body) => body.appstoreId === APP_STORE_ID && body.manifestId === APP_MANIFEST.id && body.appId).reply(201, { });
settingsdb.set(settings.CLOUDRON_TOKEN_KEY, USER_1_APPSTORE_TOKEN, function (error) {
if (error) return done(error);
@@ -477,6 +425,7 @@ describe('App API', function () {
APP_ID = res.body.id;
expect(fake2.isDone()).to.be.ok();
expect(fake3.isDone()).to.be.ok();
taskId = res.body.taskId;
done();
});
});
@@ -553,9 +502,18 @@ describe('App API', function () {
});
});
it('can stop the task', function (done) {
superagent.post(SERVER_URL + '/api/v1/tasks/' + taskId + '/stop')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
setTimeout(done, 4000); // wait for it to really die
});
});
it('can uninstall app', function (done) {
var fake1 = nock(config.apiServerOrigin()).get(function (uri) { return uri.indexOf('/api/v1/cloudronapps/') >= 0; }).reply(200, { });
var fake2 = nock(config.apiServerOrigin()).delete(function (uri) { return uri.indexOf('/api/v1/cloudronapps/') >= 0; }).reply(204, { });
var fake1 = nock(settings.apiServerOrigin()).get(function (uri) { return uri.indexOf('/api/v1/cloudronapps/') >= 0; }).reply(200, { });
var fake2 = nock(settings.apiServerOrigin()).delete(function (uri) { return uri.indexOf('/api/v1/cloudronapps/') >= 0; }).reply(204, { });
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
.query({ access_token: token })
@@ -568,8 +526,8 @@ describe('App API', function () {
});
it('app install succeeds again', function (done) {
var fake1 = nock(config.apiServerOrigin()).get('/api/v1/apps/' + APP_STORE_ID).reply(200, { manifest: APP_MANIFEST });
var fake2 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/cloudronapps') >= 0; }, (body) => body.appstoreId === APP_STORE_ID && body.manifestId === APP_MANIFEST.id && body.appId).reply(201, { });
var fake1 = nock(settings.apiServerOrigin()).get('/api/v1/apps/' + APP_STORE_ID).reply(200, { manifest: APP_MANIFEST });
var fake2 = nock(settings.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/cloudronapps') >= 0; }, (body) => body.appstoreId === APP_STORE_ID && body.manifestId === APP_MANIFEST.id && body.appId).reply(201, { });
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
@@ -626,7 +584,7 @@ describe('App installation', function () {
.get('/api/v1/apps/' + APP_STORE_ID + '/versions/' + APP_MANIFEST.version + '/icon')
.replyWithFile(200, path.resolve(__dirname, '../../../assets/avatar.png'));
var port = parseInt(url.parse(config.apiServerOrigin()).port, 10);
var port = parseInt(url.parse(settings.apiServerOrigin()).port, 10);
http.createServer(apiHockInstance.handler).listen(port, callback);
},
@@ -640,13 +598,11 @@ describe('App installation', function () {
], done);
});
after(stopBox);
var appResult = null, appEntry = null;
it('can install test app', function (done) {
var fake1 = nock(config.apiServerOrigin()).get('/api/v1/apps/' + APP_STORE_ID).reply(200, { manifest: APP_MANIFEST });
var fake2 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/cloudronapps') >= 0; }, (body) => body.appstoreId === APP_STORE_ID && body.manifestId === APP_MANIFEST.id && body.appId).reply(201, { });
var fake1 = nock(settings.apiServerOrigin()).get('/api/v1/apps/' + APP_STORE_ID).reply(200, { manifest: APP_MANIFEST });
var fake2 = nock(settings.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/cloudronapps') >= 0; }, (body) => body.appstoreId === APP_STORE_ID && body.manifestId === APP_MANIFEST.id && body.appId).reply(201, { });
var count = 0;
function checkInstallStatus() {
@@ -655,8 +611,8 @@ describe('App installation', function () {
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
if (res.body.installationState === appdb.ISTATE_INSTALLED) { appResult = res.body; return done(null); }
if (res.body.installationState === appdb.ISTATE_ERROR) return done(new Error('Install error'));
if (res.body.installationState === apps.ISTATE_INSTALLED) { appResult = res.body; return done(null); }
if (res.body.installationState === apps.ISTATE_ERROR) return done(new Error('Install error'));
if (++count > 500) return done(new Error('Timedout'));
setTimeout(checkInstallStatus, 1000);
@@ -671,6 +627,7 @@ describe('App installation', function () {
expect(fake1.isDone()).to.be.ok();
expect(fake2.isDone()).to.be.ok();
APP_ID = res.body.id;
checkInstallStatus();
});
});
@@ -695,11 +652,11 @@ describe('App installation', function () {
docker.getContainer(appEntry.containerId).inspect(function (error, data) {
expect(error).to.not.be.ok();
expect(data.Config.ExposedPorts['7777/tcp']).to.eql({ });
expect(data.Config.Env).to.contain('WEBADMIN_ORIGIN=' + config.adminOrigin());
expect(data.Config.Env).to.contain('API_ORIGIN=' + config.adminOrigin());
expect(data.Config.Env).to.contain('CLOUDRON_WEBADMIN_ORIGIN=' + settings.adminOrigin());
expect(data.Config.Env).to.contain('CLOUDRON_API_ORIGIN=' + settings.adminOrigin());
expect(data.Config.Env).to.contain('CLOUDRON=1');
expect(data.Config.Env).to.contain('APP_ORIGIN=https://' + APP_LOCATION + '.' + DOMAIN_0.domain);
expect(data.Config.Env).to.contain('APP_DOMAIN=' + APP_LOCATION + '.' + DOMAIN_0.domain);
expect(data.Config.Env).to.contain('CLOUDRON_APP_ORIGIN=https://' + APP_LOCATION + '.' + DOMAIN_0.domain);
expect(data.Config.Env).to.contain('CLOUDRON_APP_DOMAIN=' + APP_LOCATION + '.' + DOMAIN_0.domain);
// Hostname must not be set of app fqdn or app location!
expect(data.Config.Hostname).to.not.contain(APP_LOCATION);
expect(data.Config.Env).to.contain('ECHO_SERVER_PORT=7171');
@@ -788,16 +745,16 @@ describe('App installation', function () {
expect(error).to.not.be.ok();
expect(client.id.length).to.be(40); // cid- + 32 hex chars (128 bits) + 4 hyphens
expect(client.clientSecret.length).to.be(256); // 32 hex chars (8 * 256 bits)
expect(data.Config.Env).to.contain('OAUTH_CLIENT_ID=' + client.id);
expect(data.Config.Env).to.contain('OAUTH_CLIENT_SECRET=' + client.clientSecret);
expect(data.Config.Env).to.contain('CLOUDRON_OAUTH_CLIENT_ID=' + client.id);
expect(data.Config.Env).to.contain('CLOUDRON_OAUTH_CLIENT_SECRET=' + client.clientSecret);
done();
});
});
});
it('installation - app can populate addons', function (done) {
superagent.get('http://localhost:' + appEntry.httpPort + '/populate_addons').end(function (err, res) {
expect(!err).to.be.ok();
superagent.get(`http://localhost:${appEntry.httpPort}/populate_addons`).end(function (error, res) {
expect(!error).to.be.ok();
expect(res.statusCode).to.equal(200);
for (var key in res.body) {
expect(res.body[key]).to.be('OK');
@@ -841,7 +798,7 @@ describe('App installation', function () {
it('logStream - stream logs', function (done) {
var options = {
port: config.get('port'), host: 'localhost', path: '/api/v1/apps/' + APP_ID + '/logstream?access_token=' + token,
port: constants.PORT, host: 'localhost', path: '/api/v1/apps/' + APP_ID + '/logstream?access_token=' + token,
headers: { 'Accept': 'text/event-stream', 'Connection': 'keep-alive' }
};
@@ -944,8 +901,8 @@ describe('App installation', function () {
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
if (res.body.installationState === appdb.ISTATE_INSTALLED) { appResult = res.body; expect(appResult).to.be.ok(); return done(null); }
if (res.body.installationState === appdb.ISTATE_ERROR) return done(new Error('Install error'));
if (res.body.installationState === apps.ISTATE_INSTALLED) { appResult = res.body; expect(appResult).to.be.ok(); return done(null); }
if (res.body.installationState === apps.ISTATE_ERROR) return done(new Error('Install error'));
if (++count > 50) return done(new Error('Timedout'));
setTimeout(checkConfigureStatus.bind(null, count, done), 1000);
});
@@ -1107,15 +1064,15 @@ describe('App installation', function () {
});
it('can uninstall app', function (done) {
var fake1 = nock(config.apiServerOrigin()).get(function (uri) { return uri.indexOf('/api/v1/cloudronapps/') >= 0; }).reply(200, { });
var fake2 = nock(config.apiServerOrigin()).delete(function (uri) { return uri.indexOf('/api/v1/cloudronapps/') >= 0; }).reply(204, { });
var fake1 = nock(settings.apiServerOrigin()).get(function (uri) { return uri.indexOf('/api/v1/cloudronapps/') >= 0; }).reply(200, { });
var fake2 = nock(settings.apiServerOrigin()).delete(function (uri) { return uri.indexOf('/api/v1/cloudronapps/') >= 0; }).reply(204, { });
var count = 0;
function checkUninstallStatus() {
superagent.get(SERVER_URL + '/api/v1/apps/' + APP_ID)
.query({ access_token: token })
.end(function (err, res) {
if (res) console.log('Uninstall progress', res.body.installationState, res.body.installationProgress);
if (res) console.log('Uninstall progress', res.body.installationState, res.body.errorMessage);
if (res.statusCode === 404) return done(null);
if (++count > 50) return done(new Error('Timedout'));
@@ -1171,4 +1128,7 @@ describe('App installation', function () {
done();
});
});
// this is here so that --bail does not stop the box code
it('stop box', stopBox);
});
+18 -22
View File
@@ -6,32 +6,30 @@
'use strict';
var async = require('async'),
config = require('../../config.js'),
constants = require('../../constants.js'),
database = require('../../database.js'),
expect = require('expect.js'),
nock = require('nock'),
path = require('path'),
safe = require('safetydance'),
settings = require('../../settings.js'),
superagent = require('superagent'),
server = require('../../server.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
var SERVER_URL = 'http://localhost:' + constants.PORT;
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var AUTHORIZED_KEYS_FILE = path.join(config.baseDir(), 'authorized_keys');
var token = null;
function setup(done) {
nock.cleanAll();
config._reset();
config.setFqdn('example-ssh-test.com');
safe.fs.unlinkSync(AUTHORIZED_KEYS_FILE);
async.series([
server.start.bind(server),
database._clear,
settings._setApiServerOrigin.bind(null, 'http://localhost:6060'),
settings.setAdmin.bind(null, 'appstore-test.example.com', 'my.appstore-test.example.com'),
function createAdmin(callback) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
@@ -53,8 +51,6 @@ function cleanup(done) {
database._clear(function (error) {
expect(error).to.not.be.ok();
config._reset();
server.stop(done);
});
}
@@ -82,11 +78,11 @@ describe('Appstore Apps API', function () {
});
it('register cloudron', function (done) {
var scope1 = nock(config.apiServerOrigin())
var scope1 = nock(settings.apiServerOrigin())
.post('/api/v1/login', (body) => body.email && body.password)
.reply(200, { userId: 'userId', accessToken: 'SECRET_TOKEN' });
var scope2 = nock(config.apiServerOrigin())
var scope2 = nock(settings.apiServerOrigin())
.post('/api/v1/register_cloudron', (body) => !!body.domain && body.accessToken === 'SECRET_TOKEN')
.reply(201, { cloudronId: 'cid', cloudronToken: 'CLOUDRON_TOKEN', licenseKey: 'lkey' });
@@ -102,8 +98,8 @@ describe('Appstore Apps API', function () {
});
it('can list apps', function (done) {
var scope1 = nock(config.apiServerOrigin())
.get(`/api/v1/apps?accessToken=CLOUDRON_TOKEN&boxVersion=${config.version()}&unstable=false`, () => true)
var scope1 = nock(settings.apiServerOrigin())
.get(`/api/v1/apps?accessToken=CLOUDRON_TOKEN&boxVersion=${constants.VERSION}&unstable=false`, () => true)
.reply(200, { apps: [] });
superagent.get(SERVER_URL + '/api/v1/appstore/apps')
@@ -116,7 +112,7 @@ describe('Appstore Apps API', function () {
});
it('can get app', function (done) {
var scope1 = nock(config.apiServerOrigin())
var scope1 = nock(settings.apiServerOrigin())
.get('/api/v1/apps/org.wordpress.cloudronapp?accessToken=CLOUDRON_TOKEN', () => true)
.reply(200, { apps: [] });
@@ -130,7 +126,7 @@ describe('Appstore Apps API', function () {
});
it('can get app version', function (done) {
var scope1 = nock(config.apiServerOrigin())
var scope1 = nock(settings.apiServerOrigin())
.get('/api/v1/apps/org.wordpress.cloudronapp/versions/3.4.2?accessToken=CLOUDRON_TOKEN', () => true)
.reply(200, { apps: [] });
@@ -150,11 +146,11 @@ describe('Subscription API - no signup', function () {
after(cleanup);
it('can setup subscription', function (done) {
var scope1 = nock(config.apiServerOrigin())
var scope1 = nock(settings.apiServerOrigin())
.post('/api/v1/login', (body) => body.email && body.password)
.reply(200, { userId: 'userId', accessToken: 'SECRET_TOKEN' });
var scope2 = nock(config.apiServerOrigin())
var scope2 = nock(settings.apiServerOrigin())
.post('/api/v1/register_cloudron', (body) => !!body.domain && body.accessToken === 'SECRET_TOKEN')
.reply(201, { cloudronId: 'cid', cloudronToken: 'CLOUDRON_TOKEN', licenseKey: 'lkey' });
@@ -185,15 +181,15 @@ describe('Subscription API - signup', function () {
after(cleanup);
it('can setup subscription', function (done) {
var scope1 = nock(config.apiServerOrigin())
var scope1 = nock(settings.apiServerOrigin())
.post('/api/v1/register_user', (body) => body.email && body.password)
.reply(201, { });
var scope2 = nock(config.apiServerOrigin())
var scope2 = nock(settings.apiServerOrigin())
.post('/api/v1/login', (body) => body.email && body.password)
.reply(200, { userId: 'userId', accessToken: 'SECRET_TOKEN' });
var scope3 = nock(config.apiServerOrigin())
var scope3 = nock(settings.apiServerOrigin())
.post('/api/v1/register_cloudron', (body) => !!body.domain && body.accessToken === 'SECRET_TOKEN')
.reply(201, { cloudronId: 'cid', cloudronToken: 'CLOUDRON_TOKEN', licenseKey: 'lkey' });
@@ -210,7 +206,7 @@ describe('Subscription API - signup', function () {
});
it('can get subscription', function (done) {
var scope1 = nock(config.apiServerOrigin())
var scope1 = nock(settings.apiServerOrigin())
.get('/api/v1/subscription?accessToken=CLOUDRON_TOKEN', () => true)
.reply(200, { subscription: { plan: { id: 'free' } }, email: 'test@cloudron.io' });
+2 -3
View File
@@ -7,7 +7,7 @@
var appdb = require('../../appdb.js'),
async = require('async'),
config = require('../../config.js'),
constants = require('../../constants.js'),
database = require('../../database.js'),
domains = require('../../domains.js'),
expect = require('expect.js'),
@@ -16,7 +16,7 @@ var appdb = require('../../appdb.js'),
server = require('../../server.js'),
settings = require('../../settings.js');
const SERVER_URL = 'http://localhost:' + config.get('port');
const SERVER_URL = 'http://localhost:' + constants.PORT;
const USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
@@ -35,7 +35,6 @@ var token = null;
function setup(done) {
nock.cleanAll();
config._reset();
async.series([
server.start,
+2 -5
View File
@@ -7,7 +7,7 @@
var accesscontrol = require('../../accesscontrol.js'),
async = require('async'),
config = require('../../config.js'),
constants = require('../../constants.js'),
clients = require('../../clients.js'),
database = require('../../database.js'),
oauth2 = require('../oauth2.js'),
@@ -17,15 +17,12 @@ var accesscontrol = require('../../accesscontrol.js'),
superagent = require('superagent'),
server = require('../../server.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
var SERVER_URL = 'http://localhost:' + constants.PORT;
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null;
function setup(done) {
config._reset();
config.setFqdn('example-clients-test.com');
async.series([
server.start,
database._clear,
+7 -11
View File
@@ -6,7 +6,7 @@
/* global after:false */
let async = require('async'),
config = require('../../config.js'),
constants = require('../../constants.js'),
database = require('../../database.js'),
expect = require('expect.js'),
hat = require('../../hat.js'),
@@ -18,7 +18,7 @@ let async = require('async'),
settings = require('../../settings.js'),
tokendb = require('../../tokendb.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
var SERVER_URL = 'http://localhost:' + constants.PORT;
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null; // authentication token
@@ -26,13 +26,11 @@ var USERNAME_1 = 'userTheFirst', EMAIL_1 = 'taO@zen.mac', userId_1, token_1;
function setup(done) {
nock.cleanAll();
config._reset();
config.setFqdn('example-cloudron-test.com');
config.setAdminFqdn('my.example-cloudron-test.com');
async.series([
server.start.bind(server),
database._clear,
settings._setApiServerOrigin.bind(null, 'http://localhost:6060'),
settings.setBackupConfig.bind(null, { provider: 'filesystem', backupFolder: '/tmp', format: 'tgz' })
], done);
}
@@ -41,8 +39,6 @@ function cleanup(done) {
database._clear(function (error) {
expect(error).to.not.be.ok();
config._reset();
server.stop(done);
});
}
@@ -187,9 +183,9 @@ describe('Cloudron', function () {
.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.adminFqdn).to.eql(config.adminFqdn());
expect(result.body.version).to.eql(config.version());
expect(result.body.webServerOrigin).to.eql('https://cloudron.io');
expect(result.body.adminFqdn).to.eql(settings.adminFqdn());
expect(result.body.version).to.eql(constants.VERSION);
expect(result.body.memory).to.eql(os.totalmem());
expect(result.body.cloudronName).to.be.a('string');
@@ -242,7 +238,7 @@ describe('Cloudron', function () {
it('logStream - stream logs', function (done) {
var options = {
host: 'localhost',
port: config.get('port'),
port: constants.PORT,
path: '/api/v1/cloudron/logstream/box?lines=10&access_token=' + token,
headers: { 'Accept': 'text/event-stream', 'Connection': 'keep-alive' }
};
+5 -8
View File
@@ -7,21 +7,18 @@
/* global after:false */
var async = require('async'),
config = require('../../config.js'),
constants = require('../../constants.js'),
database = require('../../database.js'),
expect = require('expect.js'),
speakeasy = require('speakeasy'),
superagent = require('superagent'),
server = require('../../server.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
var SERVER_URL = 'http://localhost:' + constants.PORT;
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
function setup(done) {
config._reset();
config.setFqdn('example-developer-test.com');
async.series([
server.start.bind(server),
database._clear
@@ -179,7 +176,7 @@ describe('Developer API', function () {
async.series([
setup,
function (callback) {
superagent.post(`${SERVER_URL}/api/v1/cloudron/activate`).query({ setupToken: 'somesetuptoken' }).send({ username: USERNAME, password: PASSWORD, email: EMAIL }).end(function (error, result) {
superagent.post(`${SERVER_URL}/api/v1/cloudron/activate`).query({ setupToken: 'somesetuptoken' }).send({ username: USERNAME, password: PASSWORD, email: EMAIL }).end(function (error) {
callback(error);
});
},
@@ -201,7 +198,7 @@ describe('Developer API', function () {
encoding: 'base32'
});
superagent.post(`${SERVER_URL}/api/v1/profile/twofactorauthentication/enable`).query({ access_token: accessToken }).send({ totpToken: totpToken }).end(function (error, result) {
superagent.post(`${SERVER_URL}/api/v1/profile/twofactorauthentication/enable`).query({ access_token: accessToken }).send({ totpToken: totpToken }).end(function (error) {
callback(error);
});
}
@@ -211,7 +208,7 @@ describe('Developer API', function () {
after(function (done) {
async.series([
function (callback) {
superagent.post(`${SERVER_URL}/api/v1/profile/twofactorauthentication/disable`).query({ access_token: accessToken }).send({ password: PASSWORD }).end(function (error, result) {
superagent.post(`${SERVER_URL}/api/v1/profile/twofactorauthentication/disable`).query({ access_token: accessToken }).send({ password: PASSWORD }).end(function (error) {
callback(error);
});
},
+2 -6
View File
@@ -7,7 +7,7 @@
var async = require('async'),
child_process = require('child_process'),
config = require('../../config.js'),
constants = require('../../constants.js'),
database = require('../../database.js'),
domaindb = require('../../domaindb.js'),
expect = require('expect.js'),
@@ -18,11 +18,10 @@ var async = require('async'),
server = require('../../server.js'),
_ = require('underscore');
var SERVER_URL = 'http://localhost:' + config.get('port');
var SERVER_URL = 'http://localhost:' + constants.PORT;
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null;
var DOMAIN = 'example-domains-test.com';
var DOMAIN_0 = {
domain: 'cloudron.com',
@@ -45,9 +44,6 @@ var DOMAIN_1 = {
describe('Domains API', function () {
before(function (done) {
config._reset();
config.setFqdn(DOMAIN);
async.series([
server.start.bind(null),
database._clear.bind(null),
+2 -5
View File
@@ -8,7 +8,7 @@
var accesscontrol = require('../../accesscontrol.js'),
async = require('async'),
config = require('../../config.js'),
constants = require('../../constants.js'),
database = require('../../database.js'),
eventlogdb = require('../../eventlogdb.js'),
expect = require('expect.js'),
@@ -17,7 +17,7 @@ var accesscontrol = require('../../accesscontrol.js'),
server = require('../../server.js'),
tokendb = require('../../tokendb.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
var SERVER_URL = 'http://localhost:' + constants.PORT;
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null;
@@ -36,9 +36,6 @@ var EVENT_0 = {
};
function setup(done) {
config._reset();
config.setFqdn('example-eventlog-test.com');
async.series([
server.start.bind(server),
+3 -6
View File
@@ -8,7 +8,7 @@
var accesscontrol = require('../../accesscontrol.js'),
async = require('async'),
config = require('../../config.js'),
constants = require('../../constants.js'),
database = require('../../database.js'),
expect = require('expect.js'),
hat = require('../../hat.js'),
@@ -16,7 +16,7 @@ var accesscontrol = require('../../accesscontrol.js'),
superagent = require('superagent'),
tokendb = require('../../tokendb.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
var SERVER_URL = 'http://localhost:' + constants.PORT;
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var USERNAME_1 = 'user', PASSWORD_1 = 'Foobar?1337', EMAIL_1 ='happy@me.com';
@@ -27,9 +27,6 @@ var GROUP_NAME = 'externals';
var groupObject, group1Object;
function setup(done) {
config._reset();
config.setFqdn('example-groups-test.com');
async.series([
server.start.bind(server),
@@ -127,7 +124,7 @@ describe('Groups API', function () {
group1Object = result.body;
done();
});
})
});
it('cannot add user to invalid group', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + userId + '/groups')
+23 -24
View File
@@ -6,17 +6,18 @@
/* global after:false */
var async = require('async'),
config = require('../../config.js'),
constants = require('../../constants.js'),
database = require('../../database.js'),
expect = require('expect.js'),
mail = require('../../mail.js'),
maildb = require('../../maildb.js'),
server = require('../../server.js'),
settings = require('../../settings.js'),
superagent = require('superagent'),
userdb = require('../../userdb.js'),
_ = require('underscore');
var SERVER_URL = 'http://localhost:' + config.get('port');
var SERVER_URL = 'http://localhost:' + constants.PORT;
const ADMIN_DOMAIN = {
domain: 'admin.com',
@@ -41,8 +42,6 @@ var token = null;
var userId = '';
function setup(done) {
config._reset();
async.series([
server.start.bind(null),
database._clear.bind(null),
@@ -310,7 +309,7 @@ describe('Mail API', function () {
expect(res.body.dns.spf.domain).to.eql(spfDomain);
expect(res.body.dns.spf.type).to.eql('TXT');
expect(res.body.dns.spf.value).to.eql(null);
expect(res.body.dns.spf.expected).to.eql('v=spf1 a:' + config.adminFqdn() + ' ~all');
expect(res.body.dns.spf.expected).to.eql('v=spf1 a:' + settings.adminFqdn() + ' ~all');
expect(res.body.dns.spf.status).to.eql(false);
expect(res.body.dns.dmarc).to.be.an('object');
@@ -322,13 +321,13 @@ describe('Mail API', function () {
expect(res.body.dns.mx).to.be.an('object');
expect(res.body.dns.mx.type).to.eql('MX');
expect(res.body.dns.mx.value).to.eql(null);
expect(res.body.dns.mx.expected).to.eql('10 ' + config.mailFqdn() + '.');
expect(res.body.dns.mx.expected).to.eql('10 ' + settings.mailFqdn() + '.');
expect(res.body.dns.mx.status).to.eql(false);
expect(res.body.dns.ptr).to.be.an('object');
expect(res.body.dns.ptr.type).to.eql('PTR');
// expect(res.body.ptr.value).to.eql(null); this will be anything random
expect(res.body.dns.ptr.expected).to.eql(config.mailFqdn());
expect(res.body.dns.ptr.expected).to.eql(settings.mailFqdn());
expect(res.body.dns.ptr.status).to.eql(false);
done();
@@ -349,7 +348,7 @@ describe('Mail API', function () {
expect(res.statusCode).to.equal(200);
expect(res.body.dns.spf).to.be.an('object');
expect(res.body.dns.spf.expected).to.eql('v=spf1 a:' + config.adminFqdn() + ' ~all');
expect(res.body.dns.spf.expected).to.eql('v=spf1 a:' + settings.adminFqdn() + ' ~all');
expect(res.body.dns.spf.status).to.eql(false);
expect(res.body.dns.spf.value).to.eql(null);
@@ -365,11 +364,11 @@ describe('Mail API', function () {
expect(res.body.dns.mx).to.be.an('object');
expect(res.body.dns.mx.status).to.eql(false);
expect(res.body.dns.mx.expected).to.eql('10 ' + config.mailFqdn() + '.');
expect(res.body.dns.mx.expected).to.eql('10 ' + settings.mailFqdn() + '.');
expect(res.body.dns.mx.value).to.eql(null);
expect(res.body.dns.ptr).to.be.an('object');
expect(res.body.dns.ptr.expected).to.eql(config.mailFqdn());
expect(res.body.dns.ptr.expected).to.eql(settings.mailFqdn());
expect(res.body.dns.ptr.status).to.eql(false);
// expect(res.body.ptr.value).to.eql(null); this will be anything random
@@ -380,7 +379,7 @@ describe('Mail API', function () {
it('succeeds with all different spf, dkim, dmarc, mx, ptr records', function (done) {
clearDnsAnswerQueue();
dnsAnswerQueue[mxDomain].MX = [ { priority: '20', exchange: config.mailFqdn() }, { priority: '30', exchange: config.mailFqdn() } ];
dnsAnswerQueue[mxDomain].MX = [ { priority: '20', exchange: settings.mailFqdn() }, { priority: '30', exchange: settings.mailFqdn() } ];
dnsAnswerQueue[dmarcDomain].TXT = [['v=DMARC2; p=reject; pct=100']];
dnsAnswerQueue[dkimDomain].TXT = [['v=DKIM2; t=s; p=' + mail._readDkimPublicKeySync(DOMAIN_0.domain)]];
dnsAnswerQueue[spfDomain].TXT = [['v=spf1 a:random.com ~all']];
@@ -391,7 +390,7 @@ describe('Mail API', function () {
expect(res.statusCode).to.equal(200);
expect(res.body.dns.spf).to.be.an('object');
expect(res.body.dns.spf.expected).to.eql('v=spf1 a:' + config.adminFqdn() + ' a:random.com ~all');
expect(res.body.dns.spf.expected).to.eql('v=spf1 a:' + settings.adminFqdn() + ' a:random.com ~all');
expect(res.body.dns.spf.status).to.eql(false);
expect(res.body.dns.spf.value).to.eql('v=spf1 a:random.com ~all');
@@ -407,11 +406,11 @@ describe('Mail API', function () {
expect(res.body.dns.mx).to.be.an('object');
expect(res.body.dns.mx.status).to.eql(false);
expect(res.body.dns.mx.expected).to.eql('10 ' + config.mailFqdn() + '.');
expect(res.body.dns.mx.value).to.eql('20 ' + config.mailFqdn() + '. 30 ' + config.mailFqdn() + '.');
expect(res.body.dns.mx.expected).to.eql('10 ' + settings.mailFqdn() + '.');
expect(res.body.dns.mx.value).to.eql('20 ' + settings.mailFqdn() + '. 30 ' + settings.mailFqdn() + '.');
expect(res.body.dns.ptr).to.be.an('object');
expect(res.body.dns.ptr.expected).to.eql(config.mailFqdn());
expect(res.body.dns.ptr.expected).to.eql(settings.mailFqdn());
expect(res.body.dns.ptr.status).to.eql(false);
// expect(res.body.ptr.value).to.eql(null); this will be anything random
@@ -424,7 +423,7 @@ describe('Mail API', function () {
it('succeeds with existing embedded spf', function (done) {
clearDnsAnswerQueue();
dnsAnswerQueue[spfDomain].TXT = [['v=spf1 a:example.com a:' + config.mailFqdn() + ' ~all']];
dnsAnswerQueue[spfDomain].TXT = [['v=spf1 a:example.com a:' + settings.mailFqdn() + ' ~all']];
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/status')
.query({ access_token: token })
@@ -434,8 +433,8 @@ describe('Mail API', function () {
expect(res.body.dns.spf).to.be.an('object');
expect(res.body.dns.spf.domain).to.eql(spfDomain);
expect(res.body.dns.spf.type).to.eql('TXT');
expect(res.body.dns.spf.value).to.eql('v=spf1 a:example.com a:' + config.mailFqdn() + ' ~all');
expect(res.body.dns.spf.expected).to.eql('v=spf1 a:example.com a:' + config.mailFqdn() + ' ~all');
expect(res.body.dns.spf.value).to.eql('v=spf1 a:example.com a:' + settings.mailFqdn() + ' ~all');
expect(res.body.dns.spf.expected).to.eql('v=spf1 a:example.com a:' + settings.mailFqdn() + ' ~all');
expect(res.body.dns.spf.status).to.eql(true);
done();
@@ -464,10 +463,10 @@ describe('Mail API', function () {
it('succeeds with all correct records', function (done) {
clearDnsAnswerQueue();
dnsAnswerQueue[mxDomain].MX = [ { priority: '10', exchange: config.mailFqdn() } ];
dnsAnswerQueue[mxDomain].MX = [ { priority: '10', exchange: settings.mailFqdn() } ];
dnsAnswerQueue[dmarcDomain].TXT = [['v=DMARC1; p=reject; pct=100']];
dnsAnswerQueue[dkimDomain].TXT = [['v=DKIM1; t=s; p=', mail._readDkimPublicKeySync(DOMAIN_0.domain) ]];
dnsAnswerQueue[spfDomain].TXT = [['v=spf1 a:' + config.adminFqdn() + ' ~all']];
dnsAnswerQueue[spfDomain].TXT = [['v=spf1 a:' + settings.adminFqdn() + ' ~all']];
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/status')
.query({ access_token: token })
@@ -484,8 +483,8 @@ describe('Mail API', function () {
expect(res.body.dns.spf).to.be.an('object');
expect(res.body.dns.spf.domain).to.eql(spfDomain);
expect(res.body.dns.spf.type).to.eql('TXT');
expect(res.body.dns.spf.value).to.eql('v=spf1 a:' + config.adminFqdn() + ' ~all');
expect(res.body.dns.spf.expected).to.eql('v=spf1 a:' + config.adminFqdn() + ' ~all');
expect(res.body.dns.spf.value).to.eql('v=spf1 a:' + settings.adminFqdn() + ' ~all');
expect(res.body.dns.spf.expected).to.eql('v=spf1 a:' + settings.adminFqdn() + ' ~all');
expect(res.body.dns.spf.status).to.eql(true);
expect(res.body.dns.dmarc).to.be.an('object');
@@ -495,8 +494,8 @@ describe('Mail API', function () {
expect(res.body.dns.mx).to.be.an('object');
expect(res.body.dns.mx.status).to.eql(true);
expect(res.body.dns.mx.expected).to.eql('10 ' + config.mailFqdn() + '.');
expect(res.body.dns.mx.value).to.eql('10 ' + config.mailFqdn() + '.');
expect(res.body.dns.mx.expected).to.eql('10 ' + settings.mailFqdn() + '.');
expect(res.body.dns.mx.value).to.eql('10 ' + settings.mailFqdn() + '.');
done();
});
+52 -54
View File
@@ -11,7 +11,7 @@ var accesscontrol = require('../../accesscontrol.js'),
async = require('async'),
clientdb = require('../../clientdb.js'),
clients = require('../../clients.js'),
config = require('../../config.js'),
constants = require('../../constants.js'),
database = require('../../database.js'),
domains = require('../../domains.js'),
expect = require('expect.js'),
@@ -21,6 +21,7 @@ var accesscontrol = require('../../accesscontrol.js'),
querystring = require('querystring'),
request = require('request'),
server = require('../../server.js'),
settings = require('../../settings.js'),
speakeasy = require('speakeasy'),
superagent = require('superagent'),
urlParse = require('url').parse,
@@ -28,7 +29,7 @@ var accesscontrol = require('../../accesscontrol.js'),
users = require('../../users.js'),
uuid = require('uuid');
var SERVER_URL = 'http://localhost:' + config.get('port');
var SERVER_URL = 'http://localhost:' + constants.PORT;
let AUDIT_SOURCE = { ip: '1.2.3.4', userId: 'someuserid' };
@@ -53,7 +54,8 @@ describe('OAuth2', function () {
createdAt: (new Date()).toUTCString(),
modifiedAt: (new Date()).toUTCString(),
resetToken: hat(256),
displayName: ''
displayName: '',
source: ''
};
var APP_0 = {
@@ -199,14 +201,11 @@ describe('OAuth2', function () {
};
function setup(done) {
config._reset();
config.setFqdn(APP_0.domain);
config.setAdminFqdn('my.' + APP_0.domain);
async.series([
server.start,
database._clear,
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE),
settings.setAdmin.bind(null, DOMAIN_0.domain, 'my.' + DOMAIN_0.domain),
clientdb.add.bind(null, CLIENT_0.id, CLIENT_0.appId, CLIENT_0.type, CLIENT_0.clientSecret, CLIENT_0.redirectURI, CLIENT_0.scope),
clientdb.add.bind(null, CLIENT_1.id, CLIENT_1.appId, CLIENT_1.type, CLIENT_1.clientSecret, CLIENT_1.redirectURI, CLIENT_1.scope),
clientdb.add.bind(null, CLIENT_2.id, CLIENT_2.appId, CLIENT_2.type, CLIENT_2.clientSecret, CLIENT_2.redirectURI, CLIENT_2.scope),
@@ -368,7 +367,7 @@ describe('OAuth2', function () {
expect(response.statusCode).to.eql(200);
expect(body).to.eql('<script>window.location.href = "/api/v1/session/login?returnTo=' + CLIENT_0.redirectURI + '";</script>');
request.get(SERVER_URL + '/api/v1/session/login?returnTo=' + CLIENT_0.redirectURI, { jar: true, followRedirect: false }, function (error, response, body) {
request.get(SERVER_URL + '/api/v1/session/login?returnTo=' + CLIENT_0.redirectURI, { jar: true, followRedirect: false }, function (error, response) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
expect(response.headers.location).to.eql(CLIENT_0.redirectURI);
@@ -385,7 +384,7 @@ describe('OAuth2', function () {
expect(response.statusCode).to.eql(200);
expect(body).to.eql('<script>window.location.href = "/api/v1/session/login?returnTo=' + CLIENT_1.redirectURI + '";</script>');
request.get(SERVER_URL + '/api/v1/session/login?returnTo=' + CLIENT_1.redirectURI, { jar: true, followRedirect: false }, function (error, response, body) {
request.get(SERVER_URL + '/api/v1/session/login?returnTo=' + CLIENT_1.redirectURI, { jar: true, followRedirect: false }, function (error, response) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
expect(response.headers.location).to.eql(CLIENT_1.redirectURI);
@@ -436,7 +435,7 @@ describe('OAuth2', function () {
expect(response.statusCode).to.eql(200);
expect(body).to.eql('<script>window.location.href = "/api/v1/session/login?returnTo=' + CLIENT_4.redirectURI + '";</script>');
request.get(SERVER_URL + '/api/v1/session/login?returnTo=' + CLIENT_4.redirectURI, { jar: true, followRedirect: false }, function (error, response, body) {
request.get(SERVER_URL + '/api/v1/session/login?returnTo=' + CLIENT_4.redirectURI, { jar: true, followRedirect: false }, function (error, response) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
expect(response.headers.location).to.eql(CLIENT_4.redirectURI);
@@ -492,7 +491,7 @@ describe('OAuth2', function () {
var url = SERVER_URL + '/api/v1/session/login?returnTo=' + CLIENT_2.redirectURI;
var data = {};
request.post({ url: url, jar: jar, form: data }, function (error, response, body) {
request.post({ url: url, jar: jar, form: data }, function (error, response) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -513,7 +512,7 @@ describe('OAuth2', function () {
password: USER_0.password
};
request.post({ url: url, jar: jar, form: data }, function (error, response, body) {
request.post({ url: url, jar: jar, form: data }, function (error, response) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -534,7 +533,7 @@ describe('OAuth2', function () {
password: 'password'
};
request.post({ url: url, jar: jar, form: data }, function (error, response, body) {
request.post({ url: url, jar: jar, form: data }, function (error, response) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -555,7 +554,7 @@ describe('OAuth2', function () {
password: USER_0.password
};
request.post({ url: url, jar: jar, form: data }, function (error, response, body) {
request.post({ url: url, jar: jar, form: data }, function (error, response) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -577,7 +576,7 @@ describe('OAuth2', function () {
password: USER_0.password
};
request.post({ url: url, jar: jar, form: data }, function (error, response, body) {
request.post({ url: url, jar: jar, form: data }, function (error, response) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -616,7 +615,7 @@ describe('OAuth2', function () {
encoding: 'base32'
});
superagent.post(`${SERVER_URL}/api/v1/profile/twofactorauthentication/enable`).query({ access_token: accessToken }).send({ totpToken: totpToken }).end(function (error, result) {
superagent.post(`${SERVER_URL}/api/v1/profile/twofactorauthentication/enable`).query({ access_token: accessToken }).send({ totpToken: totpToken }).end(function (error) {
callback(error);
});
}
@@ -652,7 +651,7 @@ describe('OAuth2', function () {
password: USER_0.password
};
request.post({ url: url, jar: jar, form: data }, function (error, response, body) {
request.post({ url: url, jar: jar, form: data }, function (error, response) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -674,7 +673,7 @@ describe('OAuth2', function () {
totpToken: 'wrongtoken'
};
request.post({ url: url, jar: jar, form: data }, function (error, response, body) {
request.post({ url: url, jar: jar, form: data }, function (error, response) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -700,7 +699,7 @@ describe('OAuth2', function () {
totpToken: totpToken
};
request.post({ url: url, jar: jar, form: data }, function (error, response, body) {
request.post({ url: url, jar: jar, form: data }, function (error, response) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -739,7 +738,7 @@ describe('OAuth2', function () {
password: USER_0.password
};
request.post({ url: url, jar: jar, form: data }, function (error, response, body) {
request.post({ url: url, jar: jar, form: data }, function (error, response) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -758,7 +757,7 @@ describe('OAuth2', function () {
startAuthorizationFlow(CLIENT_2, 'code', function (jar) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_2.redirectURI + '&client_id=' + CLIENT_2.id + '&response_type=code';
request.get(url, { jar: jar, followRedirect: false }, function (error, response, body) {
request.get(url, { jar: jar, followRedirect: false }, function (error, response) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -775,7 +774,7 @@ describe('OAuth2', function () {
startAuthorizationFlow(CLIENT_2, 'token', function (jar) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_2.redirectURI + '&client_id=' + CLIENT_2.id + '&response_type=token';
request.get(url, { jar: jar, followRedirect: false }, function (error, response, body) {
request.get(url, { jar: jar, followRedirect: false }, function (error, response) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -810,7 +809,7 @@ describe('OAuth2', function () {
startAuthorizationFlow(CLIENT_7, 'code', function (jar) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_7.redirectURI + '&client_id=' + CLIENT_7.id + '&response_type=code';
request.get(url, { jar: jar, followRedirect: false }, function (error, response, body) {
request.get(url, { jar: jar, followRedirect: false }, function (error, response) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -857,7 +856,7 @@ describe('OAuth2', function () {
startAuthorizationFlow(CLIENT_7, 'token', function (jar) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_7.redirectURI + '&client_id=' + CLIENT_7.id + '&response_type=token';
request.get(url, { jar: jar, followRedirect: false }, function (error, response, body) {
request.get(url, { jar: jar, followRedirect: false }, function (error, response) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -883,7 +882,7 @@ describe('OAuth2', function () {
it('fails after logout', function (done) {
startAuthorizationFlow(CLIENT_2, 'token', function (jar) {
request.get(SERVER_URL + '/api/v1/session/logout', { jar: jar, followRedirect: false }, function (error, response, body) {
request.get(SERVER_URL + '/api/v1/session/logout', { jar: jar, followRedirect: false }, function (error, response) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
expect(response.headers.location).to.eql('/');
@@ -903,7 +902,7 @@ describe('OAuth2', function () {
it('fails after logout width redirect', function (done) {
startAuthorizationFlow(CLIENT_2, 'token', function (jar) {
request.get(SERVER_URL + '/api/v1/session/logout', { jar: jar, followRedirect: false, qs: { redirect: 'http://foobar' } }, function (error, response, body) {
request.get(SERVER_URL + '/api/v1/session/logout', { jar: jar, followRedirect: false, qs: { redirect: 'http://foobar' } }, function (error, response) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
expect(response.headers.location).to.eql('http://foobar');
@@ -945,7 +944,7 @@ describe('OAuth2', function () {
password: USER_0.password
};
request.post({ url: url, jar: jar, form: data }, function (error, response, body) {
request.post({ url: url, jar: jar, form: data }, function (error, response) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -964,7 +963,7 @@ describe('OAuth2', function () {
startAuthorizationFlow('code', function (jar) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_2.redirectURI + '&client_id=' + CLIENT_2.id + '&response_type=code';
request.get(url, { jar: jar, followRedirect: false }, function (error, response, body) {
request.get(url, { jar: jar, followRedirect: false }, function (error, response) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -1017,7 +1016,7 @@ describe('OAuth2', function () {
password: USER_0.password
};
request.post({ url: url, jar: jar, form: data }, function (error, response, body) {
request.post({ url: url, jar: jar, form: data }, function (error, response) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -1036,7 +1035,7 @@ describe('OAuth2', function () {
startAuthorizationFlow('code', function (jar) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_2.redirectURI + '&client_id=' + CLIENT_2.id + '&response_type=code';
request.get(url, { jar: jar, followRedirect: false }, function (error, response, body) {
request.get(url, { jar: jar, followRedirect: false }, function (error, response) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -1045,7 +1044,7 @@ describe('OAuth2', function () {
expect(tmp.query.redirectURI).to.eql(CLIENT_2.redirectURI + '/');
expect(tmp.query.code).to.be.a('string');
request.post(SERVER_URL + '/api/v1/oauth/token', { jar: jar }, function (error, response, body) {
request.post(SERVER_URL + '/api/v1/oauth/token', { jar: jar }, function (error, response) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(401);
@@ -1059,7 +1058,7 @@ describe('OAuth2', function () {
startAuthorizationFlow('code', function (jar) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_2.redirectURI + '&client_id=' + CLIENT_2.id + '&response_type=code';
request.get(url, { jar: jar, followRedirect: false }, function (error, response, body) {
request.get(url, { jar: jar, followRedirect: false }, function (error, response) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -1075,7 +1074,7 @@ describe('OAuth2', function () {
client_secret: CLIENT_2.clientSecret
};
request.post(SERVER_URL + '/api/v1/oauth/token', { jar: jar, json: data }, function (error, response, body) {
request.post(SERVER_URL + '/api/v1/oauth/token', { jar: jar, json: data }, function (error, response) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(401);
done();
@@ -1088,7 +1087,7 @@ describe('OAuth2', function () {
startAuthorizationFlow('code', function (jar) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_2.redirectURI + '&client_id=' + CLIENT_2.id + '&response_type=code';
request.get(url, { jar: jar, followRedirect: false }, function (error, response, body) {
request.get(url, { jar: jar, followRedirect: false }, function (error, response) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -1104,7 +1103,7 @@ describe('OAuth2', function () {
client_secret: CLIENT_2.clientSecret
};
request.post(SERVER_URL + '/api/v1/oauth/token', { jar: jar, json: data }, function (error, response, body) {
request.post(SERVER_URL + '/api/v1/oauth/token', { jar: jar, json: data }, function (error, response) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(501);
done();
@@ -1117,7 +1116,7 @@ describe('OAuth2', function () {
startAuthorizationFlow('code', function (jar) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_2.redirectURI + '&client_id=' + CLIENT_2.id + '&response_type=code';
request.get(url, { jar: jar, followRedirect: false }, function (error, response, body) {
request.get(url, { jar: jar, followRedirect: false }, function (error, response) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -1133,7 +1132,7 @@ describe('OAuth2', function () {
client_secret: CLIENT_2.clientSecret
};
request.post(SERVER_URL + '/api/v1/oauth/token', { jar: jar, json: data }, function (error, response, body) {
request.post(SERVER_URL + '/api/v1/oauth/token', { jar: jar, json: data }, function (error, response) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(400);
done();
@@ -1146,7 +1145,7 @@ describe('OAuth2', function () {
startAuthorizationFlow('code', function (jar) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_2.redirectURI + '&client_id=' + CLIENT_2.id + '&response_type=code';
request.get(url, { jar: jar, followRedirect: false }, function (error, response, body) {
request.get(url, { jar: jar, followRedirect: false }, function (error, response) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -1162,7 +1161,7 @@ describe('OAuth2', function () {
// client_secret: CLIENT_2.clientSecret
};
request.post(SERVER_URL + '/api/v1/oauth/token', { jar: jar, json: data }, function (error, response, body) {
request.post(SERVER_URL + '/api/v1/oauth/token', { jar: jar, json: data }, function (error, response) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(401);
done();
@@ -1175,7 +1174,7 @@ describe('OAuth2', function () {
startAuthorizationFlow('code', function (jar) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_2.redirectURI + '&client_id=' + CLIENT_2.id + '&response_type=code';
request.get(url, { jar: jar, followRedirect: false }, function (error, response, body) {
request.get(url, { jar: jar, followRedirect: false }, function (error, response) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -1191,7 +1190,7 @@ describe('OAuth2', function () {
client_secret: CLIENT_2.clientSecret+CLIENT_2.clientSecret
};
request.post(SERVER_URL + '/api/v1/oauth/token', { jar: jar, json: data }, function (error, response, body) {
request.post(SERVER_URL + '/api/v1/oauth/token', { jar: jar, json: data }, function (error, response) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(401);
done();
@@ -1204,7 +1203,7 @@ describe('OAuth2', function () {
startAuthorizationFlow('code', function (jar) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_2.redirectURI + '&client_id=' + CLIENT_2.id + '&response_type=code';
request.get(url, { jar: jar, followRedirect: false }, function (error, response, body) {
request.get(url, { jar: jar, followRedirect: false }, function (error, response) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -1220,7 +1219,7 @@ describe('OAuth2', function () {
client_secret: CLIENT_2.clientSecret
};
request.post(SERVER_URL + '/api/v1/oauth/token', { jar: jar, json: data }, function (error, response, body) {
request.post(SERVER_URL + '/api/v1/oauth/token', { jar: jar, json: data }, function (error, response) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(401);
done();
@@ -1233,7 +1232,7 @@ describe('OAuth2', function () {
startAuthorizationFlow('code', function (jar) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_2.redirectURI + '&client_id=' + CLIENT_2.id + '&response_type=code';
request.get(url, { jar: jar, followRedirect: false }, function (error, response, body) {
request.get(url, { jar: jar, followRedirect: false }, function (error, response) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -1283,7 +1282,8 @@ describe('Password', function () {
createdAt: (new Date()).toUTCString(),
modifiedAt: (new Date()).toUTCString(),
resetToken: hat(256),
displayName: ''
displayName: '',
source: ''
};
// make csrf always succeed for testing
@@ -1295,14 +1295,12 @@ describe('Password', function () {
};
function setup(done) {
server.start(function (error) {
expect(error).to.not.be.ok();
database._clear(function (error) {
expect(error).to.not.be.ok();
userdb.add(USER_0.userId, USER_0, done);
});
});
async.series([
server.start,
database._clear,
settings.setAdmin.bind(null, 'example.com', 'my.example.com'),
userdb.add.bind(null, USER_0.userId, USER_0)
], done);
}
function cleanup(done) {
@@ -1476,7 +1474,7 @@ describe('Password', function () {
});
it('succeeds', function (done) {
var scope = nock(config.adminOrigin())
var scope = nock(settings.adminOrigin())
.filteringPath(function (path) {
path = path.replace(/accessToken=[^&]*/, 'accessToken=token');
path = path.replace(/expiresAt=[^&]*/, 'expiresAt=1234');
+2 -5
View File
@@ -7,7 +7,7 @@
'use strict';
var accesscontrol = require('../../accesscontrol.js'),
config = require('../../config.js'),
constants = require('../../constants.js'),
database = require('../../database.js'),
expect = require('expect.js'),
hat = require('../../hat.js'),
@@ -16,7 +16,7 @@ var accesscontrol = require('../../accesscontrol.js'),
server = require('../../server.js'),
tokendb = require('../../tokendb.js');
const SERVER_URL = 'http://localhost:' + config.get('port');
const SERVER_URL = 'http://localhost:' + constants.PORT;
const USERNAME_0 = 'superaDmIn';
const PASSWORD = 'Foobar?1337';
@@ -30,9 +30,6 @@ describe('Profile API', function () {
var token_0;
function setup(done) {
config._reset();
config.setFqdn('example-profile-test.com');
server.start(function (error) {
expect(!error).to.be.ok();
+2 -4
View File
@@ -7,20 +7,18 @@
'use strict';
var async = require('async'),
config = require('../../config.js'),
constants = require('../../constants.js'),
database = require('../../database.js'),
expect = require('expect.js'),
superagent = require('superagent'),
server = require('../../server.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
var SERVER_URL = 'http://localhost:' + constants.PORT;
var DOMAIN = 'example-server-test.com';
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null;
function setup(done) {
config._reset();
async.series([
server.start,
database._clear
+1 -6
View File
@@ -6,7 +6,6 @@
/* global after:false */
var async = require('async'),
config = require('../../config.js'),
constants = require('../../constants.js'),
database = require('../../database.js'),
expect = require('expect.js'),
@@ -15,16 +14,12 @@ var async = require('async'),
server = require('../../server.js'),
superagent = require('superagent');
var SERVER_URL = 'http://localhost:' + config.get('port');
var SERVER_URL = 'http://localhost:' + constants.PORT;
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null;
function setup(done) {
config._reset();
config.setFqdn('example-settings-test.com');
config.setAdminFqdn('my.example-settings-test.com');
async.series([
server.start.bind(null),
database._clear.bind(null),
+6 -9
View File
@@ -6,27 +6,26 @@
'use strict';
var async = require('async'),
config = require('../../config.js'),
constants = require('../../constants.js'),
database = require('../../database.js'),
expect = require('expect.js'),
nock = require('nock'),
path = require('path'),
paths = require('../../paths.js'),
safe = require('safetydance'),
settings = require('../../settings.js'),
settingsdb = require('../../settingsdb.js'),
superagent = require('superagent'),
server = require('../../server.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
var SERVER_URL = 'http://localhost:' + constants.PORT;
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var AUTHORIZED_KEYS_FILE = path.join(config.baseDir(), 'authorized_keys');
var AUTHORIZED_KEYS_FILE = path.join(paths.baseDir(), 'authorized_keys');
var token = null;
function setup(done) {
nock.cleanAll();
config._reset();
config.setFqdn('example-ssh-test.com');
safe.fs.unlinkSync(AUTHORIZED_KEYS_FILE);
async.series([
@@ -57,8 +56,6 @@ function cleanup(done) {
database._clear(function (error) {
expect(error).to.not.be.ok();
config._reset();
server.stop(done);
});
}
@@ -229,7 +226,7 @@ describe('Support API', function () {
});
it('succeeds with ticket type', function (done) {
var scope2 = nock(config.apiServerOrigin())
var scope2 = nock(settings.apiServerOrigin())
.filteringRequestBody(function (/* unusedBody */) { return ''; }) // strip out body
.post('/api/v1/ticket?accessToken=CLOUDRON_TOKEN')
.reply(201, { });
@@ -245,7 +242,7 @@ describe('Support API', function () {
});
it('succeeds with app type', function (done) {
var scope2 = nock(config.apiServerOrigin())
var scope2 = nock(settings.apiServerOrigin())
.filteringRequestBody(function (/* unusedBody */) { return ''; }) // strip out body
.post('/api/v1/ticket?accessToken=CLOUDRON_TOKEN')
.reply(201, { });
+3 -6
View File
@@ -6,7 +6,7 @@
'use strict';
var async = require('async'),
config = require('../../config.js'),
constants = require('../../constants.js'),
database = require('../../database.js'),
domains = require('../../domains.js'),
eventlog = require('../../eventlog.js'),
@@ -16,7 +16,7 @@ var async = require('async'),
settingsdb = require('../../settingsdb.js'),
superagent = require('superagent');
const SERVER_URL = 'http://localhost:' + config.get('port');
const SERVER_URL = 'http://localhost:' + constants.PORT;
const USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
@@ -32,9 +32,6 @@ const DOMAIN_0 = {
let AUDIT_SOURCE = { ip: '1.2.3.4' };
function setup(done) {
config._reset();
config.setFqdn(DOMAIN_0.domain);
async.series([
server.start,
database._clear,
@@ -70,7 +67,7 @@ describe('Internal API', function () {
describe('backup', function () {
it('succeeds', function (done) {
superagent.post(config.sysadminOrigin() + '/api/v1/backup')
superagent.post(`http://127.0.0.1:${constants.SYSADMIN_PORT}/api/v1/backup`)
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
+80 -84
View File
@@ -6,23 +6,19 @@
/* global after:false */
var async = require('async'),
config = require('../../config.js'),
constants = require('../../constants.js'),
database = require('../../database.js'),
expect = require('expect.js'),
server = require('../../server.js'),
superagent = require('superagent'),
tasks = require('../../tasks.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
var SERVER_URL = 'http://localhost:' + constants.PORT;
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null;
function setup(done) {
config._reset();
config.setFqdn('example-tasks-test.com');
config.setAdminFqdn('my.example-tasks-test.com');
async.series([
server.start.bind(null),
database._clear.bind(null),
@@ -57,105 +53,105 @@ describe('Tasks API', function () {
after(cleanup);
it('can get task', function (done) {
let taskId = null;
let task = tasks.startTask(tasks._TASK_IDENTITY, [ 'ping' ]);
task.on('error', done);
task.on('start', (tid) => { taskId = tid; });
tasks.add(tasks._TASK_IDENTITY, [ 'ping' ], function (error, taskId) {
if (error) return done(error);
task.on('finish', function () {
superagent.get(SERVER_URL + '/api/v1/tasks/' + taskId)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.percent).to.be(100);
expect(res.body.args).to.be(undefined);
expect(res.body.active).to.be(false); // finished
expect(res.body.result).to.be('ping');
expect(res.body.errorMessage).to.be(null);
done();
});
tasks.startTask(taskId, {}, function () {
superagent.get(SERVER_URL + '/api/v1/tasks/' + taskId)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.percent).to.be(100);
expect(res.body.args).to.be(undefined);
expect(res.body.active).to.be(false); // finished
expect(res.body.success).to.be(true);
expect(res.body.result).to.be('ping');
expect(res.body.error).to.be(null);
done();
});
});
});
});
it('can get logs', function (done) {
let taskId = null;
let task = tasks.startTask(tasks._TASK_CRASH, [ 'ping' ]);
task.on('error', done);
task.on('start', (tid) => { taskId = tid; });
tasks.add(tasks._TASK_CRASH, [ 'ping' ], function (error, taskId) {
if (error) return done(error);
task.on('finish', function () {
superagent.get(SERVER_URL + '/api/v1/tasks/' + taskId + '/logs')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
done();
});
tasks.startTask(taskId, {}, function () {
superagent.get(SERVER_URL + '/api/v1/tasks/' + taskId + '/logs')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
done();
});
});
});
});
it('cannot stop inactive task', function (done) {
let taskId = null;
let task = tasks.startTask(tasks._TASK_IDENTITY, [ 'ping' ]);
task.on('error', done);
task.on('start', (tid) => { taskId = tid; });
tasks.add(tasks._TASK_IDENTITY, [ 'ping' ], function (error, taskId) {
if (error) return done(error);
task.on('finish', function () {
superagent.post(SERVER_URL + '/api/v1/tasks/' + taskId + '/stop')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
done();
});
tasks.startTask(taskId, {}, function () {
superagent.post(SERVER_URL + '/api/v1/tasks/' + taskId + '/stop')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
done();
});
});
});
});
it('can stop task', function (done) {
let taskId = null;
let task = tasks.startTask(tasks._TASK_SLEEP, [ 10000 ]);
task.on('error', done);
task.on('start', (tid) => {
taskId = tid;
superagent.post(SERVER_URL + '/api/v1/tasks/' + taskId + '/stop')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
});
});
task.on('finish', () => {
superagent.get(SERVER_URL + '/api/v1/tasks/' + taskId)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.percent).to.be(100);
expect(res.body.active).to.be(false); // finished
expect(res.body.result).to.be(null);
expect(res.body.errorMessage).to.contain('signal SIGTERM');
done();
});
tasks.add(tasks._TASK_SLEEP, [ 10000 ], function (error, taskId) {
if (error) return done(error);
tasks.startTask(taskId, {}, function () {
superagent.get(SERVER_URL + '/api/v1/tasks/' + taskId)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.percent).to.be(100);
expect(res.body.active).to.be(false); // finished
expect(res.body.success).to.be(false);
expect(res.body.result).to.be(null);
expect(res.body.error.message).to.contain('signal SIGTERM');
done();
});
});
setTimeout(function () {
superagent.post(SERVER_URL + '/api/v1/tasks/' + taskId + '/stop')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
});
}, 100);
});
});
it('can list tasks', function (done) {
let taskId = null;
let task = tasks.startTask(tasks._TASK_IDENTITY, [ 'ping' ]);
task.on('error', done);
task.on('start', (tid) => { taskId = tid; });
tasks.add(tasks._TASK_IDENTITY, [ 'ping' ], function (error, taskId) {
if (error) return done(error);
task.on('finish', function () {
superagent.get(SERVER_URL + '/api/v1/tasks?type=' + tasks._TASK_IDENTITY)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.tasks.length >= 1).to.be(true);
expect(res.body.tasks[0].id).to.be(taskId);
expect(res.body.tasks[0].percent).to.be(100);
expect(res.body.tasks[0].args).to.be(undefined);
expect(res.body.tasks[0].active).to.be(false); // finished
expect(res.body.tasks[0].result).to.be('ping');
expect(res.body.tasks[0].errorMessage).to.be(null);
done();
});
tasks.startTask(taskId, {}, function () {
superagent.get(SERVER_URL + '/api/v1/tasks?type=' + tasks._TASK_IDENTITY)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.tasks.length >= 1).to.be(true);
expect(res.body.tasks[0].id).to.be(taskId);
expect(res.body.tasks[0].percent).to.be(100);
expect(res.body.tasks[0].args).to.be(undefined);
expect(res.body.tasks[0].active).to.be(false); // finished
expect(res.body.tasks[0].success).to.be(true); // finished
expect(res.body.tasks[0].result).to.be('ping');
expect(res.body.tasks[0].error).to.be(null);
done();
});
});
});
});
});
+2 -4
View File
@@ -7,7 +7,7 @@
var accesscontrol = require('../../accesscontrol.js'),
async = require('async'),
config = require('../../config.js'),
constants = require('../../constants.js'),
database = require('../../database.js'),
domains = require('../../domains.js'),
tokendb = require('../../tokendb.js'),
@@ -20,7 +20,7 @@ var accesscontrol = require('../../accesscontrol.js'),
server = require('../../server.js'),
users = require('../../users.js');
const SERVER_URL = 'http://localhost:' + config.get('port');
const SERVER_URL = 'http://localhost:' + constants.PORT;
const DOMAIN_0 = {
domain: 'example-user-test.com',
@@ -42,8 +42,6 @@ const USERNAME_4 = 'importedUser', EMAIL_4 = 'import@external.com';
var groupObject;
function setup(done) {
config._reset();
mailer._mailQueue = [];
async.series([
+2
View File
@@ -69,6 +69,8 @@ function update(req, res, next) {
if (req.user.id === req.params.userId && !req.body.admin) return next(new HttpError(409, 'Cannot remove admin flag on self'));
}
if ('active' in req.body && typeof req.body.active !== 'boolean') return next(new HttpError(400, 'active must be a boolean'));
users.update(req.params.userId, req.body, auditSource.fromRequest(req), function (error) {
if (error && error.reason === UsersError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === UsersError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
+4 -5
View File
@@ -4,11 +4,10 @@ exports = module.exports = {
sync: sync
};
var appdb = require('./appdb.js'),
apps = require('./apps.js'),
let apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
config = require('./config.js'),
constants = require('./constants.js'),
CronJob = require('cron').CronJob,
debug = require('debug')('box:scheduler'),
docker = require('./docker.js'),
@@ -119,7 +118,7 @@ function createCronJobs(app, schedulerConfig) {
const randomSecond = Math.floor(60*Math.random()); // don't start all crons to decrease memory pressure
var cronTime = (config.TEST ? '*/5 ' : `${randomSecond} `) + task.schedule; // time ticks faster in tests
var cronTime = (constants.TEST ? '*/5 ' : `${randomSecond} `) + task.schedule; // time ticks faster in tests
debug(`createCronJobs: ${app.fqdn} task ${taskName} scheduled at ${cronTime} with cmd ${task.command}`);
@@ -149,7 +148,7 @@ function runTask(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 || app.health !== appdb.HEALTH_HEALTHY) {
if (app.installationState !== apps.ISTATE_INSTALLED || app.runState !== apps.RSTATE_RUNNING || app.health !== apps.HEALTH_HEALTHY) {
debug(`runTask: skipped task ${taskName} because app ${app.fqdn} has run state ${app.installationState}`);
return callback();
}
+7 -2
View File
@@ -10,14 +10,19 @@ if (process.argv[2] === '--check') return console.log('OK');
require('supererror')({ splatchError: true });
var assert = require('assert'),
async = require('async'),
backups = require('../backups.js'),
database = require('../database.js'),
debug = require('debug')('box:backupupload');
debug = require('debug')('box:backupupload'),
settings = require('../settings.js');
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
database.initialize(callback);
async.series([
database.initialize,
settings.initCache
], callback);
}
// Main process starts here
+7 -4
View File
@@ -9,7 +9,7 @@ var accesscontrol = require('./accesscontrol.js'),
assert = require('assert'),
async = require('async'),
cloudron = require('./cloudron.js'),
config = require('./config.js'),
constants = require('./constants.js'),
database = require('./database.js'),
eventlog = require('./eventlog.js'),
express = require('express'),
@@ -19,6 +19,7 @@ var accesscontrol = require('./accesscontrol.js'),
passport = require('passport'),
path = require('path'),
routes = require('./routes/index.js'),
settings = require('./settings.js'),
ws = require('ws');
var gHttpServer = null;
@@ -134,6 +135,7 @@ function initializeExpressSync() {
router.get ('/api/v1/cloudron/logstream/:unit', cloudronScope, routes.cloudron.getLogStream);
router.get ('/api/v1/cloudron/eventlog', cloudronScope, routes.eventlog.list);
router.get ('/api/v1/cloudron/eventlog/:eventId', cloudronScope, routes.eventlog.get);
router.post('/api/v1/cloudron/sync_external_ldap', cloudronScope, routes.cloudron.syncExternalLdap);
// tasks
router.get ('/api/v1/tasks', settingsScope, routes.tasks.list);
@@ -367,10 +369,11 @@ function start(callback) {
async.series([
routes.accesscontrol.initialize, // hooks up authentication strategies into passport
database.initialize,
settings.initCache, // pre-load very often used settings
cloudron.initialize,
gHttpServer.listen.bind(gHttpServer, config.get('port'), '127.0.0.1'),
gSysadminHttpServer.listen.bind(gSysadminHttpServer, config.get('sysadminPort'), '127.0.0.1'),
eventlog.add.bind(null, eventlog.ACTION_START, { userId: null, username: 'boot' }, { version: config.version() })
gHttpServer.listen.bind(gHttpServer, constants.PORT, '127.0.0.1'),
gSysadminHttpServer.listen.bind(gSysadminHttpServer, constants.SYSADMIN_PORT, '127.0.0.1'),
eventlog.add.bind(null, eventlog.ACTION_START, { userId: null, username: 'boot' }, { version: constants.VERSION })
], callback);
}
+128 -10
View File
@@ -30,6 +30,9 @@ exports = module.exports = {
getPlatformConfig: getPlatformConfig,
setPlatformConfig: setPlatformConfig,
getExternalLdapConfig: getExternalLdapConfig,
setExternalLdapConfig: setExternalLdapConfig,
getLicenseKey: getLicenseKey,
setLicenseKey: setLicenseKey,
@@ -39,16 +42,31 @@ exports = module.exports = {
getCloudronToken: getCloudronToken,
setCloudronToken: setCloudronToken,
get: get,
getAll: getAll,
initCache: initCache,
// these values come from the cache
apiServerOrigin: apiServerOrigin,
webServerOrigin: webServerOrigin,
adminDomain: adminDomain,
setAdmin: setAdmin,
// these values are derived
adminOrigin: adminOrigin,
adminFqdn: adminFqdn,
mailFqdn: mailFqdn,
isDemo: isDemo,
// booleans. if you add an entry here, be sure to fix getAll
DYNAMIC_DNS_KEY: 'dynamic_dns',
UNSTABLE_APPS_KEY: 'unstable_apps',
DEMO_KEY: 'demo',
// json. if you add an entry here, be sure to fix getAll
BACKUP_CONFIG_KEY: 'backup_config',
PLATFORM_CONFIG_KEY: 'platform_config',
EXTERNAL_LDAP_KEY: 'external_ldap_config',
// strings
APP_AUTOUPDATE_PATTERN_KEY: 'app_autoupdate_pattern',
@@ -59,8 +77,17 @@ exports = module.exports = {
CLOUDRON_ID_KEY: 'cloudron_id',
CLOUDRON_TOKEN_KEY: 'cloudron_token',
API_SERVER_ORIGIN_KEY: 'api_server_origin',
WEB_SERVER_ORIGIN_KEY: 'web_server_origin',
ADMIN_DOMAIN_KEY: 'admin_domain',
ADMIN_FQDN_KEY: 'admin_fqdn',
PROVIDER_KEY: 'provider',
// blobs
CLOUDRON_AVATAR_KEY: 'cloudron_avatar', // not stored in db but can be used for locked flag
// testing
_setApiServerOrigin: setApiServerOrigin
};
var addons = require('./addons.js'),
@@ -71,6 +98,9 @@ var addons = require('./addons.js'),
cron = require('./cron.js'),
CronJob = require('cron').CronJob,
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:settings'),
externalldap = require('./externalldap.js'),
ExternalLdapError = externalldap.ExternalLdapError,
moment = require('moment-timezone'),
paths = require('./paths.js'),
safe = require('safetydance'),
@@ -78,7 +108,7 @@ var addons = require('./addons.js'),
util = require('util'),
_ = require('underscore');
var gDefaults = (function () {
let gDefaults = (function () {
var result = { };
result[exports.APP_AUTOUPDATE_PATTERN_KEY] = '00 30 1,3,5,23 * * *';
result[exports.BOX_AUTOUPDATE_PATTERN_KEY] = '00 00 1,3,5,23 * * *';
@@ -98,10 +128,18 @@ var gDefaults = (function () {
intervalSecs: 24 * 60 * 60 // ~1 day
};
result[exports.PLATFORM_CONFIG_KEY] = {};
result[exports.EXTERNAL_LDAP_KEY] = {};
result[exports.ADMIN_DOMAIN_KEY] = '';
result[exports.ADMIN_FQDN_KEY] = '';
result[exports.API_SERVER_ORIGIN_KEY] = 'https://api.cloudron.io';
result[exports.WEB_SERVER_ORIGIN_KEY] = 'https://cloudron.io';
result[exports.DEMO_KEY] = false;
return result;
})();
let gCache = {};
function SettingsError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
@@ -383,6 +421,37 @@ function setPlatformConfig(platformConfig, callback) {
});
}
function getExternalLdapConfig(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.EXTERNAL_LDAP_KEY, function (error, value) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.EXTERNAL_LDAP_KEY]);
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
callback(null, JSON.parse(value));
});
}
function setExternalLdapConfig(externalLdapConfig, callback) {
assert.strictEqual(typeof externalLdapConfig, 'object');
assert.strictEqual(typeof callback, 'function');
externalldap.testConfig(externalLdapConfig, function (error) {
if (error && error.reason === ExternalLdapError.BAD_FIELD) return callback(new SettingsError(SettingsError.BAD_FIELD, error.message));
if (error && error.reason === ExternalLdapError.EXTERNAL_ERROR) return callback(new SettingsError(SettingsError.EXTERNAL_ERROR, error.message));
if (error && error.reason === ExternalLdapError.INVALID_CREDENTIALS) return callback(new SettingsError(SettingsError.BAD_FIELD, 'invalid bind credentials'));
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
settingsdb.set(exports.EXTERNAL_LDAP_KEY, JSON.stringify(externalLdapConfig), function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
notifyChange(exports.EXTERNAL_LDAP_KEY, externalLdapConfig);
callback(null);
});
});
}
function getLicenseKey(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -467,9 +536,10 @@ function getAll(callback) {
// convert booleans
result[exports.DYNAMIC_DNS_KEY] = !!result[exports.DYNAMIC_DNS_KEY];
result[exports.UNSTABLE_APPS_KEY] = !!result[exports.UNSTABLE_APPS_KEY];
result[exports.DEMO_KEY] = !!result[exports.DEMO_KEY];
// convert JSON objects
[exports.BACKUP_CONFIG_KEY, exports.PLATFORM_CONFIG_KEY ].forEach(function (key) {
[exports.BACKUP_CONFIG_KEY, exports.PLATFORM_CONFIG_KEY, exports.EXTERNAL_LDAP_KEY ].forEach(function (key) {
result[key] = typeof result[key] === 'object' ? result[key] : safe.JSON.parse(result[key]);
});
@@ -477,14 +547,62 @@ function getAll(callback) {
});
}
function get(name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
function initCache(callback) {
debug('initCache: pre-load settings');
settingsdb.get(name, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.PLATFORM_CONFIG_KEY]);
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
getAll(function (error, allSettings) {
if (error) return callback(error);
callback(null, result);
gCache = {
apiServerOrigin: allSettings[exports.API_SERVER_ORIGIN_KEY],
webServerOrigin: allSettings[exports.WEB_SERVER_ORIGIN_KEY],
adminDomain: allSettings[exports.ADMIN_DOMAIN_KEY],
adminFqdn: allSettings[exports.ADMIN_FQDN_KEY],
isDemo: allSettings[exports.DEMO_KEY]
};
callback();
});
}
// this is together so we can do this in a transaction later
function setAdmin(adminDomain, adminFqdn, callback) {
assert.strictEqual(typeof adminDomain, 'string');
assert.strictEqual(typeof adminFqdn, 'string');
assert.strictEqual(typeof callback, 'function');
settingsdb.set(exports.ADMIN_DOMAIN_KEY, adminDomain, function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
settingsdb.set(exports.ADMIN_FQDN_KEY, adminFqdn, function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
gCache.adminDomain = adminDomain;
gCache.adminFqdn = adminFqdn;
callback(null);
});
});
}
function setApiServerOrigin(origin, callback) {
assert.strictEqual(typeof origin, 'string');
assert.strictEqual(typeof callback, 'function');
settingsdb.set(exports.API_SERVER_ORIGIN_KEY, origin, function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
gCache.apiServerOrigin = origin;
notifyChange(exports.API_SERVER_ORIGIN_KEY, origin);
callback(null);
});
}
function apiServerOrigin() { return gCache.apiServerOrigin; }
function webServerOrigin() { return gCache.webServerOrigin; }
function adminDomain() { return gCache.adminDomain; }
function adminFqdn() { return gCache.adminFqdn; }
function isDemo() { return gCache.isDemo; }
function mailFqdn() { return adminFqdn(); }
function adminOrigin() { return 'https://' + adminFqdn(); }
+1 -1
View File
@@ -16,7 +16,7 @@ function startSftp(existingInfra, callback) {
const tag = infra.images.sftp.tag;
const memoryLimit = 256;
if (existingInfra.version === infra.version && infra.images.graphite.tag === existingInfra.images.graphite.tag) return callback();
if (existingInfra.version === infra.version && infra.images.sftp.tag === existingInfra.images.sftp.tag) return callback();
const cmd = `docker run --restart=always -d --name="sftp" \
--hostname sftp \
+5 -3
View File
@@ -8,15 +8,17 @@ exports = module.exports = {
};
let assert = require('assert'),
config = require('./config.js'),
constants = require('./constants.js'),
shell = require('./shell.js'),
once = require('once'),
path = require('path'),
paths = require('./paths.js'),
sysinfo = require('./sysinfo.js'),
util = require('util');
// the logic here is also used in the cloudron-support tool
var AUTHORIZED_KEYS_FILEPATH = config.TEST ? path.join(config.baseDir(), 'authorized_keys') : ((config.provider() === 'ec2' || config.provider() === 'lightsail' || config.provider() === 'ami') ? '/home/ubuntu/.ssh/authorized_keys' : '/root/.ssh/authorized_keys'),
AUTHORIZED_KEYS_USER = config.TEST ? process.getuid() : ((config.provider() === 'ec2' || config.provider() === 'lightsail' || config.provider() === 'ami') ? 'ubuntu' : 'root'),
var AUTHORIZED_KEYS_FILEPATH = constants.TEST ? path.join(paths.baseDir(), 'authorized_keys') : ((sysinfo.provider() === 'ec2' || sysinfo.provider() === 'lightsail' || sysinfo.provider() === 'ami') ? '/home/ubuntu/.ssh/authorized_keys' : '/root/.ssh/authorized_keys'),
AUTHORIZED_KEYS_USER = constants.TEST ? process.getuid() : ((sysinfo.provider() === 'ec2' || sysinfo.provider() === 'lightsail' || sysinfo.provider() === 'ami') ? 'ubuntu' : 'root'),
AUTHORIZED_KEYS_CMD = path.join(__dirname, 'scripts/remotesupport.sh');
function SupportError(reason, errorOrMessage) {
+26 -3
View File
@@ -3,14 +3,19 @@
exports = module.exports = {
SysInfoError: SysInfoError,
getPublicIp: getPublicIp
getPublicIp: getPublicIp,
hasIPv6: hasIPv6,
provider: provider
};
var assert = require('assert'),
config = require('./config.js'),
ec2 = require('./sysinfo/ec2.js'),
fs = require('fs'),
generic = require('./sysinfo/generic.js'),
paths = require('./paths.js'),
scaleway = require('./sysinfo/scaleway.js'),
safe = require('safetydance'),
util = require('util');
function SysInfoError(reason, errorOrMessage) {
@@ -35,10 +40,22 @@ util.inherits(SysInfoError, Error);
SysInfoError.INTERNAL_ERROR = 'Internal Error';
SysInfoError.EXTERNAL_ERROR = 'External Error';
let gProvider = null;
function provider() {
if (gProvider) return gProvider;
gProvider = safe.fs.readFileSync(paths.PROVIDER_FILE, 'utf8');
if (!gProvider) return gProvider = 'generic';
return gProvider;
}
function getApi(callback) {
assert.strictEqual(typeof callback, 'function');
switch (config.provider()) {
switch (provider()) {
case 'ec2': return callback(null, ec2);
case 'lightsail': return callback(null, ec2);
case 'ami': return callback(null, ec2);
@@ -56,3 +73,9 @@ function getPublicIp(callback) {
api.getPublicIp(callback);
});
}
function hasIPv6() {
const IPV6_PROC_FILE = '/proc/net/if_inet6';
// on contabo, /proc/net/if_inet6 is an empty file. so just exists is not enough
return fs.existsSync(IPV6_PROC_FILE) && fs.readFileSync(IPV6_PROC_FILE, 'utf8').trim().length !== 0;
}
+1 -2
View File
@@ -6,7 +6,6 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
config = require('../config.js'),
superagent = require('superagent'),
SysInfoError = require('../sysinfo.js').SysInfoError;
@@ -16,7 +15,7 @@ function getPublicIp(callback) {
if (process.env.BOX_ENV === 'test') return callback(null, '127.0.0.1');
async.retry({ times: 10, interval: 5000 }, function (callback) {
superagent.get(config.apiServerOrigin() + '/api/v1/helper/public_ip').timeout(30 * 1000).end(function (error, result) {
superagent.get('https://api.cloudron.io/api/v1/helper/public_ip').timeout(30 * 1000).end(function (error, result) {
if (error || result.statusCode !== 200) {
console.error('Error getting IP', error);
return callback(new SysInfoError(SysInfoError.EXTERNAL_ERROR, 'Unable to detect IP. API server unreachable'));
+6 -3
View File
@@ -13,7 +13,7 @@ let assert = require('assert'),
DatabaseError = require('./databaseerror'),
safe = require('safetydance');
const TASKS_FIELDS = [ 'id', 'type', 'argsJson', 'percent', 'message', 'errorMessage', 'creationTime', 'resultJson', 'ts' ];
const TASKS_FIELDS = [ 'id', 'type', 'argsJson', 'percent', 'message', 'errorJson', 'creationTime', 'resultJson', 'ts' ];
function postProcess(task) {
assert.strictEqual(typeof task, 'object');
@@ -26,6 +26,9 @@ function postProcess(task) {
task.result = JSON.parse(task.resultJson);
delete task.resultJson;
task.error = safe.JSON.parse(task.errorJson);
delete task.errorJson;
}
function add(task, callback) {
@@ -50,8 +53,8 @@ function update(id, data, callback) {
let args = [ ];
let fields = [ ];
for (let k in data) {
if (k === 'result') {
fields.push('resultJson = ?');
if (k === 'result' || k === 'error') {
fields.push(`${k}Json = ?`);
args.push(JSON.stringify(data[k]));
} else {
fields.push(k + ' = ?');

Some files were not shown because too many files have changed in this diff Show More