Compare commits

...

358 Commits

Author SHA1 Message Date
Girish Ramakrishnan bc09e4204b use debug instead of console.error 2015-10-20 19:03:34 -07:00
Girish Ramakrishnan 1a2948df85 VolumesFrom is part of HostConfig 2015-10-20 17:34:47 -07:00
Girish Ramakrishnan 16df15cf55 containerId does not mean it is running 2015-10-20 16:56:57 -07:00
Girish Ramakrishnan 0566bad6d9 bump infra version 2015-10-20 15:07:35 -07:00
Girish Ramakrishnan edc90ccc00 bump test image 2015-10-20 14:40:27 -07:00
Girish Ramakrishnan 3688602d16 test the scheduler 2015-10-20 14:30:50 -07:00
Girish Ramakrishnan 0deadc5cf2 autodetect image id 2015-10-20 13:07:25 -07:00
Girish Ramakrishnan 10ac435d53 addons is mandatory 2015-10-20 12:57:00 -07:00
Girish Ramakrishnan 16f025181f ensure boolean 2015-10-20 12:49:02 -07:00
Girish Ramakrishnan 3808f60e69 appState can be null 2015-10-20 12:32:50 -07:00
Girish Ramakrishnan a00615bd4e manifest always has addons 2015-10-20 12:27:23 -07:00
Girish Ramakrishnan 14bc2c7232 rename isSubcontainer -> isAppContainer 2015-10-20 10:55:06 -07:00
Girish Ramakrishnan 76d286703c ignore portBindings and exportPorts for subcontainers 2015-10-20 10:42:35 -07:00
Girish Ramakrishnan c80a5b59ab do not dump containerOptions 2015-10-20 10:27:53 -07:00
Girish Ramakrishnan db6882e9f5 do not kill containers on restart 2015-10-20 10:22:42 -07:00
Girish Ramakrishnan 3fd9d9622b schedulerConfig cannot be null 2015-10-20 09:44:46 -07:00
Girish Ramakrishnan 5ae4c891de scheduler: sync more often to catch bugs sooner 2015-10-20 09:36:55 -07:00
Girish Ramakrishnan fb2e7cb009 scheduler: crash fixes 2015-10-20 09:36:30 -07:00
Johannes Zellner 8124f0ac7f Remove cloudron name from the setup wizard 2015-10-20 13:36:25 +02:00
Johannes Zellner 446f571bec The activate route does not take a cloudron name anymore 2015-10-20 13:12:37 +02:00
Johannes Zellner 142ae76542 Finally remove the cloudron name from the api wrapper and index page 2015-10-20 12:59:57 +02:00
Johannes Zellner ed1873f47e Remove cloudron name related UI in the settings view 2015-10-20 12:57:51 +02:00
Johannes Zellner 0ee04e6ef3 Remove cloudron name usage in error.html 2015-10-20 12:55:30 +02:00
Johannes Zellner 1e4475b275 Remove cloudron name usage in naked domain page 2015-10-20 12:46:45 +02:00
Johannes Zellner 9dd9743943 Do not use cloudron name in appstatus 2015-10-20 12:41:52 +02:00
Johannes Zellner 5fbcebf80b Stop using the cloudron name in the oauth views 2015-10-20 12:31:16 +02:00
Girish Ramakrishnan 852b016389 scheduler: do not save cronjob object in state
the cronjob object has lots of js stuff and stringify fails
2015-10-20 01:31:11 -07:00
Girish Ramakrishnan 73f28d7653 put back request 2015-10-20 00:20:21 -07:00
Girish Ramakrishnan 1f28678c27 scheduler: make it work 2015-10-20 00:05:19 -07:00
Girish Ramakrishnan daba68265c stop all containers of an app 2015-10-20 00:05:19 -07:00
Girish Ramakrishnan 6d04481c27 fix debug tag 2015-10-19 23:38:55 -07:00
Girish Ramakrishnan ed5d6f73bb scheduler: fix require 2015-10-19 22:42:13 -07:00
Girish Ramakrishnan d0360e9e68 scheduler: load/save state 2015-10-19 22:41:42 -07:00
Girish Ramakrishnan 32ddda404c explicitly specify all to 0 (this is the default) 2015-10-19 22:09:38 -07:00
Girish Ramakrishnan 41de667e3d do not set container name (we use labels instead) 2015-10-19 22:09:38 -07:00
Girish Ramakrishnan 8530e70af6 delete all containers of an app 2015-10-19 22:09:34 -07:00
Girish Ramakrishnan 7a840ad15f scheduler: make stopJobs async 2015-10-19 21:36:55 -07:00
Girish Ramakrishnan 682c2721d2 scheduler: kill existing tasks if they are still running 2015-10-19 21:36:23 -07:00
Girish Ramakrishnan fb56795cbd merge start options into hostconfig 2015-10-19 21:35:02 -07:00
Girish Ramakrishnan 15aa4ecc5d Add docker.createSubcontainer 2015-10-19 21:33:53 -07:00
Girish Ramakrishnan 351d7d22fb rename tasks to tasksConfig 2015-10-19 16:29:28 -07:00
Girish Ramakrishnan 79999887a9 job -> cronJob 2015-10-19 16:27:03 -07:00
Girish Ramakrishnan 25d74ed649 createContainer takes optional command 2015-10-19 16:22:35 -07:00
Girish Ramakrishnan 9346666b3e add labels to container 2015-10-19 16:01:04 -07:00
Girish Ramakrishnan 13453552b5 createContainer only takes app object 2015-10-19 16:00:40 -07:00
Girish Ramakrishnan ef38074b55 add asserts 2015-10-19 15:51:02 -07:00
Girish Ramakrishnan e5e8eea7ac make it work without app object 2015-10-19 15:45:43 -07:00
Girish Ramakrishnan 9be2efc4f2 downloadImage only requires manifest now 2015-10-19 15:37:57 -07:00
Girish Ramakrishnan 990b7a2d20 implement scheduler
- scan for apps every 10 minutes and schedules tasks
- uses docker.exec
    - there is no way to control exec container. docker developers
      feel exec is for debugging purposes primarily
- future version will be based on docker run instead

part of #519
2015-10-19 14:53:34 -07:00
Girish Ramakrishnan 8d6dd62ef4 refactor container code into docker.js 2015-10-19 14:44:01 -07:00
Girish Ramakrishnan 69d09e8133 use docker.connection 2015-10-19 14:09:20 -07:00
Girish Ramakrishnan 6671b211e0 export a connection property from docker.js 2015-10-19 11:24:21 -07:00
Girish Ramakrishnan 307e815e97 remove unused require 2015-10-19 11:18:50 -07:00
Girish Ramakrishnan d8e2bd6ff5 Refactor docker.js to not have mac stuff 2015-10-19 11:14:11 -07:00
Girish Ramakrishnan e74c2f686b remove unused require 2015-10-19 11:05:31 -07:00
Girish Ramakrishnan c7d5115a56 Remove vbox.js
... and all related mac code. It's totally untested at this point and
most likely doesn't work
2015-10-19 10:54:36 -07:00
Girish Ramakrishnan 774ba11a92 Move HostConfig to createContainer
Newer docker has obsoleted HostConfig in start container
2015-10-19 10:38:46 -07:00
Girish Ramakrishnan 322edbdc20 getByAppIdAndType 2015-10-19 08:58:07 -07:00
Johannes Zellner c1ba551e07 Cleanup some of the html form elements 2015-10-19 10:31:19 +02:00
Johannes Zellner 9917412329 Indicate during app installation and configuration if the app is a single user app 2015-10-19 10:29:51 +02:00
Girish Ramakrishnan 2f4adb4d5f keep addon listing alphabetical 2015-10-18 20:06:26 -07:00
Girish Ramakrishnan b61b864094 make callback noop 2015-10-17 13:57:19 -07:00
Johannes Zellner fa193276c9 Require exactly one user in accessRestriction for singleUser app installations 2015-10-16 20:01:45 +02:00
Johannes Zellner 0ca09c384a Hide client secret field for simple auth 2015-10-16 19:41:50 +02:00
Johannes Zellner a6a39cc4e6 Adapt clients.getAllWithDetailsByUserId() to new client types 2015-10-16 19:36:12 +02:00
Johannes Zellner c9f84f6259 Show user selection for singleUser apps 2015-10-16 18:06:49 +02:00
Johannes Zellner 07063ca4f0 Adjust the webadmin to the accessRestriction changes 2015-10-16 16:14:23 +02:00
Johannes Zellner b5cfdcf875 Fixup the unit tests for accessRestriction format change 2015-10-16 16:06:13 +02:00
Johannes Zellner 373db25077 Make accessRestriction a JSON format to prepare for group access control 2015-10-16 15:32:19 +02:00
Johannes Zellner f8c2ebe61a Taks accessRestriction and oauthProxy into account for an update through the cli 2015-10-16 14:50:00 +02:00
Johannes Zellner ae23fade1e Show oauthProxy and accessRestriction values at app installation and configuration 2015-10-16 14:50:00 +02:00
Johannes Zellner 5386c05c0d Give developer tokens the correct scopes 2015-10-16 14:50:00 +02:00
Johannes Zellner aed94c8aaf roleDeveloper is no more 2015-10-16 14:50:00 +02:00
Johannes Zellner 37185fc4d5 Only allow simple auth clients through simple auth 2015-10-16 14:49:51 +02:00
Johannes Zellner cc64c6c9f7 Test using simple auth credentials in oauth 2015-10-16 11:48:12 +02:00
Johannes Zellner 0c0782ccd7 Fixup oauth to not allow simple auth clients 2015-10-16 11:27:42 +02:00
Johannes Zellner 5bc9f9e995 use clientdb types in authorization endpoint 2015-10-16 11:22:16 +02:00
Johannes Zellner 22402d1741 Remove legacy test auth client type 2015-10-16 10:05:58 +02:00
Johannes Zellner 8f203b07a1 Fix indentation 2015-10-16 09:19:05 +02:00
Girish Ramakrishnan 9c157246b7 add type field to clients table 2015-10-15 17:35:47 -07:00
Girish Ramakrishnan d0dfe1ef7f remove unused variable 2015-10-15 17:35:47 -07:00
Girish Ramakrishnan a9ccc7e2aa remove updating clients
clients are immutable
2015-10-15 16:08:17 -07:00
Girish Ramakrishnan 63edbae1be minor rename 2015-10-15 15:51:51 -07:00
Girish Ramakrishnan 8afe537497 fix typo 2015-10-15 15:32:14 -07:00
Girish Ramakrishnan f33844d8f1 fix debug tag 2015-10-15 15:19:28 -07:00
Girish Ramakrishnan c750d00355 ignore any tmp cleanup errors 2015-10-15 14:47:43 -07:00
Girish Ramakrishnan bb9b39e3c0 callback can be null 2015-10-15 14:25:38 -07:00
Girish Ramakrishnan 057b89ab8e Check error code of image removal 2015-10-15 14:06:05 -07:00
Girish Ramakrishnan 23fc4bec36 callback can be null 2015-10-15 12:06:38 -07:00
Girish Ramakrishnan 6b82fb9ddb Remove old addon images on infra update
Fixes #329
2015-10-15 12:01:31 -07:00
Girish Ramakrishnan a3ca5a36e8 update test image 2015-10-15 11:11:54 -07:00
Girish Ramakrishnan f57c91847d addons do not write to /var/log anymore 2015-10-15 11:00:51 -07:00
Johannes Zellner eda4dc83a3 Do not fail in container.sh when trying to remove non-existing directories 2015-10-15 18:06:57 +02:00
Johannes Zellner 5a0bf8071e Handle the various appId types we have by now 2015-10-15 17:57:07 +02:00
Johannes Zellner 09dfc6a34b Get the oauth2 debug()s in shape 2015-10-15 16:55:48 +02:00
Johannes Zellner 3b8ebe9a59 Fixup the oauth tests with accessRestriction support 2015-10-15 16:50:05 +02:00
Johannes Zellner 2ba1092809 Adhere to accessRestriction for oauth authorization endpoint 2015-10-15 16:49:13 +02:00
Johannes Zellner 7c97ab5408 Revert "Since we got fully rid of the decision dialog, no need to serialze the client anymore"
This is now again required, due to the accesRestriction check

This reverts commit 2c9ff1ee3b.
2015-10-15 16:33:05 +02:00
Johannes Zellner ac1991f8d1 Fix typo in oauth test 2015-10-15 15:37:56 +02:00
Johannes Zellner 2a573f6ac5 Fixup the simpleauth tests 2015-10-15 15:19:01 +02:00
Johannes Zellner 9833d0cce6 Adhere to accessRestriction in simple auth 2015-10-15 15:18:40 +02:00
Johannes Zellner fbc3ed0213 Add apps.hasAccessTo() 2015-10-15 15:06:34 +02:00
Johannes Zellner c916a76e6b Prepare simpleauth test for accessRestriction 2015-10-15 13:29:44 +02:00
Johannes Zellner ae1bfaf0c8 roleUser is gone as well 2015-10-15 12:50:48 +02:00
Johannes Zellner 0aedff4fec roleAdmin is gone 2015-10-15 12:37:42 +02:00
Johannes Zellner 73d88a3491 Rewrite accessRestriction validator 2015-10-15 12:37:42 +02:00
Girish Ramakrishnan 5d389337cd make /var/log readonly
Expect apps to redirect logs of stdout/stderr

Part of #503
2015-10-15 00:46:50 -07:00
Girish Ramakrishnan a977597217 cleanup tmpdir in janitor 2015-10-14 23:21:03 -07:00
Girish Ramakrishnan b3b4106b99 Add janitor tests 2015-10-14 22:50:07 -07:00
Girish Ramakrishnan 7f29eed326 fold janitor into main box code cron job
the volume cleaner will now also come into janitor
2015-10-14 22:39:34 -07:00
Girish Ramakrishnan ec895a4f31 do not use -f to logrotate
Normally, logrotate is run as a daily cron job. It will not modify a log
multiple times in one day unless the criterion for that log is based on
the log's size and logrotate is being run multiple times each day, or
unless the -f or --force option is used.
2015-10-14 15:10:53 -07:00
Girish Ramakrishnan 3fc0a96bb0 Add docker volumes janitor
This cleans up tmp and logrotates /var/log every 12 hours.

Note that this janitor is separate from the box janitor because they
run as different users.

Fixes #503
2015-10-14 14:18:36 -07:00
Girish Ramakrishnan c154f342c2 show restore button if we have a lastBackupId
This is the only way to roll back even if you have a functioning app.
Use cases include:
1. You updated and something doesn't work
2. The app is in 'starting...' state (so it's installed) but no data yet
2015-10-14 11:36:12 -07:00
Johannes Zellner 8f1666dcca Consolidate the oauth comments 2015-10-14 16:31:55 +02:00
Johannes Zellner 9aa4750f55 Since we got fully rid of the decision dialog, no need to serialze the client anymore 2015-10-14 16:22:50 +02:00
Johannes Zellner c52d985d45 Properly skip decision dialog 2015-10-14 16:16:37 +02:00
Johannes Zellner 376d8d9a38 Cleanup the client serialization 2015-10-14 16:15:51 +02:00
Johannes Zellner 08de0a4e79 Add token exchange tests 2015-10-14 16:15:32 +02:00
Johannes Zellner 11d327edcf Remove unused session error route 2015-10-14 15:51:55 +02:00
Johannes Zellner d2f7b83ea7 Add oauth callback tests 2015-10-14 15:50:00 +02:00
Johannes Zellner 72ca1b39e8 Add oauth session logout test 2015-10-14 15:38:40 +02:00
Johannes Zellner 69bd234abc Test for unkown client_id 2015-10-14 15:30:10 +02:00
Johannes Zellner 94e6978abf Add test for grant type requests 2015-10-14 15:08:04 +02:00
Johannes Zellner b5272cbf4d roleAdmin is not part of scopes anymore 2015-10-14 14:59:54 +02:00
Johannes Zellner edb213089c Add oauth2 test when user is already logged in with his session 2015-10-14 14:46:25 +02:00
Johannes Zellner b772cf3e5a Add tester tag 2015-10-14 14:46:03 +02:00
Johannes Zellner e86d043794 The oauth callback does not need a header and footer 2015-10-14 14:36:41 +02:00
Johannes Zellner 4727187071 Also test loginForm submit with email 2015-10-14 14:31:10 +02:00
Johannes Zellner d8b8f5424c add loginForm submit tests 2015-10-14 14:30:53 +02:00
Johannes Zellner 8425c99a4e Also test oauth clients with oauth proxy 2015-10-14 13:38:37 +02:00
Johannes Zellner c023dbbc1c Do not handle addon-simpleauth in oauth 2015-10-14 13:35:33 +02:00
Johannes Zellner af516f42b4 Add oauth login form tests 2015-10-14 13:34:20 +02:00
Johannes Zellner dbd8e6a08d Add more oauth tests for the authorization endpoint 2015-10-14 12:03:04 +02:00
Johannes Zellner c24bec9bc6 Remove unused contentType middleware 2015-10-14 11:48:57 +02:00
Johannes Zellner 9854598648 Fix typo to repair oauth and simple auth login
Second time breakage, time for a test ;-)
2015-10-13 21:55:02 +02:00
Johannes Zellner 1e7e2e5e97 Remove decision dialog related route 2015-10-13 20:39:08 +02:00
Johannes Zellner 081e496878 Remove unused oauth decision dialog 2015-10-13 20:32:27 +02:00
Johannes Zellner aaff7f463a Cleanup the authorization endpoint 2015-10-13 18:23:32 +02:00
Girish Ramakrishnan 55f937bf51 SIMPLE_AUTH_URL -> SIMPLE_AUTH_ORIGIN 2015-10-13 08:40:41 -07:00
Johannes Zellner d5d1d061bb We also allow non admins to use the webadmin 2015-10-13 15:13:36 +02:00
Johannes Zellner bc6f602891 Remove unused angular filter for accessRestrictionLabel 2015-10-13 15:11:30 +02:00
Johannes Zellner ca461057e7 Also update the test image id 2015-10-13 14:24:53 +02:00
Johannes Zellner b1c5c2468a Fix test to support docker api up to 1.19 and v1.20 2015-10-13 14:24:41 +02:00
Johannes Zellner 562ce3192f Print error when apptask.pullImage() failed 2015-10-13 13:25:43 +02:00
Johannes Zellner 3787dd98b4 Do not crash if a boxVersionsUrl is not set
This prevents test failures when the cron job runs
2015-10-13 13:22:23 +02:00
Johannes Zellner 6c667e4325 Remove console.log 2015-10-13 13:06:50 +02:00
Johannes Zellner 0eec693a85 Update TEST_IMAGE_TAG 2015-10-13 12:30:02 +02:00
Johannes Zellner c3bf672c2a Ensure we deal with booleans 2015-10-13 12:29:40 +02:00
Johannes Zellner c3a3b6412f Support oauthProxy in webadmin 2015-10-13 11:49:50 +02:00
Johannes Zellner 44291b842a Fix apps-test.js 2015-10-13 10:41:57 +02:00
Johannes Zellner 36cf502b56 Addons take longer to startup 2015-10-13 10:41:57 +02:00
Johannes Zellner 2df77cf280 Fix settings-test.js 2015-10-13 10:41:57 +02:00
Johannes Zellner a453e49c27 Fix backups-test.js 2015-10-13 10:41:57 +02:00
Johannes Zellner e34c34de46 Fixup the apptask-test.js 2015-10-13 10:41:57 +02:00
Johannes Zellner 8dc5bf96e3 Fix apps-test.js 2015-10-13 10:41:57 +02:00
Johannes Zellner d2c3e1d1ae Fix database tests 2015-10-13 10:41:57 +02:00
Johannes Zellner 4eab101b78 use app.oauthProxy instead of app.accessRestriction 2015-10-13 10:41:57 +02:00
Johannes Zellner e460d6d15b Add apps.oauthProxy 2015-10-13 10:41:57 +02:00
Girish Ramakrishnan 3012f68a56 pullImage: handle stream error 2015-10-12 21:56:34 -07:00
Girish Ramakrishnan 1909050be2 remove redundant log 2015-10-12 21:54:25 -07:00
Girish Ramakrishnan d4c62c7295 check for 200 instead of 201 2015-10-12 21:54:18 -07:00
Girish Ramakrishnan 4eb3d1b918 login must return 200 2015-10-12 20:21:27 -07:00
Girish Ramakrishnan fb6bf50e48 signal redis to backup using SAVE 2015-10-12 13:30:58 -07:00
Johannes Zellner d8213f99b1 Ensure we only set the visibility in the progress bar on app restore to not break the layout 2015-10-12 20:32:43 +02:00
Johannes Zellner 7d7b759930 Add navbar with avatar and name to oauth views 2015-10-12 19:56:04 +02:00
Johannes Zellner 6f2bc555e0 Make application name and cloudron name more apparent in oauth login 2015-10-12 17:26:02 +02:00
Johannes Zellner a8c43ddf4a Show app icon instead of cloudron avatar in oauth login 2015-10-12 16:50:49 +02:00
Johannes Zellner 3eabc27877 Make app icon url public to be used in oauth login screen 2015-10-12 16:49:55 +02:00
Johannes Zellner ad379bd766 Support the new oauth client id prefix 2015-10-12 15:18:51 +02:00
Johannes Zellner c1047535d4 Update to new manifestformat 2015-10-12 13:22:56 +02:00
Girish Ramakrishnan 10142cc00b make a note of appid format 2015-10-11 14:19:38 -07:00
Girish Ramakrishnan 5e1487d12a appId format has changed in clientdb 2015-10-11 14:16:38 -07:00
Girish Ramakrishnan 39e0c13701 apptest: remove mail addon 2015-10-11 13:53:50 -07:00
Girish Ramakrishnan c80d984ee6 start the mail addon 2015-10-11 13:48:23 -07:00
Girish Ramakrishnan 3e474767d1 print the values otherwise it gets very confusing 2015-10-11 13:45:02 -07:00
Girish Ramakrishnan e2b954439c ensure redis container is stopped before removing it
this is required for the configure/update case where the redis container
might be holding some data in memory.

sending redis SIGTERM will make it commit to disk.
2015-10-11 12:08:35 -07:00
Girish Ramakrishnan 950d1eb5c3 remove associated volumes
note that this does not remove host mounts
2015-10-11 11:37:23 -07:00
Girish Ramakrishnan f48a2520c3 remove RSTATE_ERROR
if startContainer failed, it will still returning success because
it running the db update result
2015-10-11 11:18:30 -07:00
Girish Ramakrishnan f366d41034 regenerate shrinkwrap with --no-optional for ldapjs 2015-10-11 10:39:41 -07:00
Girish Ramakrishnan b3c40e1ba7 test image is now 5.0.0 2015-10-11 10:03:42 -07:00
Girish Ramakrishnan b686d6e011 use latest docker images 2015-10-11 10:03:42 -07:00
Johannes Zellner 5663198bfb Do not log with morgan during test runs for simple auth server 2015-10-11 17:52:22 +02:00
Johannes Zellner bad50fd78b Merge branch 'simpleauth' 2015-10-11 17:40:10 +02:00
Johannes Zellner 9bd43e3f74 Update to new manifestformat version 1.9.0 2015-10-11 17:19:39 +02:00
Johannes Zellner f0fefab8ad Prefix morgan logs with source 2015-10-11 17:19:39 +02:00
Johannes Zellner 449231c791 Do not show http request logs during tests 2015-10-11 17:19:39 +02:00
Johannes Zellner bd161ec677 Remove serving up webadmin/ in test case 2015-10-11 17:19:39 +02:00
Johannes Zellner 8040d4ac2d Add simpleauth logging middleware 2015-10-11 17:19:39 +02:00
Johannes Zellner 06d7820566 read ldap port from config.js 2015-10-11 17:19:39 +02:00
Johannes Zellner 4818a3feee Specify addon env vars for simple auth 2015-10-11 17:19:39 +02:00
Johannes Zellner 9fcb2c0733 Fix the check install to keep up with the base image version 2015-10-11 17:19:39 +02:00
Johannes Zellner 6906b4159a Revert "Attach accessTokens to req for further use"
This reverts commit 895812df1e9226415640b74a001c1f8c1affab01.
2015-10-11 17:19:39 +02:00
Johannes Zellner 763b9309f6 Fixup the simple auth unit tests 2015-10-11 17:19:39 +02:00
Johannes Zellner 2bb4d1c22b Remove the simpleauth route handler 2015-10-11 17:19:39 +02:00
Johannes Zellner 23303363ee Move simple auth to separate express server 2015-10-11 17:19:39 +02:00
Johannes Zellner 79c17abad2 Add simpleAuthPort to config.js 2015-10-11 17:19:39 +02:00
Johannes Zellner 3234e0e3f0 Fixup the simple auth logout route and add tests 2015-10-11 17:19:39 +02:00
Johannes Zellner 982cd1e1f3 Attach accessTokens to req for further use
This helps with extracting the token, which can come
from various places like headers, body or query
2015-10-11 17:19:38 +02:00
Johannes Zellner df39fc86a4 add simple auth login unit tests 2015-10-11 17:19:38 +02:00
Johannes Zellner 870e0c4144 Fixup the simple login routes for unknown clients 2015-10-11 17:19:38 +02:00
Johannes Zellner 57704b706e Handle 404 in case subdomain does not exist on delete attempt 2015-10-11 17:19:38 +02:00
Johannes Zellner 223e0dfd1f Add SIMPLE_AUTH_ORIGIN 2015-10-11 17:19:38 +02:00
Johannes Zellner 51c438b0b6 Return correct error codes 2015-10-11 17:19:38 +02:00
Girish Ramakrishnan 93d210a754 Bump the graphite image 2015-10-10 09:57:07 -07:00
Girish Ramakrishnan 265ee15ac7 fix oldConfig madness
There is a crash when:
* App is configured. So, oldConfig now has {loc, access, portb }
* Cloudron is restored. The restore code path accesses the oldConfig.manifest.addons.

oldConfig is basically a messaging passing thing. It's not really a
db field. With that spirit, we simply pass an empty message in setup_infra.sh
2015-10-09 11:59:25 -07:00
Girish Ramakrishnan d0da47e0b3 fix comment 2015-10-09 11:48:59 -07:00
Girish Ramakrishnan 0e8553d1a7 code path applies to upgraded cloudrons as well 2015-10-09 11:08:54 -07:00
Girish Ramakrishnan 9229dd2fd5 Add oldConfigJson in schema file 2015-10-09 11:08:16 -07:00
Girish Ramakrishnan c2a8744240 fix typo 2015-10-09 10:04:50 -07:00
Girish Ramakrishnan bc7e07f6a6 mail: not required to expose port 25 2015-10-09 09:56:37 -07:00
Girish Ramakrishnan bfd6f8965e print mail server ip 2015-10-09 09:50:50 -07:00
Girish Ramakrishnan eb1e4a1aea mail now runs on port 2500 2015-10-09 09:29:17 -07:00
Girish Ramakrishnan dc3e8a9cb5 mail now runs on port 2500 2015-10-09 09:13:28 -07:00
Johannes Zellner 494bcc1711 prefix oauth client id and app ids 2015-10-09 11:45:53 +02:00
Johannes Zellner 7e071d9f23 add simpleauth addon hooks 2015-10-09 11:44:32 +02:00
Johannes Zellner 6f821222db Add simple auth routes 2015-10-09 11:37:46 +02:00
Johannes Zellner 6e464dbc81 Add simple auth route handlers 2015-10-09 11:37:39 +02:00
Johannes Zellner be8ef370c6 Add simple auth logic for login/logout 2015-10-09 11:37:17 +02:00
Johannes Zellner 39a05665b0 Update node modules to support v4.1.1
The sqlite3 versions we had used so far did not work with
new node versions
2015-10-09 10:15:10 +02:00
Girish Ramakrishnan 737e22116a Reword upgrade warning 2015-10-08 16:31:44 -07:00
Girish Ramakrishnan 43e1e4829f new test image 3.0.0 2015-10-08 16:07:14 -07:00
Girish Ramakrishnan c95778178f make rootfs readonly based on targetBoxVersion 2015-10-08 11:48:33 -07:00
Girish Ramakrishnan 04870313b7 Launch apps with readonly rootfs
We explicitly mark /tmp, /run and /var/log as writable volumes.
Docker creates such volumes in it's own volumes directory. Note
that these volumes are separate from host binds (/app/data).

When removing the container the docker created volumes are
removed (but not host binds).

Fixes #196
2015-10-08 11:33:17 -07:00
Girish Ramakrishnan 6ca040149c run addons as readonly 2015-10-08 11:07:28 -07:00
Girish Ramakrishnan e487b9d46b update mail image 2015-10-08 11:06:29 -07:00
Girish Ramakrishnan 1375e16ad2 mongodb: readonly rootfs 2015-10-08 10:24:15 -07:00
Girish Ramakrishnan 312f1f0085 mysql: readonly rootfs 2015-10-08 09:43:05 -07:00
Girish Ramakrishnan 721900fc47 postgresql: readonly rootfs 2015-10-08 09:20:25 -07:00
Girish Ramakrishnan 2d815a92a3 redis: use readonly rootfs 2015-10-08 09:00:43 -07:00
Girish Ramakrishnan 1c192b7c11 pass options param in setup call 2015-10-08 02:08:27 -07:00
Girish Ramakrishnan 4a887336bc Do not send app down mails for dev mode apps
Fixes #501
2015-10-07 18:46:48 -07:00
Girish Ramakrishnan 8f6521f942 pass addon options to all functions 2015-10-07 16:10:08 -07:00
Girish Ramakrishnan fbdfaa4dc7 rename setup and teardown functions of oauth addon 2015-10-07 15:55:57 -07:00
Girish Ramakrishnan bf4290db3e remove token addon, its a relic of the past 2015-10-07 15:44:55 -07:00
Johannes Zellner 94ad633128 Also unset the returnTo after login 2015-10-01 16:26:17 +02:00
Johannes Zellner c552917991 Reset the target url after oauth login
This is required for the cloudron button to work for users
which are not logged in
2015-10-01 16:16:29 +02:00
Johannes Zellner a7ee8c853e Keep checkInstall in sync 2015-09-30 16:12:51 +02:00
Girish Ramakrishnan 29e4879451 fix test image version 2015-09-29 20:22:38 -07:00
Girish Ramakrishnan 8b92344808 redirect stderr 2015-09-29 19:23:39 -07:00
Girish Ramakrishnan 0877cec2e6 Fix EE leak warning 2015-09-29 14:40:23 -07:00
Girish Ramakrishnan b1ca577be7 use newer test image that dies immediately on stop/term 2015-09-29 14:33:07 -07:00
Girish Ramakrishnan 9b484f5ac9 new version of mysql prints error with -p 2015-09-29 14:13:58 -07:00
Girish Ramakrishnan b6a9fd81da refactor our test docker image details 2015-09-29 13:59:17 -07:00
Girish Ramakrishnan f19113f88e rename test iamge under cloudron/ 2015-09-29 12:52:54 -07:00
Girish Ramakrishnan 3837bee51f retry pulling image
fixes #497
2015-09-29 12:47:03 -07:00
Girish Ramakrishnan 89c3296632 debug the status code as well 2015-09-28 23:18:50 -07:00
Girish Ramakrishnan db55f0696e stringify object when appending to string 2015-09-28 23:10:09 -07:00
Girish Ramakrishnan 03d4ae9058 new base image 0.4.0 2015-09-28 19:33:58 -07:00
Girish Ramakrishnan f8b41b703c Use fqdn to generate domain name of txt records 2015-09-28 17:20:59 -07:00
Girish Ramakrishnan 2a989e455c Ensure TXT records are added as dotted domains
Fixes #498
2015-09-28 16:35:58 -07:00
Girish Ramakrishnan cd24decca0 Send dns status requests in series
And abort status checking after the first one fails. Otherwise, this
bombards the appstore unnecessarily. And checks for status of other
things unnecessarily.
2015-09-28 16:23:39 -07:00
Girish Ramakrishnan f39842a001 ldap: allow non-anonymous searches
Add LDAP_BIND_DN and LDAP_BIND_PASSWORD that allow
apps to bind before a search. There appear to be two kinds of
ldap flows:

1. App simply binds using cn=<username>,$LDAP_USERS_BASE_DN. This
   works swimmingly today.

2. App searches the username under a "bind_dn" using some admin
   credentials. It takes the result and uses the first dn in the
   result as the user dn. It then binds as step 1.

This commit tries to help out the case 2) apps. These apps really
insist on having some credentials for searching.
2015-09-25 21:28:47 -07:00
Girish Ramakrishnan 2a39526a4c Remove old app ids from updatechecker state
Fixes #472
2015-09-22 22:46:14 -07:00
Girish Ramakrishnan ded5d4c98b debug message when notification is skipped 2015-09-22 22:41:42 -07:00
Girish Ramakrishnan a0ca59c3f2 Fix typo 2015-09-22 20:22:17 -07:00
Girish Ramakrishnan 53cfc49807 Save version instead of boolean so we get notified when version changes
part of #472
2015-09-22 16:11:15 -07:00
Girish Ramakrishnan 942eb579e4 save/restore notification state of updatechecker
part of #472
2015-09-22 16:11:04 -07:00
Girish Ramakrishnan 5819cfe412 Fix progress message 2015-09-22 13:02:09 -07:00
Johannes Zellner 5cb62ca412 Remove start/stop buttons in webadmin
Fixes #495
2015-09-22 22:00:42 +02:00
Johannes Zellner df10c245de app.js is no more 2015-09-22 22:00:42 +02:00
Girish Ramakrishnan 4a804dc52b Do a complete backup for updates
The backup cron job ensures backups every 4 hours which simply does
a 'box' backup listing. If we do only a 'box' backup during update,
this means that this cron job skips doing a backup and thus the apps
are not backed up.

This results in the janitor on the CaaS side complaining that the
app backups are too old.

Since we don't stop apps anymore during updates, it makes sense
to simply backup everything for updates as well. This is probably
what the user wants anyway.
2015-09-22 12:51:58 -07:00
Girish Ramakrishnan ed2f25a998 better debugs 2015-09-21 16:02:58 -07:00
Girish Ramakrishnan 7510c9fe29 Fix typo 2015-09-21 15:57:06 -07:00
Girish Ramakrishnan 78a1d53728 copy old backup as failed/errored apps
This ensures that
a) we don't get emails from janitor about bad app backups
b) that the backups are persisted over the s3 lifecycle

Fixes #493
2015-09-21 15:03:10 -07:00
Girish Ramakrishnan e9b078cd58 add backups.copyLastBackup 2015-09-21 14:14:43 -07:00
Girish Ramakrishnan dd8b928684 aws: add copyObject 2015-09-21 14:02:00 -07:00
Girish Ramakrishnan 185b574bdc Add custom apparmor profile for cloudron apps
Docker generates an apparmor profile on the fly under /etc/apparmor.d/docker.
This profile gets overwritten on every docker daemon start.

This profile allows processes to ptrace themselves. This is required by
circus (python process manager) for reasons unknown to me. It floods the logs
with
    audit[7623]: <audit-1400> apparmor="DENIED" operation="ptrace" profile="docker-default" pid=7623 comm="python3.4" requested_mask="trace" denied_mask="trace" peer="docker-default"

This is easily tested using:
    docker run -it cloudron/base:0.3.3 /bin/bash
        a) now do ps
        b) journalctl should show error log as above

    docker run --security-opt=apparmor:docker-cloudron-app -it cloudron/base:0.3.3 /bin/bash
        a) now do ps
        b) no error!

Note that despite this, the process may not have ability to ptrace since it does not
have CAP_PTRACE. Also, security-opt is the profile name (inside the apparmor config file)
and not the filename.

References:
    https://groups.google.com/forum/#!topic/docker-user/xvxpaceTCyw
    https://github.com/docker/docker/issues/7276
    https://bugs.launchpad.net/ubuntu/+source/docker.io/+bug/1320869

This is an infra update because we need to recreate containers to get the right profile.

Fixes #492
2015-09-21 11:01:44 -07:00
Girish Ramakrishnan a89726a8c6 Add custom debug.formatArgs to remove timestamp prefix in logs
Fixes #490

See also:
https://github.com/visionmedia/debug/issues/216
2015-09-21 09:05:14 -07:00
Girish Ramakrishnan c80aca27e6 remove unnecessary supererror call 2015-09-21 09:04:16 -07:00
Girish Ramakrishnan 029acab333 use correct timezone in updater
fixes #491
2015-09-18 14:46:44 -07:00
Girish Ramakrishnan 4f9f10e130 timezone detection is based on browser location/ip and not cloudron region intentionally 2015-09-18 13:40:22 -07:00
Girish Ramakrishnan 9ba11d2e14 print body on failure 2015-09-18 12:03:48 -07:00
Girish Ramakrishnan 23a5a1f79f timezone is already determined automatically using activation 2015-09-18 12:02:36 -07:00
Girish Ramakrishnan e8dc617d40 print tz for debugging 2015-09-18 10:51:52 -07:00
Girish Ramakrishnan d56794e846 clear backup progress when initiating backup
this ensures that tools can do:
1. backup
2. wait_for_backup

without the synchronous clear, we might get the progress state of
an earlier backup.
2015-09-17 21:17:59 -07:00
Girish Ramakrishnan 2663ec7da0 cloudron.backup does not wait for backup to complete 2015-09-17 16:35:59 -07:00
Girish Ramakrishnan eec4ae98cd add comment for purpose on internal server 2015-09-17 16:27:46 -07:00
Girish Ramakrishnan c31a0f4e09 Store dates as iso strings in database
ideally, the database schema should be TIMESTAMP
2015-09-17 13:51:55 -07:00
Girish Ramakrishnan 739db23514 Use the default timezone in settings
Fixes #485
2015-09-16 16:36:08 -07:00
Girish Ramakrishnan 8598fb444b store timezone in config.js (part of provision data) 2015-09-16 15:54:56 -07:00
Girish Ramakrishnan 0b630ff504 Remove debug that is flooding the logs 2015-09-16 10:50:15 -07:00
Girish Ramakrishnan 84169dea3d Do not set process.env.NODE_TLS_REJECT_UNAUTHORIZED
Doing so will affect all https requests which is dangerous.

We have these options to solve this:
1. Use superagent.ca(). Appstore already provides wildcard certs
   for dev, staging signed with appstore_ca. But we then need to
   send across the appstore_ca cert across in the provision call.
   This is a bit of work.

2. Convert superagent into https.request calls and use the
   rejectUnauthorized option.

3. Simply use http. This is what is done in this commit.

Fixes #488
2015-09-16 10:36:03 -07:00
Girish Ramakrishnan d83b5de47a reserve the ldap and oauthproxy port 2015-09-16 10:12:59 -07:00
Girish Ramakrishnan 2719c4240f Get oauth proxy port from the configs 2015-09-16 10:06:34 -07:00
Johannes Zellner d749756b53 Do not show the update action button in non mobile view 2015-09-16 09:36:46 +02:00
Johannes Zellner 0401c61c15 Add tooltip text for the app action icons 2015-09-16 09:36:22 +02:00
Johannes Zellner 34f45da2de Show indicator when app update is available
Fixes #489
2015-09-16 09:28:43 +02:00
Girish Ramakrishnan baecbf783c journalctl seems to barf on this debug 2015-09-15 20:50:22 -07:00
Girish Ramakrishnan 2f141cd6e0 Make the times absurdly high but that is how long in takes 2015-09-15 18:56:25 -07:00
Girish Ramakrishnan 1296299d02 error is undefined 2015-09-15 18:27:09 -07:00
Girish Ramakrishnan 998ac74d32 oldConfig.location can be null
If we had an update, location is not part of oldConfig. if we now do
an infra update, location is undefined.
2015-09-15 18:08:29 -07:00
Girish Ramakrishnan b4a34e6432 Explicity debug the fields
for some reason, journalctl barfs on this line
2015-09-15 14:55:20 -07:00
Girish Ramakrishnan e70c9d55db apptask: retry for external error as well 2015-09-14 21:45:27 -07:00
Girish Ramakrishnan 268aee6265 Return busy code for 420 response 2015-09-14 21:44:44 -07:00
Girish Ramakrishnan 1ba7b0e0fb context is raw text 2015-09-14 17:25:27 -07:00
Girish Ramakrishnan 72788fdb11 add note on how to test the oom 2015-09-14 17:20:30 -07:00
Girish Ramakrishnan 435afec13c Print OOM context 2015-09-14 17:18:11 -07:00
Girish Ramakrishnan 2cb1877669 Do not reconnect for now 2015-09-14 17:10:49 -07:00
Girish Ramakrishnan edd672cba7 fix typo 2015-09-14 17:07:44 -07:00
Girish Ramakrishnan 991f37fe05 Provide app information if possible 2015-09-14 17:06:04 -07:00
Girish Ramakrishnan c147d8004b Add appdb.getByContainerId 2015-09-14 17:01:04 -07:00
Girish Ramakrishnan cdcc4dfda8 Get notification on app oom
currently, oom events arrive a little late :
https://github.com/docker/docker/issues/16074

fixes #489
2015-09-14 16:51:32 -07:00
Girish Ramakrishnan 2eaba686fb apphealthmonitor.js is not executable 2015-09-14 16:51:32 -07:00
Girish Ramakrishnan 236032b4a6 Remove supererror setup in oauthproxy and apphealthmonitor 2015-09-14 16:49:10 -07:00
Girish Ramakrishnan 5fcba59b3e set memory limits for addons
mysql, postgresql, mongodb - 100m each
mail, graphite, redis (each instance) - 75m

For reference, in yellowtent:
mongo - 5m
postgresql - 33m
mysql - 3.5m
mail: 26m
graphite - 26m
redis - 32m
2015-09-14 13:47:45 -07:00
Girish Ramakrishnan 6efd8fddeb fix require paths 2015-09-14 13:00:03 -07:00
Girish Ramakrishnan 8aff2b9e74 remove oauthproxy systemd configs 2015-09-14 12:02:38 -07:00
Girish Ramakrishnan fbae432b98 merge oauthproxy server into box server 2015-09-14 11:58:28 -07:00
Girish Ramakrishnan 9cad7773ff refactor code to prepare for merge into box server 2015-09-14 11:28:49 -07:00
Girish Ramakrishnan 4adf122486 oauthproxy: refactor for readability 2015-09-14 11:22:33 -07:00
Girish Ramakrishnan ea47c26d3f apphealthmonitor is not a executable anymore 2015-09-14 11:09:58 -07:00
Girish Ramakrishnan f57aae9545 Fix typo in assert 2015-09-14 11:09:41 -07:00
Girish Ramakrishnan cdeb830706 Add apphealthmonitor.stop 2015-09-14 11:02:06 -07:00
Girish Ramakrishnan 0c9618f19a Add ldap.stop 2015-09-14 11:01:35 -07:00
Girish Ramakrishnan 1cd9d07d8c Merge apphealthtask into box server
We used to run this as a separate process but no amount of node/v8 tweaking
makes them run as standalone with 50M RSS.

Three solutions were considered for the memory issue:
1. Use systemd timer. apphealthtask needs to run quiet frequently (10 sec)
   for the ui to get the app health update immediately after install.

2. Merge into box server (this commit)

3. Increase memory to 80M. This seems to make apphealthtask run as-is.
2015-09-14 10:52:11 -07:00
Girish Ramakrishnan f028649582 Rename app.js to box.js 2015-09-14 10:43:47 -07:00
Johannes Zellner d57236959a choose aws subdomain backend for test purpose 2015-09-13 22:02:04 +02:00
Johannes Zellner ebe975f463 Also send data with the domain deletion 2015-09-13 22:02:04 +02:00
Johannes Zellner a94267fc98 Use caas.js for subdomain business 2015-09-13 22:02:04 +02:00
Johannes Zellner f186ea7cc3 Add initial caas.js 2015-09-13 22:02:04 +02:00
Girish Ramakrishnan 29e05b1caa make janitor a systemd timer
one process lesser
2015-09-11 18:43:51 -07:00
Girish Ramakrishnan 6945a712df limit node memory usage
node needs to be told how much space it can usage, otherwise it keeps
allocating and we cannot keep it under 50M. keeping old space to 30M,
lets the memory hover around 40M

there are many options to v8 but I haven't explored them all:
--expose_gc - allows scripts to call gc()
--max_old_space_size=30 --max_semi_space_size=2048 (old/new space)
    node first allocates new objects in new space. if these objects are in use
    around for some time, it moves them to old space. the idea here is that it
    runs gc aggressively on new space since new objects die more than old ones.

    the new space is split into two halves of equal size called semi spaces.

--gc_interval=100 --optimize_for_size --max_executable_size=5 --gc_global --stack_size=1024

http://erikcorry.blogspot.com/2012/11/memory-management-flags-in-v8.html
http://jayconrod.com/posts/55/a-tour-of-v8-garbage-collection
https://code.google.com/p/chromium/issues/detail?id=280984
http://stackoverflow.com/questions/30252905/nodejs-decrease-v8-garbage-collector-memory-usage
http://www.appfruits.com/2014/08/running-node-js-on-arduino-yun/

note: this is not part of shebang because linux shebang does not support args! so we cannot
pass node args as part of shebang.
2015-09-10 21:24:36 -07:00
Girish Ramakrishnan 03048d7d2f set memorylimit for crashnotifier as well 2015-09-10 14:19:44 -07:00
Girish Ramakrishnan 28b768b146 Fix app autoupdater logic
The main issue was that app.portBindings is never null but { }
2015-09-10 11:39:29 -07:00
Girish Ramakrishnan c1e4dceb01 ssh is now on port 919 2015-09-10 10:08:40 -07:00
Johannes Zellner 954d14cd66 Warn the user when he performs an upgrade instead of update
Fixes #481
2015-09-10 14:33:00 +02:00
Johannes Zellner 2f5e9e2e26 We do have global rest error handler which take care of re-login 2015-09-10 14:16:59 +02:00
Johannes Zellner b3c058593f Force reload page if version has changed
Fixes #480
2015-09-10 13:58:27 +02:00
Johannes Zellner 3e47e11992 Ensure the stylesheets are in correct order
Fixes #484
2015-09-10 13:32:33 +02:00
Girish Ramakrishnan 8c7dfdcef2 Wait upto 3 seconds for the app to quit
Otherwise systemd will kill us and we get crash emails.

Fixes #483
2015-09-09 16:57:43 -07:00
Girish Ramakrishnan c88591489d make apps test work 2015-09-09 15:51:56 -07:00
Girish Ramakrishnan 719404b6cf lint 2015-09-09 15:03:43 -07:00
Girish Ramakrishnan f2c27489c8 test: make unregister subdomain test work 2015-09-09 14:36:09 -07:00
Girish Ramakrishnan d6a0c93f2f test: make register subdomain work 2015-09-09 14:32:05 -07:00
Girish Ramakrishnan c64d5fd2e3 error is already Error 2015-09-09 14:26:53 -07:00
Girish Ramakrishnan 5b62aeb73a make aws endpoint configurable for tests 2015-09-09 12:03:47 -07:00
Girish Ramakrishnan 7e83f2dd4a intercept delete calls to test image 2015-09-09 11:32:09 -07:00
Girish Ramakrishnan ed48f84355 give taskmanager couple of seconds to kill all processes 2015-09-09 10:39:38 -07:00
Girish Ramakrishnan f3d15cd4a5 fix initialization of apps-test 2015-09-09 10:22:17 -07:00
Girish Ramakrishnan 8c270269db remove dead code 2015-09-09 09:28:06 -07:00
Johannes Zellner bea605310a Use memoryLimit from manifest for graphs if specified 2015-09-09 17:11:54 +02:00
Johannes Zellner 8184894563 Remove upgrade view altogether 2015-09-09 16:47:13 +02:00
Johannes Zellner 47a87cc298 Remove upgrade link in the menu 2015-09-09 16:46:28 +02:00
Johannes Zellner 553a6347e6 Actually hand the backupKey over in an update 2015-09-09 12:37:09 +02:00
Girish Ramakrishnan a35ebd57f9 call iteratorDone when finished 2015-09-09 00:43:42 -07:00
Girish Ramakrishnan 97174d7af0 make cloudron-test pass 2015-09-08 22:13:50 -07:00
Girish Ramakrishnan 659268c04a provide default backupPrefix for tests 2015-09-08 21:16:50 -07:00
Girish Ramakrishnan 67d06c5efa better debug messages 2015-09-08 21:11:46 -07:00
Girish Ramakrishnan 6e6d8c0bc5 awscredentials is now POST 2015-09-08 21:02:21 -07:00
Girish Ramakrishnan 658af3edcf disable failing subdomains test
This needs aws mock
2015-09-08 20:38:52 -07:00
Girish Ramakrishnan 9753d9dc7e removeUser takes a userId and not username 2015-09-08 16:38:02 -07:00
Girish Ramakrishnan 4e331cfb35 retry registering and unregistering subdomain 2015-09-08 12:51:25 -07:00
Girish Ramakrishnan a1fa94707b Remove ununsed error codes 2015-09-08 11:28:29 -07:00
Girish Ramakrishnan 88f1107ed6 Remove unused AWSError 2015-09-08 11:26:35 -07:00
Girish Ramakrishnan e97b9fcc60 Do not start apptask for apps that are installed and running 2015-09-08 10:24:39 -07:00
Girish Ramakrishnan 71fe643099 Check if we have reached concurrency limit before locking 2015-09-08 10:20:34 -07:00
Johannes Zellner 74874a459d Remove ... for labels while showing the progress bar 2015-09-08 15:49:10 +02:00
Johannes Zellner 7c5fc17500 Cleanup linter issues in updatechecker.js 2015-09-08 10:03:37 +02:00
104 changed files with 5354 additions and 3211 deletions
-47
View File
@@ -1,47 +0,0 @@
#!/usr/bin/env node
'use strict';
require('supererror')({ splatchError: true });
var server = require('./src/server.js'),
ldap = require('./src/ldap.js'),
config = require('./src/config.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 token: ', config.token());
console.log(' Appstore API server origin: ', config.apiServerOrigin());
console.log(' Appstore Web server origin: ', config.webServerOrigin());
console.log();
console.log('==========================================');
console.log();
server.start(function (err) {
if (err) {
console.error('Error starting server', err);
process.exit(1);
}
console.log('Server listening on port ' + config.get('port'));
ldap.start(function (error) {
if (error) {
console.error('Error LDAP starting server', err);
process.exit(1);
}
console.log('LDAP server listen on port ' + config.get('ldapPort'));
});
});
var NOOP_CALLBACK = function () { };
process.on('SIGINT', function () { server.stop(NOOP_CALLBACK); });
process.on('SIGTERM', function () { server.stop(NOOP_CALLBACK); });
Executable
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env node
'use strict';
require('supererror')({ splatchError: true });
// remove timestamp from debug() based output
require('debug').formatArgs = function formatArgs() {
arguments[0] = this.namespace + ' ' + arguments[0];
return arguments;
};
var appHealthMonitor = require('./src/apphealthmonitor.js'),
async = require('async'),
config = require('./src/config.js'),
ldap = require('./src/ldap.js'),
simpleauth = require('./src/simpleauth.js'),
oauthproxy = require('./src/oauthproxy.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 token: ', config.token());
console.log(' Appstore API server origin: ', config.apiServerOrigin());
console.log(' Appstore Web server origin: ', config.webServerOrigin());
console.log();
console.log('==========================================');
console.log();
async.series([
server.start,
ldap.start,
simpleauth.start,
appHealthMonitor.start,
oauthproxy.start
], function (error) {
if (error) {
console.error('Error starting server', error);
process.exit(1);
}
});
var NOOP_CALLBACK = function () { };
process.on('SIGINT', function () {
server.stop(NOOP_CALLBACK);
ldap.stop(NOOP_CALLBACK);
simpleauth.stop(NOOP_CALLBACK);
oauthproxy.stop(NOOP_CALLBACK);
setTimeout(process.exit.bind(process), 3000);
});
process.on('SIGTERM', function () {
server.stop(NOOP_CALLBACK);
ldap.stop(NOOP_CALLBACK);
simpleauth.stop(NOOP_CALLBACK);
oauthproxy.stop(NOOP_CALLBACK);
setTimeout(process.exit.bind(process), 3000);
});
-70
View File
@@ -1,70 +0,0 @@
#!/usr/bin/env node
'use strict';
require('supererror')({ splatchError: true });
var assert = require('assert'),
debug = require('debug')('box:janitor'),
async = require('async'),
tokendb = require('./src/tokendb.js'),
authcodedb = require('./src/authcodedb.js'),
database = require('./src/database.js');
var TOKEN_CLEANUP_INTERVAL = 30000;
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
async.series([
database.initialize
], callback);
}
function cleanupExpiredTokens(callback) {
assert.strictEqual(typeof callback, 'function');
tokendb.delExpired(function (error, result) {
if (error) return callback(error);
debug('Cleaned up %s expired tokens.', result);
callback(null);
});
}
function cleanupExpiredAuthCodes(callback) {
assert.strictEqual(typeof callback, 'function');
authcodedb.delExpired(function (error, result) {
if (error) return callback(error);
debug('Cleaned up %s expired authcodes.', result);
callback(null);
});
}
function run() {
cleanupExpiredTokens(function (error) {
if (error) console.error(error);
cleanupExpiredAuthCodes(function (error) {
if (error) console.error(error);
setTimeout(run, TOKEN_CLEANUP_INTERVAL);
});
});
}
if (require.main === module) {
initialize(function (error) {
if (error) {
console.error('janitor task exiting with error', error);
process.exit(1);
}
run();
});
}
@@ -0,0 +1,17 @@
dbm = dbm || require('db-migrate');
var type = dbm.dataType;
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN oauthProxy BOOLEAN DEFAULT 0', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN oauthProxy', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,17 @@
dbm = dbm || require('db-migrate');
var type = dbm.dataType;
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'DELETE FROM clients'),
db.runSql.bind(db, 'ALTER TABLE clients ADD COLUMN type VARCHAR(16) NOT NULL'),
], callback);
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE clients DROP COLUMN type', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,17 @@
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps CHANGE accessRestriction accessRestrictionJson VARCHAR(2048)', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps CHANGE accessRestrictionJson accessRestriction VARCHAR(2048)', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
+7 -2
View File
@@ -29,8 +29,9 @@ CREATE TABLE IF NOT EXISTS tokens(
PRIMARY KEY(accessToken));
CREATE TABLE IF NOT EXISTS clients(
id VARCHAR(128) NOT NULL UNIQUE,
id VARCHAR(128) NOT NULL UNIQUE, // prefixed with cid- to identify token easily in auth routes
appId VARCHAR(128) NOT NULL,
type VARCHAR(16) NOT NULL,
clientSecret VARCHAR(512) NOT NULL,
redirectURI VARCHAR(512) NOT NULL,
scope VARCHAR(512) NOT NULL,
@@ -48,11 +49,15 @@ CREATE TABLE IF NOT EXISTS apps(
httpPort INTEGER, // this is the nginx proxy port and not manifest.httpPort
location VARCHAR(128) NOT NULL UNIQUE,
dnsRecordId VARCHAR(512),
accessRestriction VARCHAR(512),
accessRestrictionJson VARCHAR(2048),
oauthProxy BOOLEAN DEFAULT 0,
createdAt TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
lastBackupId VARCHAR(128),
lastBackupConfigJson VARCHAR(2048), // used for appstore and non-appstore installs. it's here so it's easy to do REST validation
oldConfigJson VARCHAR(2048), // used to pass old config for apptask
PRIMARY KEY(id));
CREATE TABLE IF NOT EXISTS appPortBindings(
+800 -821
View File
File diff suppressed because it is too large Load Diff
-185
View File
@@ -1,185 +0,0 @@
#!/usr/bin/env node
'use strict';
require('supererror')({ splatchError: true });
var express = require('express'),
url = require('url'),
uuid = require('node-uuid'),
async = require('async'),
superagent = require('superagent'),
assert = require('assert'),
debug = require('debug')('box:proxy'),
proxy = require('proxy-middleware'),
session = require('cookie-session'),
database = require('./src/database.js'),
appdb = require('./src/appdb.js'),
clientdb = require('./src/clientdb.js'),
config = require('./src/config.js'),
http = require('http');
// Allow self signed certs!
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
var gSessions = {};
var gProxyMiddlewareCache = {};
var gApp = express();
var gHttpServer = http.createServer(gApp);
var CALLBACK_URI = '/callback';
var PORT = 4000;
function startServer(callback) {
assert.strictEqual(typeof callback, 'function');
gHttpServer.on('error', console.error);
gApp.use(session({
keys: ['blue', 'cheese', 'is', 'something']
}));
// ensure we have a in memory store for the session to cache client information
gApp.use(function (req, res, next) {
assert.strictEqual(typeof req.session, 'object');
if (!req.session.id || !gSessions[req.session.id]) {
req.session.id = uuid.v4();
gSessions[req.session.id] = {};
}
// attach the session data to the requeset
req.sessionData = gSessions[req.session.id];
next();
});
gApp.use(function verifySession(req, res, next) {
assert.strictEqual(typeof req.sessionData, 'object');
if (!req.sessionData.accessToken) {
req.authenticated = false;
return next();
}
superagent.get(config.adminOrigin() + '/api/v1/profile').query({ access_token: req.sessionData.accessToken}).end(function (error, result) {
if (error) {
console.error(error);
req.authenticated = false;
} else if (result.statusCode !== 200) {
req.sessionData.accessToken = null;
req.authenticated = false;
} else {
req.authenticated = true;
}
next();
});
});
gApp.use(function (req, res, next) {
// proceed if we are authenticated
if (req.authenticated) return next();
if (req.path === CALLBACK_URI && req.sessionData.returnTo) {
// exchange auth code for an access token
var query = {
response_type: 'token',
client_id: req.sessionData.clientId
};
var data = {
grant_type: 'authorization_code',
code: req.query.code,
redirect_uri: req.sessionData.returnTo,
client_id: req.sessionData.clientId,
client_secret: req.sessionData.clientSecret
};
superagent.post(config.adminOrigin() + '/api/v1/oauth/token').query(query).send(data).end(function (error, result) {
if (error) {
console.error(error);
return res.send(500, 'Unable to contact the oauth server.');
}
if (result.statusCode !== 200) {
console.error('Failed to exchange auth code for a token.', result.statusCode, result.body);
return res.send(500, 'Failed to exchange auth code for a token.');
}
req.sessionData.accessToken = result.body.access_token;
debug('user verified.');
// now redirect to the actual initially requested URL
res.redirect(req.sessionData.returnTo);
});
} else {
var port = parseInt(req.headers['x-cloudron-proxy-port'], 10);
if (!Number.isFinite(port)) {
console.error('Failed to parse nginx proxy header to get app port.');
return res.send(500, 'Routing error. No forwarded port.');
}
debug('begin verifying user for app on port %s.', port);
appdb.getByHttpPort(port, function (error, result) {
if (error) {
console.error('Unknown app.', error);
return res.send(500, 'Unknown app.');
}
clientdb.getByAppId('proxy-' + result.id, function (error, result) {
if (error) {
console.error('Unkonwn OAuth client.', error);
return res.send(500, 'Unknown OAuth client.');
}
req.sessionData.port = port;
req.sessionData.returnTo = result.redirectURI + req.path;
req.sessionData.clientId = result.id;
req.sessionData.clientSecret = result.clientSecret;
var callbackUrl = result.redirectURI + CALLBACK_URI;
var scope = 'profile,roleUser';
var oauthLogin = config.adminOrigin() + '/api/v1/oauth/dialog/authorize?response_type=code&client_id=' + result.id + '&redirect_uri=' + callbackUrl + '&scope=' + scope;
debug('begin OAuth flow for client %s.', result.name);
// begin the OAuth flow
res.redirect(oauthLogin);
});
});
}
});
gApp.use(function (req, res, next) {
var port = req.sessionData.port;
debug('proxy request for port %s with path %s.', port, req.path);
var proxyMiddleware = gProxyMiddlewareCache[port];
if (!proxyMiddleware) {
console.log('Adding proxy middleware for port %d', port);
proxyMiddleware = proxy(url.parse('http://127.0.0.1:' + port));
gProxyMiddlewareCache[port] = proxyMiddleware;
}
proxyMiddleware(req, res, next);
});
gHttpServer.listen(PORT, callback);
}
async.series([
database.initialize,
startServer
], function (error) {
if (error) {
console.error('Failed to start proxy server.', error);
process.exit(1);
}
console.log('Proxy server listening...');
});
+3 -4
View File
@@ -12,14 +12,11 @@
"engines": [
"node >= 0.12.0"
],
"bin": {
"cloudron": "./app.js"
},
"dependencies": {
"async": "^1.2.1",
"aws-sdk": "^2.1.46",
"body-parser": "^1.13.1",
"cloudron-manifestformat": "^1.7.0",
"cloudron-manifestformat": "^2.0.0",
"connect-ensure-login": "^0.1.1",
"connect-lastmile": "0.0.13",
"connect-timeout": "^1.5.0",
@@ -82,10 +79,12 @@
"gulp-uglify": "^1.1.0",
"hock": "~1.2.0",
"istanbul": "*",
"js2xmlparser": "^1.0.0",
"mocha": "*",
"nock": "^2.6.0",
"node-sass": "^3.0.0-alpha.0",
"redis": "^0.12.1",
"request": "^2.65.0",
"sinon": "^1.12.2",
"yargs": "^3.15.0"
},
+14 -8
View File
@@ -3,15 +3,21 @@
# If you change the infra version, be sure to put a warning
# in the change log
INFRA_VERSION=8
INFRA_VERSION=18
# WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
# These constants are used in the installer script as well
BASE_IMAGE=cloudron/base:0.3.3
MYSQL_IMAGE=cloudron/mysql:0.3.3
POSTGRESQL_IMAGE=cloudron/postgresql:0.3.2
MONGODB_IMAGE=cloudron/mongodb:0.3.2
REDIS_IMAGE=cloudron/redis:0.3.2 # if you change this, fix src/addons.js as well
MAIL_IMAGE=cloudron/mail:0.3.2
GRAPHITE_IMAGE=cloudron/graphite:0.3.4
BASE_IMAGE=cloudron/base:0.7.0
MYSQL_IMAGE=cloudron/mysql:0.7.0
POSTGRESQL_IMAGE=cloudron/postgresql:0.7.0
MONGODB_IMAGE=cloudron/mongodb:0.7.0
REDIS_IMAGE=cloudron/redis:0.7.0 # if you change this, fix src/addons.js as well
MAIL_IMAGE=cloudron/mail:0.7.0
GRAPHITE_IMAGE=cloudron/graphite:0.7.0
MYSQL_REPO=cloudron/mysql
POSTGRESQL_REPO=cloudron/postgresql
MONGODB_REPO=cloudron/mongodb
REDIS_REPO=cloudron/redis # if you change this, fix src/addons.js as well
MAIL_REPO=cloudron/mail
GRAPHITE_REPO=cloudron/graphite
+6 -1
View File
@@ -14,18 +14,23 @@ rm -rf "${CONFIG_DIR}"
sudo -u yellowtent mkdir "${CONFIG_DIR}"
########## systemd
rm -f /etc/systemd/system/janitor.*
cp -r "${container_files}/systemd/." /etc/systemd/system/
systemctl daemon-reload
systemctl enable cloudron.target
########## sudoers
rm /etc/sudoers.d/*
rm -f /etc/sudoers.d/*
cp "${container_files}/sudoers" /etc/sudoers.d/yellowtent
########## collectd
rm -rf /etc/collectd
ln -sfF "${DATA_DIR}/collectd" /etc/collectd
########## apparmor docker profile
cp "${container_files}/docker-cloudron-app.apparmor" /etc/apparmor.d/docker-cloudron-app
systemctl restart apparmor
########## nginx
# link nginx config to system config
unlink /etc/nginx 2>/dev/null || rm -rf /etc/nginx
@@ -0,0 +1,32 @@
#include <tunables/global>
profile docker-cloudron-app flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
ptrace peer=@{profile_name},
network,
capability,
file,
umount,
deny @{PROC}/sys/fs/** wklx,
deny @{PROC}/sysrq-trigger rwklx,
deny @{PROC}/mem rwklx,
deny @{PROC}/kmem rwklx,
deny @{PROC}/sys/kernel/[^s][^h][^m]* wklx,
deny @{PROC}/sys/kernel/*/** wklx,
deny mount,
deny /sys/[^f]*/** wklx,
deny /sys/f[^s]*/** wklx,
deny /sys/fs/[^c]*/** wklx,
deny /sys/fs/c[^g]*/** wklx,
deny /sys/fs/cg[^r]*/** wklx,
deny /sys/firmware/efi/efivars/** rwklx,
deny /sys/kernel/security/** rwklx,
}
@@ -1,15 +0,0 @@
[Unit]
Description=Cloudron App Health Monitor
OnFailure=crashnotifier@%n.service
StopWhenUnneeded=true
[Service]
Type=idle
WorkingDirectory=/home/yellowtent/box
Restart=always
ExecStart="/home/yellowtent/box/apphealthtask.js"
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmile" "BOX_ENV=cloudron" "NODE_ENV=production"
KillMode=process
User=yellowtent
Group=yellowtent
MemoryLimit=50M
+1 -1
View File
@@ -7,7 +7,7 @@ StopWhenUnneeded=true
Type=idle
WorkingDirectory=/home/yellowtent/box
Restart=always
ExecStart="/home/yellowtent/box/app.js"
ExecStart=/usr/bin/node --max_old_space_size=150 /home/yellowtent/box/box.js
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmile" "BOX_ENV=cloudron" "NODE_ENV=production"
KillMode=process
User=yellowtent
+2 -2
View File
@@ -2,8 +2,8 @@
Description=Cloudron Smart Cloud
Documentation=https://cloudron.io/documentation.html
StopWhenUnneeded=true
Requires=apphealthtask.service box.service janitor.service oauthproxy.service
After=apphealthtask.service box.service janitor.service oauthproxy.service
Requires=box.service
After=box.service
# AllowIsolate=yes
[Install]
@@ -12,4 +12,4 @@ Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmi
KillMode=process
User=yellowtent
Group=yellowtent
MemoryLimit=50M
-16
View File
@@ -1,16 +0,0 @@
[Unit]
Description=Cloudron Janitor
OnFailure=crashnotifier@%n.service
StopWhenUnneeded=true
[Service]
Type=idle
WorkingDirectory=/home/yellowtent/box
Restart=always
ExecStart="/home/yellowtent/box/janitor.js"
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmile" "BOX_ENV=cloudron" "NODE_ENV=production"
KillMode=process
User=yellowtent
Group=yellowtent
MemoryLimit=50M
@@ -1,16 +0,0 @@
[Unit]
Description=Cloudron OAuth Proxy Service
OnFailure=crashnotifier@%n.service
StopWhenUnneeded=true
[Service]
Type=idle
WorkingDirectory=/home/yellowtent/box
Restart=always
ExecStart="/home/yellowtent/box/oauthproxy.js"
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmile" "BOX_ENV=cloudron" "NODE_ENV=production"
KillMode=process
User=yellowtent
Group=yellowtent
MemoryLimit=50M
+5 -5
View File
@@ -157,14 +157,14 @@ EOF
# The domain might have changed, therefor we have to update the record
# !!! This needs to be in sync with the webadmin, specifically login_callback.js
echo "Add webadmin oauth cient"
ADMIN_SCOPES="root,developer,profile,users,apps,settings,roleUser"
ADMIN_SCOPES="root,developer,profile,users,apps,settings"
mysql -u root -p${mysql_root_password} \
-e "REPLACE INTO clients (id, appId, clientSecret, redirectURI, scope) VALUES (\"cid-webadmin\", \"webadmin\", \"secret-webadmin\", \"${admin_origin}\", \"${ADMIN_SCOPES}\")" box
-e "REPLACE INTO clients (id, appId, type, clientSecret, redirectURI, scope) VALUES (\"cid-webadmin\", \"webadmin\", \"admin\", \"secret-webadmin\", \"${admin_origin}\", \"${ADMIN_SCOPES}\")" box
echo "Add localhost test oauth cient"
ADMIN_SCOPES="root,developer,profile,users,apps,settings,roleUser"
echo "Add localhost test oauth client"
ADMIN_SCOPES="root,developer,profile,users,apps,settings"
mysql -u root -p${mysql_root_password} \
-e "REPLACE INTO clients (id, appId, clientSecret, redirectURI, scope) VALUES (\"cid-test\", \"test\", \"secret-test\", \"http://127.0.0.1:5000\", \"${ADMIN_SCOPES}\")" box
-e "REPLACE INTO clients (id, appId, type, clientSecret, redirectURI, scope) VALUES (\"cid-test\", \"test\", \"test\", \"secret-test\", \"http://127.0.0.1:5000\", \"${ADMIN_SCOPES}\")" box
set_progress "80" "Starting Cloudron"
systemctl start cloudron.target
+1 -1
View File
@@ -220,7 +220,7 @@ LoadPlugin write_graphite
</Plugin>
<Plugin processes>
ProcessMatch "app" "node app.js"
ProcessMatch "app" "node box.js"
</Plugin>
<Plugin swap>
+1 -1
View File
@@ -69,7 +69,7 @@ server {
}
<% } else if ( endpoint === 'oauthproxy' ) { %>
proxy_pass http://127.0.0.1:4000;
proxy_pass http://127.0.0.1:3003;
proxy_set_header X-Cloudron-Proxy-Port <%= port %>;
<% } else if ( endpoint === 'app' ) { %>
proxy_pass http://127.0.0.1:<%= port %>;
+40 -6
View File
@@ -28,21 +28,32 @@ fi
# graphite
graphite_container_id=$(docker run --restart=always -d --name="graphite" \
-m 75m \
--memory-swap 150m \
-p 127.0.0.1:2003:2003 \
-p 127.0.0.1:2004:2004 \
-p 127.0.0.1:8000:8000 \
-v "${DATA_DIR}/graphite:/app/data" \
--read-only -v /tmp -v /run \
"${GRAPHITE_IMAGE}")
echo "Graphite container id: ${graphite_container_id}"
if docker images "${GRAPHITE_REPO}" | tail -n +2 | awk '{ print $1 ":" $2 }' | grep -v "${GRAPHITE_IMAGE}" | xargs --no-run-if-empty docker rmi; then
echo "Removed old graphite images"
fi
# mail
# mail (MAIL_SMTP_PORT is 2500 in addons.js. used in mailer.js as well)
mail_container_id=$(docker run --restart=always -d --name="mail" \
-p 127.0.0.1:25:25 \
-m 75m \
--memory-swap 150m \
-h "${arg_fqdn}" \
-e "DOMAIN_NAME=${arg_fqdn}" \
-v "${DATA_DIR}/box/mail:/app/data" \
--read-only -v /tmp -v /run \
"${MAIL_IMAGE}")
echo "Mail container id: ${mail_container_id}"
if docker images "${MAIL_REPO}" | tail -n +2 | awk '{ print $1 ":" $2 }' | grep -v "${MAIL_IMAGE}" | xargs --no-run-if-empty docker rmi; then
echo "Removed old mail images"
fi
# mysql
mysql_addon_root_password=$(pwgen -1 -s)
@@ -52,11 +63,17 @@ readonly MYSQL_ROOT_PASSWORD='${mysql_addon_root_password}'
readonly MYSQL_ROOT_HOST='${docker0_ip}'
EOF
mysql_container_id=$(docker run --restart=always -d --name="mysql" \
-m 100m \
--memory-swap 200m \
-h "${arg_fqdn}" \
-v "${DATA_DIR}/mysql:/var/lib/mysql" \
-v "${DATA_DIR}/addons/mysql_vars.sh:/etc/mysql/mysql_vars.sh:ro" \
--read-only -v /tmp -v /run \
"${MYSQL_IMAGE}")
echo "MySQL container id: ${mysql_container_id}"
if docker images "${MYSQL_REPO}" | tail -n +2 | awk '{ print $1 ":" $2 }' | grep -v "${MYSQL_IMAGE}" | xargs --no-run-if-empty docker rmi; then
echo "Removed old mysql images"
fi
# postgresql
postgresql_addon_root_password=$(pwgen -1 -s)
@@ -64,11 +81,17 @@ cat > "${DATA_DIR}/addons/postgresql_vars.sh" <<EOF
readonly POSTGRESQL_ROOT_PASSWORD='${postgresql_addon_root_password}'
EOF
postgresql_container_id=$(docker run --restart=always -d --name="postgresql" \
-m 100m \
--memory-swap 200m \
-h "${arg_fqdn}" \
-v "${DATA_DIR}/postgresql:/var/lib/postgresql" \
-v "${DATA_DIR}/addons/postgresql_vars.sh:/etc/postgresql/postgresql_vars.sh:ro" \
--read-only -v /tmp -v /run \
"${POSTGRESQL_IMAGE}")
echo "PostgreSQL container id: ${postgresql_container_id}"
if docker images "${POSTGRESQL_REPO}" | tail -n +2 | awk '{ print $1 ":" $2 }' | grep -v "${POSTGRESQL_IMAGE}" | xargs --no-run-if-empty docker rmi; then
echo "Removed old postgresql images"
fi
# mongodb
mongodb_addon_root_password=$(pwgen -1 -s)
@@ -76,20 +99,31 @@ cat > "${DATA_DIR}/addons/mongodb_vars.sh" <<EOF
readonly MONGODB_ROOT_PASSWORD='${mongodb_addon_root_password}'
EOF
mongodb_container_id=$(docker run --restart=always -d --name="mongodb" \
-m 100m \
--memory-swap 200m \
-h "${arg_fqdn}" \
-v "${DATA_DIR}/mongodb:/var/lib/mongodb" \
-v "${DATA_DIR}/addons/mongodb_vars.sh:/etc/mongodb_vars.sh:ro" \
--read-only -v /tmp -v /run \
"${MONGODB_IMAGE}")
echo "Mongodb container id: ${mongodb_container_id}"
if docker images "${MONGODB_REPO}" | tail -n +2 | awk '{ print $1 ":" $2 }' | grep -v "${MONGODB_IMAGE}" | xargs --no-run-if-empty docker rmi; then
echo "Removed old mongodb images"
fi
# redis
if docker images "${REDIS_REPO}" | tail -n +2 | awk '{ print $1 ":" $2 }' | grep -v "${REDIS_IMAGE}" | xargs --no-run-if-empty docker rmi; then
echo "Removed old redis images"
fi
# only touch apps in installed state. any other state is just resumed by the taskmanager
if [[ "${infra_version}" == "none" ]]; then
# if no existing infra was found (for new and restoring cloudons), download app backups
# if no existing infra was found (for new, upgraded and restored cloudons), download app backups
echo "Marking installed apps for restore"
mysql -u root -ppassword -e 'UPDATE apps SET installationState = "pending_restore" WHERE installationState = "installed"' box
mysql -u root -ppassword -e 'UPDATE apps SET installationState = "pending_restore", oldConfigJson = NULL WHERE installationState = "installed"' box
else
# if existing infra was found, just mark apps for reconfiguration
mysql -u root -ppassword -e 'UPDATE apps SET installationState = "pending_configure" WHERE installationState = "installed"' box
mysql -u root -ppassword -e 'UPDATE apps SET installationState = "pending_configure", oldConfigJson = NULL WHERE installationState = "installed"' box
fi
echo -n "${INFRA_VERSION}" > "${DATA_DIR}/INFRA_VERSION"
+213 -140
View File
@@ -11,8 +11,8 @@ exports = module.exports = {
getBindsSync: getBindsSync,
// exported for testing
_allocateOAuthCredentials: allocateOAuthCredentials,
_removeOAuthCredentials: removeOAuthCredentials
_setupOauth: setupOauth,
_teardownOauth: teardownOauth
};
var appdb = require('./appdb.js'),
@@ -23,63 +23,36 @@ var appdb = require('./appdb.js'),
config = require('./config.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:addons'),
docker = require('./docker.js'),
docker = require('./docker.js').connection,
fs = require('fs'),
generatePassword = require('password-generator'),
hat = require('hat'),
MemoryStream = require('memorystream'),
once = require('once'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
shell = require('./shell.js'),
spawn = child_process.spawn,
tokendb = require('./tokendb.js'),
util = require('util'),
uuid = require('node-uuid'),
vbox = require('./vbox.js');
uuid = require('node-uuid');
var NOOP = function (app, callback) { return callback(); };
var NOOP = function (app, options, callback) { return callback(); };
// setup can be called multiple times for the same app (configure crash restart) and existing data must not be lost
// teardown is destructive. app data stored with the addon is lost
var KNOWN_ADDONS = {
oauth: {
setup: allocateOAuthCredentials,
teardown: removeOAuthCredentials,
backup: NOOP,
restore: allocateOAuthCredentials
},
token: {
setup: allocateAccessToken,
teardown: removeAccessToken,
backup: NOOP,
restore: allocateAccessToken
},
ldap: {
setup: setupLdap,
teardown: teardownLdap,
backup: NOOP,
restore: setupLdap
},
sendmail: {
setup: setupSendMail,
teardown: teardownSendMail,
backup: NOOP,
restore: setupSendMail
},
mysql: {
setup: setupMySql,
teardown: teardownMySql,
backup: backupMySql,
restore: restoreMySql,
},
postgresql: {
setup: setupPostgreSql,
teardown: teardownPostgreSql,
backup: backupPostgreSql,
restore: restorePostgreSql
localstorage: {
setup: NOOP, // docker creates the directory for us
teardown: NOOP,
backup: NOOP, // no backup because it's already inside app data
restore: NOOP
},
mongodb: {
setup: setupMongoDb,
@@ -87,18 +60,48 @@ var KNOWN_ADDONS = {
backup: backupMongoDb,
restore: restoreMongoDb
},
mysql: {
setup: setupMySql,
teardown: teardownMySql,
backup: backupMySql,
restore: restoreMySql,
},
oauth: {
setup: setupOauth,
teardown: teardownOauth,
backup: NOOP,
restore: setupOauth
},
postgresql: {
setup: setupPostgreSql,
teardown: teardownPostgreSql,
backup: backupPostgreSql,
restore: restorePostgreSql
},
redis: {
setup: setupRedis,
teardown: teardownRedis,
backup: NOOP, // no backup because we store redis as part of app's volume
backup: backupRedis,
restore: setupRedis // same thing
},
localstorage: {
setup: NOOP, // docker creates the directory for us
sendmail: {
setup: setupSendMail,
teardown: teardownSendMail,
backup: NOOP,
restore: setupSendMail
},
scheduler: {
setup: NOOP,
teardown: NOOP,
backup: NOOP, // no backup because it's already inside app data
backup: NOOP,
restore: NOOP
},
simpleauth: {
setup: setupSimpleAuth,
teardown: teardownSimpleAuth,
backup: NOOP,
restore: setupSimpleAuth
},
_docker: {
setup: NOOP,
teardown: NOOP,
@@ -128,9 +131,9 @@ function setupAddons(app, addons, callback) {
async.eachSeries(Object.keys(addons), function iterator(addon, iteratorCallback) {
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new Error('No such addon:' + addon));
debugApp(app, 'Setting up addon %s', addon);
debugApp(app, 'Setting up addon %s with options %j', addon, addons[addon]);
KNOWN_ADDONS[addon].setup(app, iteratorCallback);
KNOWN_ADDONS[addon].setup(app, addons[addon], iteratorCallback);
}, callback);
}
@@ -146,9 +149,9 @@ function teardownAddons(app, addons, callback) {
async.eachSeries(Object.keys(addons), function iterator(addon, iteratorCallback) {
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new Error('No such addon:' + addon));
debugApp(app, 'Tearing down addon %s', addon);
debugApp(app, 'Tearing down addon %s with options %j', addon, addons[addon]);
KNOWN_ADDONS[addon].teardown(app, iteratorCallback);
KNOWN_ADDONS[addon].teardown(app, addons[addon], iteratorCallback);
}, callback);
}
@@ -166,7 +169,7 @@ function backupAddons(app, addons, callback) {
async.eachSeries(Object.keys(addons), function iterator (addon, iteratorCallback) {
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new Error('No such addon:' + addon));
KNOWN_ADDONS[addon].backup(app, iteratorCallback);
KNOWN_ADDONS[addon].backup(app, addons[addon], iteratorCallback);
}, callback);
}
@@ -184,7 +187,7 @@ function restoreAddons(app, addons, callback) {
async.eachSeries(Object.keys(addons), function iterator (addon, iteratorCallback) {
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new Error('No such addon:' + addon));
KNOWN_ADDONS[addon].restore(app, iteratorCallback);
KNOWN_ADDONS[addon].restore(app, addons[addon], iteratorCallback);
}, callback);
}
@@ -236,22 +239,23 @@ function getBindsSync(app, addons) {
return binds;
}
function allocateOAuthCredentials(app, callback) {
function setupOauth(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var appId = app.id;
var id = 'cid-addon-' + uuid.v4();
var id = 'cid-' + uuid.v4();
var clientSecret = hat(256);
var redirectURI = 'https://' + config.appFqdn(app.location);
var scope = 'profile,roleUser';
var scope = 'profile';
debugApp(app, 'allocateOAuthCredentials: id:%s clientSecret:%s', id, clientSecret);
debugApp(app, 'setupOauth: id:%s clientSecret:%s', id, clientSecret);
clientdb.delByAppId('addon-' + appId, function (error) { // remove existing creds
clientdb.delByAppIdAndType(appId, clientdb.TYPE_OAUTH, function (error) { // remove existing creds
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
clientdb.add(id, 'addon-' + appId, clientSecret, redirectURI, scope, function (error) {
clientdb.add(id, appId, clientdb.TYPE_OAUTH, clientSecret, redirectURI, scope, function (error) {
if (error) return callback(error);
var env = [
@@ -267,29 +271,79 @@ function allocateOAuthCredentials(app, callback) {
});
}
function removeOAuthCredentials(app, callback) {
function teardownOauth(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'removeOAuthCredentials');
debugApp(app, 'teardownOauth');
clientdb.delByAppId('addon-' + app.id, function (error) {
clientdb.delByAppIdAndType(app.id, clientdb.TYPE_OAUTH, function (error) {
if (error && error.reason !== DatabaseError.NOT_FOUND) console.error(error);
appdb.unsetAddonConfig(app.id, 'oauth', callback);
});
}
function setupLdap(app, callback) {
function setupSimpleAuth(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var appId = app.id;
var id = 'cid-' + uuid.v4();
var scope = 'profile';
debugApp(app, 'setupSimpleAuth: id:%s', id);
clientdb.delByAppIdAndType(app.id, clientdb.TYPE_SIMPLE_AUTH, function (error) { // remove existing creds
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
clientdb.add(id, appId, clientdb.TYPE_SIMPLE_AUTH, '', '', scope, function (error) {
if (error) return callback(error);
var env = [
'SIMPLE_AUTH_SERVER=172.17.42.1',
'SIMPLE_AUTH_PORT=' + config.get('simpleAuthPort'),
'SIMPLE_AUTH_URL=http://172.17.42.1:' + config.get('simpleAuthPort'), // obsolete, remove
'SIMPLE_AUTH_ORIGIN=http://172.17.42.1:' + config.get('simpleAuthPort'),
'SIMPLE_AUTH_CLIENT_ID=' + id
];
debugApp(app, 'Setting simple auth addon config to %j', env);
appdb.setAddonConfig(appId, 'simpleauth', env, callback);
});
});
}
function teardownSimpleAuth(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'teardownSimpleAuth');
clientdb.delByAppIdAndType(app.id, clientdb.TYPE_SIMPLE_AUTH, function (error) {
if (error && error.reason !== DatabaseError.NOT_FOUND) console.error(error);
appdb.unsetAddonConfig(app.id, 'simpleauth', callback);
});
}
function setupLdap(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var env = [
'LDAP_SERVER=172.17.42.1',
'LDAP_PORT=3002',
'LDAP_URL=ldap://172.17.42.1:3002',
'LDAP_PORT=' + config.get('ldapPort'),
'LDAP_URL=ldap://172.17.42.1:' + config.get('ldapPort'),
'LDAP_USERS_BASE_DN=ou=users,dc=cloudron',
'LDAP_GROUPS_BASE_DN=ou=groups,dc=cloudron'
'LDAP_GROUPS_BASE_DN=ou=groups,dc=cloudron',
'LDAP_BIND_DN=cn='+ app.id + ',ou=apps,dc=cloudron',
'LDAP_BIND_PASSWORD=' + hat(256) // this is ignored
];
debugApp(app, 'Setting up LDAP');
@@ -297,8 +351,9 @@ function setupLdap(app, callback) {
appdb.setAddonConfig(app.id, 'ldap', env, callback);
}
function teardownLdap(app, callback) {
function teardownLdap(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Tearing down LDAP');
@@ -306,13 +361,14 @@ function teardownLdap(app, callback) {
appdb.unsetAddonConfig(app.id, 'ldap', callback);
}
function setupSendMail(app, callback) {
function setupSendMail(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var env = [
'MAIL_SMTP_SERVER=mail',
'MAIL_SMTP_PORT=25',
'MAIL_SMTP_PORT=2500', // if you change this, change the mail container
'MAIL_SMTP_USERNAME=' + (app.location || app.id), // use app.id for bare domains
'MAIL_DOMAIN=' + config.fqdn()
];
@@ -322,8 +378,9 @@ function setupSendMail(app, callback) {
appdb.setAddonConfig(app.id, 'sendmail', env, callback);
}
function teardownSendMail(app, callback) {
function teardownSendMail(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Tearing down sendmail');
@@ -331,8 +388,9 @@ function teardownSendMail(app, callback) {
appdb.unsetAddonConfig(app.id, 'sendmail', callback);
}
function setupMySql(app, callback) {
function setupMySql(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Setting up mysql');
@@ -365,7 +423,11 @@ function setupMySql(app, callback) {
});
}
function teardownMySql(app, callback) {
function teardownMySql(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var container = docker.getContainer('mysql');
var cmd = [ '/addons/mysql/service.sh', 'remove', app.id ];
@@ -387,7 +449,7 @@ function teardownMySql(app, callback) {
});
}
function backupMySql(app, callback) {
function backupMySql(app, options, callback) {
debugApp(app, 'Backing up mysql');
callback = once(callback); // ChildProcess exit may or may not be called after error
@@ -406,10 +468,10 @@ function backupMySql(app, callback) {
cp.stderr.pipe(process.stderr);
}
function restoreMySql(app, callback) {
function restoreMySql(app, options, callback) {
callback = once(callback); // ChildProcess exit may or may not be called after error
setupMySql(app, function (error) {
setupMySql(app, options, function (error) {
if (error) return callback(error);
debugApp(app, 'restoreMySql');
@@ -431,8 +493,9 @@ function restoreMySql(app, callback) {
});
}
function setupPostgreSql(app, callback) {
function setupPostgreSql(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Setting up postgresql');
@@ -465,7 +528,11 @@ function setupPostgreSql(app, callback) {
});
}
function teardownPostgreSql(app, callback) {
function teardownPostgreSql(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var container = docker.getContainer('postgresql');
var cmd = [ '/addons/postgresql/service.sh', 'remove', app.id ];
@@ -487,7 +554,7 @@ function teardownPostgreSql(app, callback) {
});
}
function backupPostgreSql(app, callback) {
function backupPostgreSql(app, options, callback) {
debugApp(app, 'Backing up postgresql');
callback = once(callback); // ChildProcess exit may or may not be called after error
@@ -506,10 +573,10 @@ function backupPostgreSql(app, callback) {
cp.stderr.pipe(process.stderr);
}
function restorePostgreSql(app, callback) {
function restorePostgreSql(app, options, callback) {
callback = once(callback); // ChildProcess exit may or may not be called after error
setupPostgreSql(app, function (error) {
setupPostgreSql(app, options, function (error) {
if (error) return callback(error);
debugApp(app, 'restorePostgreSql');
@@ -531,8 +598,9 @@ function restorePostgreSql(app, callback) {
});
}
function setupMongoDb(app, callback) {
function setupMongoDb(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Setting up mongodb');
@@ -565,7 +633,11 @@ function setupMongoDb(app, callback) {
});
}
function teardownMongoDb(app, callback) {
function teardownMongoDb(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var container = docker.getContainer('mongodb');
var cmd = [ '/addons/mongodb/service.sh', 'remove', app.id ];
@@ -587,7 +659,7 @@ function teardownMongoDb(app, callback) {
});
}
function backupMongoDb(app, callback) {
function backupMongoDb(app, options, callback) {
debugApp(app, 'Backing up mongodb');
callback = once(callback); // ChildProcess exit may or may not be called after error
@@ -606,10 +678,10 @@ function backupMongoDb(app, callback) {
cp.stderr.pipe(process.stderr);
}
function restoreMongoDb(app, callback) {
function restoreMongoDb(app, options, callback) {
callback = once(callback); // ChildProcess exit may or may not be called after error
setupMongoDb(app, function (error) {
setupMongoDb(app, options, function (error) {
if (error) return callback(error);
debugApp(app, 'restoreMongoDb');
@@ -642,14 +714,34 @@ function forwardRedisPort(appId, callback) {
var redisPort = parseInt(safe.query(data, 'NetworkSettings.Ports.6379/tcp[0].HostPort'), 10);
if (!Number.isInteger(redisPort)) return callback(new Error('Unable to get container port mapping'));
vbox.forwardFromHostToVirtualBox('redis-' + appId, redisPort);
return callback(null);
});
}
function stopAndRemoveRedis(container, callback) {
function ignoreError(func) {
return function (callback) {
func(function (error) {
if (error) debug('stopAndRemoveRedis: Ignored error:', error);
callback();
});
};
}
// stopping redis with SIGTERM makes it commit the database to disk
async.series([
ignoreError(container.stop.bind(container, { t: 10 })),
ignoreError(container.wait.bind(container)),
ignoreError(container.remove.bind(container, { force: true, v: true }))
], callback);
}
// Ensures that app's addon redis container is running. Can be called when named container already exists/running
function setupRedis(app, callback) {
function setupRedis(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var redisPassword = generatePassword(64, false /* memorable */);
var redisVarsFile = path.join(paths.ADDON_CONFIG_DIR, 'redis-' + app.id + '_vars.sh');
var redisDataDir = path.join(paths.DATA_DIR, app.id + '/redis');
@@ -664,27 +756,28 @@ function setupRedis(app, callback) {
name: 'redis-' + app.id,
Hostname: config.appFqdn(app.location),
Tty: true,
Image: 'cloudron/redis:0.3.2', // if you change this, fix setup/INFRA_VERSION as well
Image: 'cloudron/redis:0.7.0', // if you change this, fix setup/INFRA_VERSION as well
Cmd: null,
Volumes: {},
VolumesFrom: []
};
var isMac = os.platform() === 'darwin';
var startOptions = {
Binds: [
redisVarsFile + ':/etc/redis/redis_vars.sh:ro',
redisDataDir + ':/var/lib/redis:rw'
],
// On Mac (boot2docker), we have to export the port to external world for port forwarding from Mac to work
// On linux, export to localhost only for testing purposes and not for the app itself
PortBindings: {
'6379/tcp': [{ HostPort: '0', HostIp: isMac ? '0.0.0.0' : '127.0.0.1' }]
Volumes: {
'/tmp': {},
'/run': {}
},
RestartPolicy: {
'Name': 'always',
'MaximumRetryCount': 0
VolumesFrom: [],
HostConfig: {
Binds: [
redisVarsFile + ':/etc/redis/redis_vars.sh:ro',
redisDataDir + ':/var/lib/redis:rw'
],
Memory: 1024 * 1024 * 75, // 100mb
MemorySwap: 1024 * 1024 * 75 * 2, // 150mb
PortBindings: {
'6379/tcp': [{ HostPort: '0', HostIp: '127.0.0.1' }]
},
ReadonlyRootfs: true,
RestartPolicy: {
'Name': 'always',
'MaximumRetryCount': 0
}
}
};
@@ -696,11 +789,11 @@ function setupRedis(app, callback) {
];
var redisContainer = docker.getContainer(createOptions.name);
redisContainer.remove({ force: true, v: false }, function (ignoredError) {
stopAndRemoveRedis(redisContainer, function () {
docker.createContainer(createOptions, function (error) {
if (error && error.statusCode !== 409) return callback(error); // if not already created
redisContainer.start(startOptions, function (error) {
redisContainer.start(function (error) {
if (error && error.statusCode !== 304) return callback(error); // if not already running
appdb.setAddonConfig(app.id, 'redis', env, function (error) {
@@ -713,19 +806,21 @@ function setupRedis(app, callback) {
});
}
function teardownRedis(app, callback) {
function teardownRedis(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var container = docker.getContainer('redis-' + app.id);
var removeOptions = {
force: true, // kill container if it's running
v: false // removes volumes associated with the container
v: true // removes volumes associated with the container
};
container.remove(removeOptions, function (error) {
if (error && error.statusCode !== 404) return callback(new Error('Error removing container:' + error));
vbox.unforwardFromHostToVirtualBox('redis-' + app.id);
safe.fs.unlinkSync(paths.ADDON_CONFIG_DIR, 'redis-' + app.id + '_vars.sh');
shell.sudo('teardownRedis', [ RMAPPDIR_CMD, app.id + '/redis' ], function (error, stdout, stderr) {
@@ -736,40 +831,18 @@ function teardownRedis(app, callback) {
});
}
function allocateAccessToken(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
function backupRedis(app, options, callback) {
debugApp(app, 'Backing up redis');
var token = tokendb.generateToken();
var expiresAt = Number.MAX_SAFE_INTEGER; // basically never expire
var scopes = 'profile,users'; // TODO This should be put into the manifest and the user should know those
var clientId = ''; // meaningless for apps so far
callback = once(callback); // ChildProcess exit may or may not be called after error
tokendb.delByIdentifier(tokendb.PREFIX_APP + app.id, function (error) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
tokendb.add(token, tokendb.PREFIX_APP + app.id, clientId, expiresAt, scopes, function (error) {
if (error) return callback(error);
var env = [
'CLOUDRON_TOKEN=' + token
];
debugApp(app, 'Setting token addon config to %j', env);
appdb.setAddonConfig(appId, 'token', env, callback);
});
var cp = spawn('/usr/bin/docker', [ 'exec', 'redis-' + app.id, '/addons/redis/service.sh', 'backup' ]);
cp.on('error', callback);
cp.on('exit', function (code, signal) {
debugApp(app, 'backupRedis: done. code:%s signal:%s', code, signal);
if (!callback.called) callback(code ? 'backupRedis failed with status ' + code : null);
});
cp.stdout.pipe(process.stdout);
cp.stderr.pipe(process.stderr);
}
function removeAccessToken(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
tokendb.delByIdentifier(tokendb.PREFIX_APP + app.id, function (error) {
if (error && error.reason !== DatabaseError.NOT_FOUND) console.error(error);
appdb.unsetAddonConfig(app.id, 'token', callback);
});
}
+34 -10
View File
@@ -6,6 +6,7 @@ exports = module.exports = {
get: get,
getBySubdomain: getBySubdomain,
getByHttpPort: getByHttpPort,
getByContainerId: getByContainerId,
add: add,
exists: exists,
del: del,
@@ -39,7 +40,6 @@ exports = module.exports = {
RSTATE_PENDING_START: 'pending_start',
RSTATE_PENDING_STOP: 'pending_stop',
RSTATE_STOPPED: 'stopped', // app stopped by use
RSTATE_ERROR: 'error',
// run codes (keep in sync in UI)
HEALTH_HEALTHY: 'healthy',
@@ -57,13 +57,9 @@ var assert = require('assert'),
safe = require('safetydance'),
util = require('util');
var APPS_FIELDS = [ 'id', 'appStoreId', 'installationState', 'installationProgress', 'runState',
'health', 'containerId', 'manifestJson', 'httpPort', 'location', 'dnsRecordId',
'accessRestriction', 'lastBackupId', 'lastBackupConfigJson', 'oldConfigJson' ].join(',');
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.installationProgress', 'apps.runState',
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'apps.location', 'apps.dnsRecordId',
'apps.accessRestriction', 'apps.lastBackupId', 'apps.lastBackupConfigJson', 'apps.oldConfigJson' ].join(',');
'apps.accessRestrictionJson', 'apps.lastBackupId', 'apps.lastBackupConfigJson', 'apps.oldConfigJson', 'apps.oauthProxy' ].join(',');
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'environmentVariable', 'appId' ].join(',');
@@ -95,6 +91,13 @@ function postProcess(result) {
for (var i = 0; i < environmentVariables.length; i++) {
result.portBindings[environmentVariables[i]] = parseInt(hostPorts[i], 10);
}
result.oauthProxy = !!result.oauthProxy;
assert(result.accessRestrictionJson === null || typeof result.accessRestrictionJson === 'string');
result.accessRestriction = safe.JSON.parse(result.accessRestrictionJson);
if (result.accessRestriction && !result.accessRestriction.users) result.accessRestriction.users = [];
delete result.accessRestrictionJson;
}
function get(id, callback) {
@@ -145,6 +148,22 @@ function getByHttpPort(httpPort, callback) {
});
}
function getByContainerId(containerId, callback) {
assert.strictEqual(typeof containerId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
+ ' FROM apps LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId WHERE containerId = ? GROUP BY apps.id', [ containerId ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
postProcess(result[0]);
callback(null, result[0]);
});
}
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -160,24 +179,26 @@ function getAll(callback) {
});
}
function add(id, appStoreId, manifest, location, portBindings, accessRestriction, callback) {
function add(id, appStoreId, manifest, location, portBindings, accessRestriction, oauthProxy, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof appStoreId, 'string');
assert(manifest && typeof manifest === 'object');
assert.strictEqual(typeof manifest.version, 'string');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof portBindings, 'object');
assert.strictEqual(typeof accessRestriction, 'string');
assert.strictEqual(typeof accessRestriction, 'object');
assert.strictEqual(typeof oauthProxy, 'boolean');
assert.strictEqual(typeof callback, 'function');
portBindings = portBindings || { };
var manifestJson = JSON.stringify(manifest);
var accessRestrictionJson = JSON.stringify(accessRestriction);
var queries = [ ];
queries.push({
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestriction) VALUES (?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, exports.ISTATE_PENDING_INSTALL, location, accessRestriction ]
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, oauthProxy) VALUES (?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, exports.ISTATE_PENDING_INSTALL, location, accessRestrictionJson, oauthProxy ]
});
Object.keys(portBindings).forEach(function (env) {
@@ -286,6 +307,9 @@ function updateWithConstraints(id, app, constraints, callback) {
} else if (p === 'oldConfig') {
fields.push('oldConfigJson = ?');
values.push(JSON.stringify(app[p]));
} else if (p === 'accessRestriction') {
fields.push('accessRestrictionJson = ?');
values.push(JSON.stringify(app[p]));
} else if (p !== 'portBindings') {
fields.push(p + ' = ?');
values.push(app[p]);
+71 -30
View File
@@ -1,44 +1,33 @@
#!/usr/bin/env node
'use strict';
require('supererror')({ splatchError: true });
var appdb = require('./src/appdb.js'),
var appdb = require('./appdb.js'),
assert = require('assert'),
async = require('async'),
database = require('./src/database.js'),
DatabaseError = require('./src/databaseerror.js'),
debug = require('debug')('box:apphealthtask'),
docker = require('./src/docker.js'),
mailer = require('./src/mailer.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:apphealthmonitor'),
docker = require('./docker.js').connection,
mailer = require('./mailer.js'),
superagent = require('superagent'),
util = require('util');
exports = module.exports = {
run: run
start: start,
stop: stop
};
var HEALTHCHECK_INTERVAL = 10 * 1000; // every 10 seconds. this needs to be small since the UI makes only healthy apps clickable
var UNHEALTHY_THRESHOLD = 3 * 60 * 1000; // 3 minutes
var gHealthInfo = { }; // { time, emailSent }
var gRunTimeout = null;
var gDockerEventStream = null;
function debugApp(app, args) {
function debugApp(app) {
assert(!app || typeof app === 'object');
var prefix = app ? app.location : '(no app)';
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
async.series([
database.initialize,
mailer.initialize
], callback);
}
function setHealth(app, health, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof health, 'string');
@@ -57,7 +46,7 @@ function setHealth(app, health, callback) {
debugApp(app, 'marking as unhealthy since not seen for more than %s minutes', UNHEALTHY_THRESHOLD/(60 * 1000));
mailer.appDied(app);
if (app.appStoreId !== '') mailer.appDied(app); // do not send mails for dev apps
gHealthInfo[app.id].emailSent = true;
} else {
debugApp(app, 'waiting for sometime to update the app health');
@@ -130,18 +119,70 @@ function run() {
processApps(function (error) {
if (error) console.error(error);
setTimeout(run, HEALTHCHECK_INTERVAL);
gRunTimeout = setTimeout(run, HEALTHCHECK_INTERVAL);
});
}
if (require.main === module) {
initialize(function (error) {
if (error) {
console.error('apphealth task exiting with error', error);
process.exit(1);
}
/*
OOM can be tested using stress tool like so:
docker run -ti -m 100M cloudron/base:0.3.3 /bin/bash
apt-get update && apt-get install stress
stress --vm 1 --vm-bytes 200M --vm-hang 0
*/
function processDockerEvents() {
// note that for some reason, the callback is called only on the first event
debug('Listening for docker events');
docker.getEvents({ filters: JSON.stringify({ event: [ 'oom' ] }) }, function (error, stream) {
if (error) return console.error(error);
run();
gDockerEventStream = stream;
stream.setEncoding('utf8');
stream.on('data', function (data) {
var ev = JSON.parse(data);
debug('Container ' + ev.id + ' went OOM');
appdb.getByContainerId(ev.id, function (error, app) {
var program = error || !app.appStoreId ? ev.id : app.appStoreId;
var context = JSON.stringify(ev);
if (app) context = context + '\n\n' + JSON.stringify(app, null, 4) + '\n';
debug('OOM Context: %s', context);
// do not send mails for dev apps
if (app.appStoreId !== '') mailer.sendCrashNotification(program, context); // app can be null if it's an addon crash
});
});
stream.on('error', function (error) {
console.error('Error reading docker events', error);
gDockerEventStream = null; // TODO: reconnect?
});
stream.on('end', function () {
console.error('Docke event stream ended');
gDockerEventStream = null; // TODO: reconnect?
stream.end();
});
});
}
function start(callback) {
assert.strictEqual(typeof callback, 'function');
debug('Starting apphealthmonitor');
processDockerEvents();
run();
callback();
}
function stop(callback) {
assert.strictEqual(typeof callback, 'function');
clearTimeout(gRunTimeout);
gDockerEventStream.end();
callback();
}
+136 -60
View File
@@ -5,6 +5,8 @@
exports = module.exports = {
AppsError: AppsError,
hasAccessTo: hasAccessTo,
get: get,
getBySubdomain: getBySubdomain,
getAll: getAll,
@@ -37,7 +39,8 @@ exports = module.exports = {
// exported for testing
_validateHostname: validateHostname,
_validatePortBindings: validatePortBindings
_validatePortBindings: validatePortBindings,
_validateAccessRestriction: validateAccessRestriction
};
var addons = require('./addons.js'),
@@ -50,7 +53,7 @@ var addons = require('./addons.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:apps'),
docker = require('./docker.js'),
docker = require('./docker.js').connection,
fs = require('fs'),
manifestFormat = require('cloudron-manifestformat'),
path = require('path'),
@@ -114,6 +117,8 @@ AppsError.BAD_STATE = 'Bad State';
AppsError.PORT_RESERVED = 'Port Reserved';
AppsError.PORT_CONFLICT = 'Port Conflict';
AppsError.BILLING_REQUIRED = 'Billing Required';
AppsError.ACCESS_DENIED = 'Access denied';
AppsError.USER_REQUIRED = 'User required';
// Hostname validation comes from RFC 1123 (section 2.1)
// Domain name validation comes from RFC 2181 (Name syntax)
@@ -140,16 +145,19 @@ function validatePortBindings(portBindings, tcpPorts) {
// these ports are reserved even if we listen only on 127.0.0.1 because we setup HostIp to be 127.0.0.1
// for custom tcp ports
var RESERVED_PORTS = [
22, /* ssh */
25, /* smtp */
53, /* dns */
80, /* http */
443, /* https */
919, /* ssh */
2003, /* graphite (lo) */
2004, /* graphite (lo) */
2020, /* install server */
config.get('port'), /* app server (lo) */
config.get('internalPort'), /* internal app server (lo) */
config.get('ldapPort'), /* ldap server (lo) */
config.get('oauthProxyPort'), /* oauth proxy server (lo) */
config.get('simpleAuthPort'), /* simple auth server (lo) */
3306, /* mysql (lo) */
8000 /* graphite (lo) */
];
@@ -176,6 +184,18 @@ function validatePortBindings(portBindings, tcpPorts) {
return null;
}
function validateAccessRestriction(accessRestriction) {
assert.strictEqual(typeof accessRestriction, 'object');
if (accessRestriction === null) return null;
if (!accessRestriction.users || !Array.isArray(accessRestriction.users)) return new Error('users array property required');
if (accessRestriction.users.length === 0) return new Error('users array cannot be empty');
if (!accessRestriction.users.every(function (e) { return typeof e === 'string'; })) return new Error('All users have to be strings');
return null;
}
function getDuplicateErrorDetails(location, portBindings, error) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof portBindings, 'object');
@@ -203,6 +223,14 @@ function getIconUrlSync(app) {
return fs.existsSync(iconPath) ? '/api/v1/apps/' + app.id + '/icon' : null;
}
function hasAccessTo(app, user) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof user, 'object');
if (app.accessRestriction === null) return true;
return app.accessRestriction.users.some(function (e) { return e === user.id; });
}
function get(appId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -248,18 +276,6 @@ function getAll(callback) {
});
}
function validateAccessRestriction(accessRestriction) {
// TODO: make the values below enumerations in the oauth code
switch (accessRestriction) {
case '':
case 'roleUser':
case 'roleAdmin':
return null;
default:
return new Error('Invalid accessRestriction');
}
}
function purchase(appStoreId, callback) {
assert.strictEqual(typeof appStoreId, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -278,13 +294,14 @@ function purchase(appStoreId, callback) {
});
}
function install(appId, appStoreId, manifest, location, portBindings, accessRestriction, icon, callback) {
function install(appId, appStoreId, manifest, location, portBindings, accessRestriction, oauthProxy, icon, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof appStoreId, 'string');
assert(manifest && typeof manifest === 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof portBindings, 'object');
assert.strictEqual(typeof accessRestriction, 'string');
assert.strictEqual(typeof accessRestriction, 'object');
assert.strictEqual(typeof oauthProxy, 'boolean');
assert(!icon || typeof icon === 'string');
assert.strictEqual(typeof callback, 'function');
@@ -303,6 +320,10 @@ function install(appId, appStoreId, manifest, location, portBindings, accessRest
error = validateAccessRestriction(accessRestriction);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
// singleUser mode requires accessRestriction to contain exactly one user
if (manifest.singleUser && accessRestriction === null) return callback(new AppsError(AppsError.USER_REQUIRED));
if (manifest.singleUser && accessRestriction.users.length !== 1) return callback(new AppsError(AppsError.USER_REQUIRED));
if (icon) {
if (!validator.isBase64(icon)) return callback(new AppsError(AppsError.BAD_FIELD, 'icon is not base64'));
@@ -316,7 +337,7 @@ function install(appId, appStoreId, manifest, location, portBindings, accessRest
purchase(appStoreId, function (error) {
if (error) return callback(error);
appdb.add(appId, appStoreId, manifest, location.toLowerCase(), portBindings, accessRestriction, function (error) {
appdb.add(appId, appStoreId, manifest, location.toLowerCase(), portBindings, accessRestriction, oauthProxy, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location.toLowerCase(), portBindings, error));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
@@ -327,11 +348,12 @@ function install(appId, appStoreId, manifest, location, portBindings, accessRest
});
}
function configure(appId, location, portBindings, accessRestriction, callback) {
function configure(appId, location, portBindings, accessRestriction, oauthProxy, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof portBindings, 'object');
assert.strictEqual(typeof accessRestriction, 'string');
assert.strictEqual(typeof accessRestriction, 'object');
assert.strictEqual(typeof oauthProxy, 'boolean');
assert.strictEqual(typeof callback, 'function');
var error = validateHostname(location, config.fqdn());
@@ -350,12 +372,14 @@ function configure(appId, location, portBindings, accessRestriction, callback) {
var values = {
location: location.toLowerCase(),
accessRestriction: accessRestriction,
oauthProxy: oauthProxy,
portBindings: portBindings,
oldConfig: {
location: app.location,
accessRestriction: app.accessRestriction,
portBindings: app.portBindings
portBindings: app.portBindings,
oauthProxy: app.oauthProxy
}
};
@@ -409,7 +433,9 @@ function update(appId, force, manifest, portBindings, icon, callback) {
portBindings: portBindings,
oldConfig: {
manifest: app.manifest,
portBindings: app.portBindings
portBindings: app.portBindings,
accessRestriction: app.accessRestriction,
oauthProxy: app.oauthProxy
}
};
@@ -509,6 +535,7 @@ function restore(appId, callback) {
oldConfig: {
location: app.location,
accessRestriction: app.accessRestriction,
oauthProxy: app.oauthProxy,
portBindings: app.portBindings,
manifest: app.manifest
}
@@ -649,27 +676,38 @@ function autoupdateApps(updateInfo, callback) { // updateInfo is { appId -> { ma
assert.strictEqual(typeof callback, 'function');
function canAutoupdateApp(app, newManifest) {
// TODO: maybe check the description as well?
if (!newManifest.tcpPorts && !app.portBindings) return true;
if (!newManifest.tcpPorts || !app.portBindings) return false;
var tcpPorts = newManifest.tcpPorts || { };
var portBindings = app.portBindings; // this is never null
for (var env in newManifest.tcpPorts) {
if (!(env in app.portBindings)) return false;
}
if (Object.keys(tcpPorts).length === 0 && Object.keys(portBindings).length === 0) return null;
if (Object.keys(tcpPorts).length === 0) return new Error('tcpPorts is now empty but portBindings is not');
if (Object.keys(portBindings).length === 0) return new Error('portBindings is now empty but tcpPorts is not');
return true;
for (var env in tcpPorts) {
if (!(env in portBindings)) return new Error(env + ' is required from user');
}
// it's fine if one or more keys got removed
return null;
}
if (!updateInfo) return callback(null);
async.eachSeries(Object.keys(updateInfo), function iterator(appId, iteratorDone) {
get(appId, function (error, app) {
if (!canAutoupdateApp(app, updateInfo[appId].manifest)) {
if (error) {
debug('Cannot autoupdate app %s : %s', appId, error.message);
return iteratorDone();
}
error = canAutoupdateApp(app, updateInfo[appId].manifest);
if (error) {
debug('app %s requires manual update. %s', appId, error.message);
return iteratorDone();
}
update(appId, false /* force */, updateInfo[appId].manifest, app.portBindings, null /* icon */, function (error) {
if (error) debug('Error initiating autoupdate of %s', appId);
if (error) debug('Error initiating autoupdate of %s. %s', appId, error.message);
iteratorDone(null);
});
@@ -677,33 +715,35 @@ function autoupdateApps(updateInfo, callback) { // updateInfo is { appId -> { ma
}, callback);
}
function backupApp(app, addonsToBackup, 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
}
// set the 'creation' date of lastBackup so that the backup persists across time based archival rules
// s3 does not allow changing creation time, so copying the last backup is easy way out for now
function reuseOldBackup(app, callback) {
assert.strictEqual(typeof app.lastBackupId, 'string');
assert.strictEqual(typeof callback, 'function');
backups.copyLastBackup(app, function (error, newBackupId) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
debugApp(app, 'reuseOldBackup: reused old backup %s as %s', app.lastBackupId, newBackupId);
callback(null, newBackupId);
});
}
function createNewBackup(app, addonsToBackup, callback) {
assert.strictEqual(typeof app, 'object');
assert(!addonsToBackup || typeof addonsToBackup, 'object');
assert.strictEqual(typeof callback, 'function');
function canBackupApp(app) {
// only backup apps that are installed or pending configure. 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 ||
app.installationState === appdb.ISTATE_PENDING_UPDATE; // called from apptask
}
if (!canBackupApp(app)) return callback(new AppsError(AppsError.BAD_STATE, 'App not healthy'));
var appConfig = {
manifest: app.manifest,
location: app.location,
portBindings: app.portBindings,
accessRestriction: app.accessRestriction
};
if (!safe.fs.writeFileSync(path.join(paths.DATA_DIR, app.id + '/config.json'), JSON.stringify(appConfig), 'utf8')) {
return callback(safe.error);
}
backups.getBackupUrl(app, function (error, result) {
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
@@ -718,13 +758,50 @@ function backupApp(app, addonsToBackup, callback) {
], function (error) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
debugApp(app, 'backupApp: successful id:%s', result.id);
callback(null, result.id);
});
});
}
setRestorePoint(app.id, result.id, appConfig, function (error) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
function backupApp(app, addonsToBackup, callback) {
assert.strictEqual(typeof app, 'object');
assert(!addonsToBackup || typeof addonsToBackup, 'object');
assert.strictEqual(typeof callback, 'function');
return callback(null, result.id);
});
var appConfig = null, backupFunction;
if (!canBackupApp(app)) {
if (!app.lastBackupId) {
debugApp(app, 'backupApp: cannot backup app');
return callback(new AppsError(AppsError.BAD_STATE, 'App not healthy and never backed up previously'));
}
appConfig = app.lastBackupConfig;
backupFunction = reuseOldBackup.bind(null, app);
} else {
appConfig = {
manifest: app.manifest,
location: app.location,
portBindings: app.portBindings,
accessRestriction: app.accessRestriction,
oauthProxy: app.oauthProxy
};
backupFunction = createNewBackup.bind(null, app, addonsToBackup);
if (!safe.fs.writeFileSync(path.join(paths.DATA_DIR, app.id + '/config.json'), JSON.stringify(appConfig), 'utf8')) {
return callback(safe.error);
}
}
backupFunction(function (error, backupId) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
debugApp(app, 'backupApp: successful id:%s', backupId);
setRestorePoint(app.id, backupId, appConfig, function (error) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
return callback(null, backupId);
});
});
}
@@ -767,4 +844,3 @@ function restoreApp(app, addonsToRestore, callback) {
});
});
}
+71 -244
View File
@@ -25,6 +25,12 @@ exports = module.exports = {
require('supererror')({ splatchError: true });
// remove timestamp from debug() based output
require('debug').formatArgs = function formatArgs() {
arguments[0] = this.namespace + ' ' + arguments[0];
return arguments;
};
var addons = require('./addons.js'),
appdb = require('./appdb.js'),
apps = require('./apps.js'),
@@ -41,17 +47,17 @@ var addons = require('./addons.js'),
hat = require('hat'),
manifestFormat = require('cloudron-manifestformat'),
net = require('net'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
semver = require('semver'),
shell = require('./shell.js'),
SubdomainError = require('./subdomainerror.js'),
subdomains = require('./subdomains.js'),
superagent = require('superagent'),
sysinfo = require('./sysinfo.js'),
util = require('util'),
uuid = require('node-uuid'),
vbox = require('./vbox.js'),
_ = require('underscore');
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/../setup/start/nginx/appconfig.ejs', { encoding: 'utf8' }),
@@ -72,6 +78,14 @@ function debugApp(app, args) {
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
function targetBoxVersion(manifest) {
if ('targetBoxVersion' in manifest) return manifest.targetBoxVersion;
if ('minBoxVersion' in manifest) return manifest.minBoxVersion;
return '0.0.1';
}
// We expect conflicts to not happen despite closing the port (parallel app installs, app update does not reconfigure nginx etc)
// https://tools.ietf.org/html/rfc6056#section-3.5 says linux uses random ephemeral port allocation
function getFreePort(callback) {
@@ -93,7 +107,7 @@ function configureNginx(app, callback) {
if (error) return callback(error);
var sourceDir = path.resolve(__dirname, '..');
var endpoint = app.accessRestriction ? 'oauthproxy' : 'app';
var endpoint = app.oauthProxy ? 'oauthproxy' : 'app';
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, { sourceDir: sourceDir, adminOrigin: config.adminOrigin(), vhost: config.appFqdn(app.location), port: freePort, endpoint: endpoint });
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
@@ -108,8 +122,6 @@ function configureNginx(app, callback) {
exports._reloadNginx,
updateApp.bind(null, app, { httpPort: freePort })
], callback);
vbox.forwardFromHostToVirtualBox(app.id + '-http', freePort);
});
}
@@ -121,143 +133,27 @@ function unconfigureNginx(app, callback) {
}
exports._reloadNginx(callback);
vbox.unforwardFromHostToVirtualBox(app.id + '-http');
}
function downloadImage(app, callback) {
debugApp(app, 'downloadImage %s', app.manifest.dockerImage);
docker.pull(app.manifest.dockerImage, function (err, stream) {
if (err) return callback(new Error('Error connecting to docker'));
// 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) || { };
debugApp(app, 'downloadImage data: %j', data);
// The information here is useless because this is per layer as opposed to per image
if (data.status) {
debugApp(app, 'progress: %s', data.status); // progressDetail { current, total }
} else if (data.error) {
debugApp(app, 'error detail: %s', data.errorDetail.message);
}
});
stream.on('end', function () {
debugApp(app, 'download image successfully');
var image = docker.getImage(app.manifest.dockerImage);
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'));
}
debugApp(app, 'This image exposes ports: %j', data.Config.ExposedPorts);
return callback(null);
});
});
});
}
function createContainer(app, callback) {
appdb.getPortBindings(app.id, function (error, portBindings) {
if (error) return callback(error);
assert(!app.containerId); // otherwise, it will trigger volumeFrom
var manifest = app.manifest;
var exposedPorts = {};
var env = [];
debugApp(app, 'creating container');
// docker portBindings requires ports to be exposed
exposedPorts[manifest.httpPort + '/tcp'] = {};
docker.createContainer(app, function (error, container) {
if (error) return callback(new Error('Error creating container: ' + error));
for (var e in portBindings) {
var hostPort = portBindings[e];
var containerPort = manifest.tcpPorts[e].containerPort || hostPort;
exposedPorts[containerPort + '/tcp'] = {};
env.push(e + '=' + hostPort);
}
env.push('CLOUDRON=1');
env.push('WEBADMIN_ORIGIN' + '=' + config.adminOrigin());
env.push('API_ORIGIN' + '=' + config.adminOrigin());
addons.getEnvironment(app, function (error, addonEnv) {
if (error) return callback(new Error('Error getting addon env: ' + error));
var containerOptions = {
name: app.id,
Hostname: config.appFqdn(app.location),
Tty: true,
Image: app.manifest.dockerImage,
Cmd: null,
Env: env.concat(addonEnv),
ExposedPorts: exposedPorts
};
debugApp(app, 'Creating container for %s', app.manifest.dockerImage);
docker.createContainer(containerOptions, function (error, container) {
if (error) return callback(new Error('Error creating container: ' + error));
updateApp(app, { containerId: container.id }, callback);
});
});
updateApp(app, { containerId: container.id }, callback);
});
}
function deleteContainer(app, callback) {
if (app.containerId === null) return callback(null);
function deleteContainers(app, callback) {
debugApp(app, 'deleting containers');
var container = docker.getContainer(app.containerId);
docker.deleteContainers(app.id, function (error) {
if (error) return callback(new Error('Error deleting container: ' + error));
var removeOptions = {
force: true, // kill container if it's running
v: false // removes volumes associated with the container
};
container.remove(removeOptions, function (error) {
if (error && error.statusCode === 404) return updateApp(app, { containerId: null }, callback);
if (error) debugApp(app, 'Error removing container', error);
callback(error);
});
}
function deleteImage(app, manifest, callback) {
var dockerImage = manifest ? manifest.dockerImage : null;
if (!dockerImage) return callback(null);
docker.getImage(dockerImage).inspect(function (error, result) {
if (error && error.statusCode === 404) return callback(null);
if (error) return callback(error);
var removeOptions = {
force: true,
noprune: false
};
// delete image by id because 'docker pull' pulls down all the tags and this is the only way to delete all tags
docker.getImage(result.Id).remove(removeOptions, function (error) {
if (error && error.statusCode === 404) return callback(null);
if (error && error.statusCode === 409) return callback(null); // another container using the image
if (error) debugApp(app, 'Error removing image', error);
callback(error);
});
updateApp(app, { containerId: null }, callback);
});
}
@@ -273,22 +169,21 @@ function allocateOAuthProxyCredentials(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (!app.accessRestriction) return callback(null);
if (!app.oauthProxy) return callback(null);
var appId = 'proxy-' + app.id;
var id = 'cid-proxy-' + uuid.v4();
var id = 'cid-' + uuid.v4();
var clientSecret = hat(256);
var redirectURI = 'https://' + config.appFqdn(app.location);
var scope = 'profile,' + app.accessRestriction;
var scope = 'profile';
clientdb.add(id, appId, clientSecret, redirectURI, scope, callback);
clientdb.add(id, app.id, clientdb.TYPE_PROXY, clientSecret, redirectURI, scope, callback);
}
function removeOAuthProxyCredentials(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
clientdb.delByAppId('proxy-' + app.id, function (error) {
clientdb.delByAppIdAndType(app.id, clientdb.TYPE_PROXY, function (error) {
if (error && error.reason !== DatabaseError.NOT_FOUND) {
debugApp(app, 'Error removing OAuth client id', error);
return callback(error);
@@ -313,85 +208,6 @@ function removeCollectdProfile(app, callback) {
});
}
function startContainer(app, callback) {
appdb.getPortBindings(app.id, function (error, portBindings) {
if (error) return callback(error);
var manifest = app.manifest;
var dockerPortBindings = { };
var isMac = os.platform() === 'darwin';
// On Mac (boot2docker), we have to export the port to external world for port forwarding from Mac to work
dockerPortBindings[manifest.httpPort + '/tcp'] = [ { HostIp: isMac ? '0.0.0.0' : '127.0.0.1', HostPort: app.httpPort + '' } ];
for (var env in portBindings) {
var hostPort = portBindings[env];
var containerPort = manifest.tcpPorts[env].containerPort || hostPort;
dockerPortBindings[containerPort + '/tcp'] = [ { HostIp: '0.0.0.0', HostPort: hostPort + '' } ];
vbox.forwardFromHostToVirtualBox(app.id + '-tcp' + containerPort, hostPort);
}
var memoryLimit = manifest.memoryLimit || 1024 * 1024 * 200; // 200mb by default
var startOptions = {
Binds: addons.getBindsSync(app, app.manifest.addons),
Memory: memoryLimit / 2,
MemorySwap: memoryLimit, // Memory + Swap
PortBindings: dockerPortBindings,
PublishAllPorts: false,
Links: addons.getLinksSync(app, app.manifest.addons),
RestartPolicy: {
"Name": "always",
"MaximumRetryCount": 0
},
CpuShares: 512 // relative to 1024 for system processes
};
var container = docker.getContainer(app.containerId);
debugApp(app, 'Starting container %s with options: %j', container.id, JSON.stringify(startOptions));
container.start(startOptions, function (error, data) {
if (error && error.statusCode !== 304) return callback(new Error('Error starting container:' + error));
return callback(null);
});
});
}
function stopContainer(app, callback) {
if (!app.containerId) {
debugApp(app, 'No previous container to stop');
return callback();
}
var container = docker.getContainer(app.containerId);
debugApp(app, 'Stopping container %s', container.id);
var options = {
t: 10 // wait for 10 seconds before killing it
};
container.stop(options, function (error) {
if (error && (error.statusCode !== 304 && error.statusCode !== 404)) return callback(new Error('Error stopping container:' + error));
var tcpPorts = safe.query(app, 'manifest.tcpPorts', { });
for (var containerPort in tcpPorts) {
vbox.unforwardFromHostToVirtualBox(app.id + '-tcp' + containerPort);
}
debugApp(app, 'Waiting for container ' + container.id);
container.wait(function (error, data) {
if (error && (error.statusCode !== 304 && error.statusCode !== 404)) return callback(new Error('Error waiting on container:' + error));
debugApp(app, 'Container stopped with status code [%s]', data ? String(data.StatusCode) : '');
return callback(null);
});
});
}
function verifyManifest(app, callback) {
debugApp(app, 'Verifying manifest');
@@ -424,29 +240,43 @@ function downloadIcon(app, callback) {
}
function registerSubdomain(app, callback) {
debugApp(app, 'Registering subdomain location [%s]', app.location);
// even though the bare domain is already registered in the appstore, we still
// need to register it so that we have a dnsRecordId to wait for it to complete
var record = { subdomain: app.location, type: 'A', value: sysinfo.getIp() };
subdomains.add(record, function (error, changeId) {
if (error) return callback(error);
async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Registering subdomain location [%s]', app.location);
debugApp(app, 'Registered subdomain.');
subdomains.add(record, function (error, changeId) {
if (error && (error.reason === SubdomainError.STILL_BUSY || error.reason === SubdomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
updateApp(app, { dnsRecordId: changeId }, callback);
retryCallback(null, error || changeId);
});
}, function (error, result) {
if (error || result instanceof Error) return callback(error || result);
updateApp(app, { dnsRecordId: result }, callback);
});
}
function unregisterSubdomain(app, location, callback) {
debugApp(app, 'Unregistering subdomain: %s', location);
// do not unregister bare domain because we show a error/cloudron info page there
if (location === '') return callback(null);
if (location === '') {
debugApp(app, 'Skip unregister of empty subdomain');
return callback(null);
}
var record = { subdomain: location, type: 'A', value: sysinfo.getIp() };
subdomains.remove(record, function (error) {
async.retry({ times: 30, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Unregistering subdomain: %s', location);
subdomains.remove(record, function (error) {
if (error && (error.reason === SubdomainError.STILL_BUSY || error.reason === SubdomainError.EXTERNAL_ERROR))return retryCallback(error); // try again
retryCallback(error);
});
}, function (error) {
if (error) debugApp(app, 'Error unregistering subdomain: %s', error);
updateApp(app, { dnsRecordId: null }, callback);
@@ -484,7 +314,7 @@ function waitForDnsPropagation(app, callback) {
// updates the app object and the database
function updateApp(app, values, callback) {
debugApp(app, 'installationState: %s progress: %s', app.installationState, app.installationProgress);
debugApp(app, 'updating app with values: %j', values);
appdb.update(app.id, values, function (error) {
if (error) return callback(error);
@@ -515,7 +345,7 @@ function install(app, callback) {
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
removeCollectdProfile.bind(null, app),
stopApp.bind(null, app),
deleteContainer.bind(null, app),
deleteContainers.bind(null, app),
addons.teardownAddons.bind(null, app, app.manifest.addons),
deleteVolume.bind(null, app),
unregisterSubdomain.bind(null, app, app.location),
@@ -536,7 +366,7 @@ function install(app, callback) {
registerSubdomain.bind(null, app),
updateApp.bind(null, app, { installationProgress: '40, Downloading image' }),
downloadImage.bind(null, app),
docker.downloadImage.bind(null, app.manifest),
updateApp.bind(null, app, { installationProgress: '50, Creating volume' }),
createVolume.bind(null, app),
@@ -601,14 +431,14 @@ function restore(app, callback) {
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
removeCollectdProfile.bind(null, app),
stopApp.bind(null, app),
deleteContainer.bind(null, app),
deleteContainers.bind(null, app),
// oldConfig can be null during upgrades
addons.teardownAddons.bind(null, app, app.oldConfig ? app.oldConfig.manifest.addons : null),
deleteVolume.bind(null, app),
function deleteImageIfChanged(done) {
if (!app.oldConfig || (app.oldConfig.manifest.dockerImage === app.manifest.dockerImage)) return done();
deleteImage(app, app.oldConfig.manifest, done);
docker.deleteImage(app.oldConfig.manifest, done);
},
removeOAuthProxyCredentials.bind(null, app),
removeIcon.bind(null, app),
@@ -627,7 +457,7 @@ function restore(app, callback) {
registerSubdomain.bind(null, app),
updateApp.bind(null, app, { installationProgress: '60, Downloading image' }),
downloadImage.bind(null, app),
docker.downloadImage.bind(null, app.manifest),
updateApp.bind(null, app, { installationProgress: '65, Creating volume' }),
createVolume.bind(null, app),
@@ -667,7 +497,7 @@ function configure(app, callback) {
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
removeCollectdProfile.bind(null, app),
stopApp.bind(null, app),
deleteContainer.bind(null, app),
deleteContainers.bind(null, app),
function (next) {
// oldConfig can be null during an infra update
if (!app.oldConfig || app.oldConfig.location === app.location) return next();
@@ -730,12 +560,12 @@ function update(app, callback) {
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
removeCollectdProfile.bind(null, app),
stopApp.bind(null, app),
deleteContainer.bind(null, app),
deleteContainers.bind(null, app),
addons.teardownAddons.bind(null, app, unusedAddons),
function deleteImageIfChanged(done) {
if (app.oldConfig.manifest.dockerImage === app.manifest.dockerImage) return done();
deleteImage(app, app.oldConfig.manifest, done);
docker.deleteImage(app.oldConfig.manifest, done);
},
// removeIcon.bind(null, app), // do not remove icon, otherwise the UI breaks for a short time...
@@ -752,7 +582,7 @@ function update(app, callback) {
downloadIcon.bind(null, app),
updateApp.bind(null, app, { installationProgress: '45, Downloading image' }),
downloadImage.bind(null, app),
docker.downloadImage.bind(null, app.manifest),
updateApp.bind(null, app, { installationProgress: '70, Updating addons' }),
addons.setupAddons.bind(null, app, app.manifest.addons),
@@ -790,7 +620,7 @@ function uninstall(app, callback) {
stopApp.bind(null, app),
updateApp.bind(null, app, { installationProgress: '20, Deleting container' }),
deleteContainer.bind(null, app),
deleteContainers.bind(null, app),
updateApp.bind(null, app, { installationProgress: '30, Teardown addons' }),
addons.teardownAddons.bind(null, app, app.manifest.addons),
@@ -799,7 +629,7 @@ function uninstall(app, callback) {
deleteVolume.bind(null, app),
updateApp.bind(null, app, { installationProgress: '50, Deleting image' }),
deleteImage.bind(null, app, app.manifest),
docker.deleteImage.bind(null, app.manifest),
updateApp.bind(null, app, { installationProgress: '60, Unregistering subdomain' }),
unregisterSubdomain.bind(null, app, app.location),
@@ -819,18 +649,15 @@ function uninstall(app, callback) {
}
function runApp(app, callback) {
startContainer(app, function (error) {
if (error) {
debugApp(app, 'Error starting container : %s', error);
return updateApp(app, { runState: appdb.RSTATE_ERROR }, callback);
}
docker.startContainer(app.containerId, function (error) {
if (error) return callback(error);
updateApp(app, { runState: appdb.RSTATE_RUNNING }, callback);
});
}
function stopApp(app, callback) {
stopContainer(app, function (error) {
docker.stopContainers(app.id, function (error) {
if (error) return callback(error);
updateApp(app, { runState: appdb.RSTATE_STOPPED }, callback);
+41 -41
View File
@@ -3,16 +3,14 @@
'use strict';
exports = module.exports = {
AWSError: AWSError,
getAWSCredentials: getAWSCredentials,
getSignedUploadUrl: getSignedUploadUrl,
getSignedDownloadUrl: getSignedDownloadUrl,
addSubdomain: addSubdomain,
delSubdomain: delSubdomain,
getChangeStatus: getChangeStatus
getChangeStatus: getChangeStatus,
copyObject: copyObject
};
var assert = require('assert'),
@@ -20,32 +18,7 @@ var assert = require('assert'),
config = require('./config.js'),
debug = require('debug')('box:aws'),
SubdomainError = require('./subdomainerror.js'),
superagent = require('superagent'),
util = require('util');
// http://dustinsenos.com/articles/customErrorsInNode
// http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
function AWSError(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(AWSError, Error);
AWSError.INTERNAL_ERROR = 'Internal Error';
AWSError.MISSING_CREDENTIALS = 'Missing AWS credentials';
superagent = require('superagent');
function getAWSCredentials(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -53,26 +26,34 @@ function getAWSCredentials(callback) {
// CaaS
if (config.token()) {
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/awscredentials';
superagent.get(url).query({ token: config.token() }).end(function (error, result) {
superagent.post(url).query({ token: config.token() }).end(function (error, result) {
if (error) return callback(error);
if (result.statusCode !== 201) return callback(new Error(result.text));
if (!result.body || !result.body.credentials) return callback(new Error('Unexpected response'));
return callback(null, {
var credentials = {
accessKeyId: result.body.credentials.AccessKeyId,
secretAccessKey: result.body.credentials.SecretAccessKey,
sessionToken: result.body.credentials.SessionToken,
region: 'us-east-1'
});
};
if (config.aws().endpoint) credentials.endpoint = new AWS.Endpoint(config.aws().endpoint);
callback(null, credentials);
});
} else {
if (!config.aws().accessKeyId || !config.aws().secretAccessKey) return callback(new AWSError(AWSError.MISSING_CREDENTIALS));
if (!config.aws().accessKeyId || !config.aws().secretAccessKey) return callback(new SubdomainError(SubdomainError.MISSING_CREDENTIALS));
callback(null, {
var credentials = {
accessKeyId: config.aws().accessKeyId,
secretAccessKey: config.aws().secretAccessKey,
region: 'us-east-1'
});
};
if (config.aws().endpoint) credentials.endpoint = new AWS.Endpoint(config.aws().endpoint);
callback(null, credentials);
}
}
@@ -80,7 +61,7 @@ function getSignedUploadUrl(filename, callback) {
assert.strictEqual(typeof filename, 'string');
assert.strictEqual(typeof callback, 'function');
debug('getSignedUploadUrl()');
debug('getSignedUploadUrl: %s', filename);
getAWSCredentials(function (error, credentials) {
if (error) return callback(error);
@@ -103,7 +84,7 @@ function getSignedDownloadUrl(filename, callback) {
assert.strictEqual(typeof filename, 'string');
assert.strictEqual(typeof callback, 'function');
debug('getSignedDownloadUrl()');
debug('getSignedDownloadUrl: %s', filename);
getAWSCredentials(function (error, credentials) {
if (error) return callback(error);
@@ -186,9 +167,9 @@ function addSubdomain(zoneName, subdomain, type, value, callback) {
var route53 = new AWS.Route53(credentials);
route53.changeResourceRecordSets(params, function(error, result) {
if (error && error.code === 'PriorRequestNotComplete') {
return callback(new SubdomainError(SubdomainError.STILL_BUSY, new Error(error)));
return callback(new SubdomainError(SubdomainError.STILL_BUSY, error.message));
} else if (error) {
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
}
debug('addSubdomain: success. changeInfoId:%j', result);
@@ -280,3 +261,22 @@ function getChangeStatus(changeId, callback) {
});
});
}
function copyObject(from, to, callback) {
assert.strictEqual(typeof from, 'string');
assert.strictEqual(typeof to, 'string');
assert.strictEqual(typeof callback, 'function');
getAWSCredentials(function (error, credentials) {
if (error) return callback(error);
var params = {
Bucket: config.aws().backupBucket, // target bucket
Key: config.aws().backupPrefix + '/' + to, // target file
CopySource: config.aws().backupBucket + '/' + config.aws().backupPrefix + '/' + from, // source
};
var s3 = new AWS.S3(credentials);
s3.copyObject(params, callback);
});
}
+18 -3
View File
@@ -6,7 +6,9 @@ exports = module.exports = {
getAllPaged: getAllPaged,
getBackupUrl: getBackupUrl,
getRestoreUrl: getRestoreUrl
getRestoreUrl: getRestoreUrl,
copyLastBackup: copyLastBackup
};
var assert = require('assert'),
@@ -76,7 +78,7 @@ function getBackupUrl(app, callback) {
backupKey: config.backupKey()
};
debug('getBackupUrl: ', obj);
debug('getBackupUrl: id:%s url:%s sessionToken:%s backupKey:%s', obj.id, obj.url, obj.sessionToken, obj.backupKey);
callback(null, obj);
});
@@ -97,8 +99,21 @@ function getRestoreUrl(backupId, callback) {
backupKey: config.backupKey()
};
debug('getRestoreUrl: ', obj);
debug('getRestoreUrl: id:%s url:%s sessionToken:%s backupKey:%s', obj.id, obj.url, obj.sessionToken, obj.backupKey);
callback(null, obj);
});
}
function copyLastBackup(app, callback) {
assert(app && typeof app === 'object');
assert.strictEqual(typeof app.lastBackupId, 'string');
assert.strictEqual(typeof callback, 'function');
var toFilename = util.format('appbackup_%s_%s-v%s.tar.gz', app.id, (new Date()).toISOString(), app.manifest.version);
aws.copyObject(app.lastBackupId, toFilename, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
return callback(null, toFilename);
});
}
+91
View File
@@ -0,0 +1,91 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
addSubdomain: addSubdomain,
delSubdomain: delSubdomain,
getChangeStatus: getChangeStatus
};
var assert = require('assert'),
config = require('./config.js'),
debug = require('debug')('box:caas'),
SubdomainError = require('./subdomainerror.js'),
superagent = require('superagent'),
util = require('util');
function addSubdomain(zoneName, subdomain, type, value, callback) {
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert.strictEqual(typeof callback, 'function');
var fqdn = subdomain !== '' && type === 'TXT' ? subdomain + '.' + config.fqdn() : config.appFqdn(subdomain);
debug('addSubdomain: zoneName: %s subdomain: %s type: %s value: %s fqdn: %s', zoneName, subdomain, type, value, fqdn);
var data = {
type: type,
value: value
};
superagent
.post(config.apiServerOrigin() + '/api/v1/domains/' + fqdn)
.query({ token: config.token() })
.send(data)
.end(function (error, result) {
if (error) return callback(error);
if (result.status === 420) return callback(new SubdomainError(SubdomainError.STILL_BUSY));
if (result.status !== 201) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
return callback(null, result.body.changeId);
});
}
function delSubdomain(zoneName, subdomain, type, value, callback) {
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert.strictEqual(typeof callback, 'function');
debug('delSubdomain: %s for domain %s.', subdomain, zoneName);
var data = {
type: type,
value: value
};
superagent
.del(config.apiServerOrigin() + '/api/v1/domains/' + config.appFqdn(subdomain))
.query({ token: config.token() })
.send(data)
.end(function (error, result) {
if (error) return callback(error);
if (result.status === 420) return callback(new SubdomainError(SubdomainError.STILL_BUSY));
if (result.status === 404) return callback(new SubdomainError(SubdomainError.NOT_FOUND));
if (result.status !== 204) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
return callback(null);
});
}
function getChangeStatus(changeId, callback) {
assert.strictEqual(typeof changeId, 'string');
assert.strictEqual(typeof callback, 'function');
if (changeId === '') return callback(null, 'INSYNC');
superagent
.get(config.apiServerOrigin() + '/api/v1/domains/' + config.fqdn() + '/status/' + changeId)
.query({ token: config.token() })
.end(function (error, result) {
if (error) return callback(error);
if (result.status !== 200) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
return callback(null, result.body.status);
});
}
+38 -21
View File
@@ -8,19 +8,27 @@ exports = module.exports = {
getAllWithTokenCountByIdentifier: getAllWithTokenCountByIdentifier,
add: add,
del: del,
update: update,
getByAppId: getByAppId,
delByAppId: delByAppId,
getByAppIdAndType: getByAppIdAndType,
_clear: clear
delByAppId: delByAppId,
delByAppIdAndType: delByAppIdAndType,
_clear: clear,
TYPE_EXTERNAL: 'external',
TYPE_OAUTH: 'addon-oauth',
TYPE_SIMPLE_AUTH: 'addon-simpleauth',
TYPE_PROXY: 'addon-proxy',
TYPE_ADMIN: 'admin'
};
var assert = require('assert'),
database = require('./database.js'),
DatabaseError = require('./databaseerror.js');
var CLIENTS_FIELDS = [ 'id', 'appId', 'clientSecret', 'redirectURI', 'scope' ].join(',');
var CLIENTS_FIELDS_PREFIXED = [ 'clients.id', 'clients.appId', 'clients.clientSecret', 'clients.redirectURI', 'clients.scope' ].join(',');
var CLIENTS_FIELDS = [ 'id', 'appId', 'type', 'clientSecret', 'redirectURI', 'scope' ].join(',');
var CLIENTS_FIELDS_PREFIXED = [ 'clients.id', 'clients.appId', 'clients.type', 'clients.clientSecret', 'clients.redirectURI', 'clients.scope' ].join(',');
function get(id, callback) {
assert.strictEqual(typeof id, 'string');
@@ -67,37 +75,33 @@ function getByAppId(appId, callback) {
});
}
function add(id, appId, clientSecret, redirectURI, scope, callback) {
assert.strictEqual(typeof id, 'string');
function getByAppIdAndType(appId, type, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof clientSecret, 'string');
assert.strictEqual(typeof redirectURI, 'string');
assert.strictEqual(typeof scope, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
var data = [ id, appId, clientSecret, redirectURI, scope ];
database.query('SELECT ' + CLIENTS_FIELDS + ' FROM clients WHERE appId = ? AND type = ? LIMIT 1', [ appId, type ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
database.query('INSERT INTO clients (id, appId, clientSecret, redirectURI, scope) VALUES (?, ?, ?, ?, ?)', data, function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS));
if (error || result.affectedRows === 0) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
return callback(null, result[0]);
});
}
function update(id, appId, clientSecret, redirectURI, scope, callback) {
function add(id, appId, type, clientSecret, redirectURI, scope, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof clientSecret, 'string');
assert.strictEqual(typeof redirectURI, 'string');
assert.strictEqual(typeof scope, 'string');
assert.strictEqual(typeof callback, 'function');
var data = [ appId, clientSecret, redirectURI, scope, id ];
var data = [ id, appId, type, clientSecret, redirectURI, scope ];
database.query('UPDATE clients SET appId = ?, clientSecret = ?, redirectURI = ?, scope = ? WHERE id = ?', data, function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
database.query('INSERT INTO clients (id, appId, type, clientSecret, redirectURI, scope) VALUES (?, ?, ?, ?, ?, ?)', data, function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS));
if (error || result.affectedRows === 0) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
@@ -127,6 +131,19 @@ function delByAppId(appId, callback) {
});
}
function delByAppIdAndType(appId, type, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM clients WHERE appId=? AND type=?', [ appId, type ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
return callback(null);
});
}
function clear(callback) {
assert.strictEqual(typeof callback, 'function');
+14 -54
View File
@@ -5,7 +5,6 @@ exports = module.exports = {
add: add,
get: get,
update: update,
del: del,
getAllWithDetailsByUserId: getAllWithDetailsByUserId,
getClientTokensByUserId: getClientTokensByUserId,
@@ -43,6 +42,7 @@ function ClientsError(reason, errorOrMessage) {
}
util.inherits(ClientsError, Error);
ClientsError.INVALID_SCOPE = 'Invalid scope';
ClientsError.INVALID_CLIENT = 'Invalid client';
function validateScope(scope) {
assert.strictEqual(typeof scope, 'string');
@@ -55,8 +55,9 @@ function validateScope(scope) {
return null;
}
function add(appIdentifier, redirectURI, scope, callback) {
assert.strictEqual(typeof appIdentifier, 'string');
function add(appId, type, redirectURI, scope, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof redirectURI, 'string');
assert.strictEqual(typeof scope, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -67,12 +68,13 @@ function add(appIdentifier, redirectURI, scope, callback) {
var id = 'cid-' + uuid.v4();
var clientSecret = hat(256);
clientdb.add(id, appIdentifier, clientSecret, redirectURI, scope, function (error) {
clientdb.add(id, appId, type, clientSecret, redirectURI, scope, function (error) {
if (error) return callback(error);
var client = {
id: id,
appId: appIdentifier,
appId: appId,
type: type,
clientSecret: clientSecret,
redirectURI: redirectURI,
scope: scope
@@ -92,23 +94,6 @@ function get(id, callback) {
});
}
// we only allow appIdentifier and redirectURI to be updated
function update(id, appIdentifier, redirectURI, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof appIdentifier, 'string');
assert.strictEqual(typeof redirectURI, 'string');
assert.strictEqual(typeof callback, 'function');
clientdb.get(id, function (error, result) {
if (error) return callback(error);
clientdb.update(id, appIdentifier, result.clientSecret, redirectURI, result.scope, function (error, result) {
if (error) return callback(error);
callback(null, result);
});
});
}
function del(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -127,54 +112,29 @@ function getAllWithDetailsByUserId(userId, callback) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, []);
if (error) return callback(error);
// We have several types of records here
// 1) webadmin has an app id of 'webadmin'
// 2) oauth proxy records are always the app id prefixed with 'proxy-'
// 3) addon oauth records for apps prefixed with 'addon-'
// 4) external app records prefixed with 'external-'
// 5) normal apps on the cloudron without a prefix
var tmp = [];
async.each(results, function (record, callback) {
if (record.appId === constants.ADMIN_CLIENT_ID) {
if (record.type === clientdb.TYPE_ADMIN) {
record.name = constants.ADMIN_NAME;
record.location = constants.ADMIN_LOCATION;
record.type = 'webadmin';
tmp.push(record);
return callback(null);
} else if (record.appId === constants.TEST_CLIENT_ID) {
record.name = constants.TEST_NAME;
record.location = constants.TEST_LOCATION;
record.type = 'test';
tmp.push(record);
return callback(null);
}
var appId = record.appId;
var type = 'app';
// Handle our different types of oauth clients
if (record.appId.indexOf('addon-') === 0) {
appId = record.appId.slice('addon-'.length);
type = 'addon';
} else if (record.appId.indexOf('proxy-') === 0) {
appId = record.appId.slice('proxy-'.length);
type = 'proxy';
}
appdb.get(appId, function (error, result) {
appdb.get(record.appId, function (error, result) {
if (error) {
console.error('Failed to get app details for oauth client', result, error);
return callback(null); // ignore error so we continue listing clients
}
record.name = result.manifest.title + (record.appId.indexOf('proxy-') === 0 ? 'OAuth Proxy' : '');
if (record.type === clientdb.TYPE_PROXY) record.name = result.manifest.title + ' Website Proxy';
if (record.type === clientdb.TYPE_OAUTH) record.name = result.manifest.title + ' OAuth';
if (record.type === clientdb.TYPE_SIMPLE_AUTH) record.name = result.manifest.title + ' Simple Auth';
if (record.type === clientdb.TYPE_EXTERNAL) record.name = result.manifest.title + ' external';
record.location = result.location;
record.type = type;
tmp.push(record);
+45 -57
View File
@@ -19,7 +19,8 @@ exports = module.exports = {
reboot: reboot,
migrate: migrate,
backup: backup,
ensureBackup: ensureBackup};
ensureBackup: ensureBackup
};
var apps = require('./apps.js'),
AppsError = require('./apps.js').AppsError,
@@ -37,7 +38,6 @@ var apps = require('./apps.js'),
progress = require('./progress.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
SettingsError = settings.SettingsError,
shell = require('./shell.js'),
subdomains = require('./subdomains.js'),
superagent = require('superagent'),
@@ -138,7 +138,7 @@ function setTimeZone(ip, callback) {
}
if (!result.body.timezone) {
debug('No timezone in geoip response');
debug('No timezone in geoip response : %j', result.body);
return callback(null);
}
@@ -148,43 +148,35 @@ function setTimeZone(ip, callback) {
});
}
function activate(username, password, email, name, ip, callback) {
function activate(username, password, email, ip, callback) {
assert.strictEqual(typeof username, 'string');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof email, 'string');
assert.strictEqual(typeof ip, 'string');
assert(!name || typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
debug('activating user:%s email:%s', username, email);
setTimeZone(ip, function () { });
setTimeZone(ip, function () { }); // TODO: get this from user. note that timezone is detected based on the browser location and not the cloudron region
if (!name) name = settings.getDefaultSync(settings.CLOUDRON_NAME_KEY);
settings.setCloudronName(name, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_NAME));
user.createOwner(username, password, email, function (error, userObject) {
if (error && error.reason === UserError.ALREADY_EXISTS) return callback(new CloudronError(CloudronError.ALREADY_PROVISIONED));
if (error && error.reason === UserError.BAD_USERNAME) return callback(new CloudronError(CloudronError.BAD_USERNAME));
if (error && error.reason === UserError.BAD_PASSWORD) return callback(new CloudronError(CloudronError.BAD_PASSWORD));
if (error && error.reason === UserError.BAD_EMAIL) return callback(new CloudronError(CloudronError.BAD_EMAIL));
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
user.createOwner(username, password, email, function (error, userObject) {
if (error && error.reason === UserError.ALREADY_EXISTS) return callback(new CloudronError(CloudronError.ALREADY_PROVISIONED));
if (error && error.reason === UserError.BAD_USERNAME) return callback(new CloudronError(CloudronError.BAD_USERNAME));
if (error && error.reason === UserError.BAD_PASSWORD) return callback(new CloudronError(CloudronError.BAD_PASSWORD));
if (error && error.reason === UserError.BAD_EMAIL) return callback(new CloudronError(CloudronError.BAD_EMAIL));
clientdb.getByAppIdAndType('webadmin', clientdb.TYPE_ADMIN, function (error, result) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
clientdb.getByAppId('webadmin', function (error, result) {
// Also generate a token so the admin creation can also act as a login
var token = tokendb.generateToken();
var expires = Date.now() + 24 * 60 * 60 * 1000; // 1 day
tokendb.add(token, tokendb.PREFIX_USER + userObject.id, result.id, expires, '*', function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
// Also generate a token so the admin creation can also act as a login
var token = tokendb.generateToken();
var expires = Date.now() + 24 * 60 * 60 * 1000; // 1 day
tokendb.add(token, tokendb.PREFIX_USER + userObject.id, result.id, expires, '*', function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
callback(null, { token: token, expires: expires });
});
callback(null, { token: token, expires: expires });
});
});
});
@@ -323,31 +315,23 @@ function addDnsRecords() {
function checkIfInSync() {
debug('addDnsRecords: Check if admin DNS record is in sync.');
var allDone = true;
async.each(changeIds, function (changeId, callback) {
async.eachSeries(changeIds, function (changeId, callback) {
subdomains.status(changeId, function (error, result) {
if (error) return callback(new Error('Failed to check if admin DNS record is in sync.', error));
if (result !== 'done') allDone = false;
if (result !== 'done') return callback(new Error(changeId + ' is not in sync. result:' + result));
callback(null);
});
}, function (error) {
if (error) console.error(error);
// retry if needed
if (error || !allDone) {
if (error) {
debug(error.message);
gAddDnsRecordsTimerId = setTimeout(checkIfInSync, 5000);
return;
}
config.set('dnsInSync', true);
// send heartbeat after the dns records are done
sendHeartbeat();
debug('addDnsRecords: done');
config.set('dnsInSync', true);
sendHeartbeat(); // send heartbeat after the dns records are done
});
}
@@ -463,7 +447,7 @@ function doUpgrade(boxUpdateInfo, callback) {
callback(e);
}
progress.set(progress.UPDATE, 5, 'Create app and box backup for upgrade');
progress.set(progress.UPDATE, 5, 'Backing up for upgrade');
backupBoxAndApps(function (error) {
if (error) return upgradeError(error);
@@ -473,7 +457,7 @@ function doUpgrade(boxUpdateInfo, callback) {
.send({ version: boxUpdateInfo.version })
.end(function (error, result) {
if (error) return upgradeError(new Error('Error making upgrade request: ' + error));
if (result.status !== 202) return upgradeError(new Error('Server not ready to upgrade: ' + result.body));
if (result.status !== 202) return upgradeError(new Error(util.format('Server not ready to upgrade. statusCode: %s body: %j', result.status, result.body)));
progress.set(progress.UPDATE, 10, 'Updating base system');
@@ -492,9 +476,9 @@ function doUpdate(boxUpdateInfo, callback) {
callback(e);
}
progress.set(progress.UPDATE, 5, 'Create box backup for update');
progress.set(progress.UPDATE, 5, 'Backing up for update');
backupBox(function (error) {
backupBoxAndApps(function (error) {
if (error) return updateError(error);
// fetch a signed sourceTarballUrl
@@ -503,7 +487,7 @@ function doUpdate(boxUpdateInfo, callback) {
.end(function (error, result) {
if (error) return updateError(new Error('Error fetching sourceTarballUrl: ' + error));
if (result.status !== 200) return updateError(new Error('Error fetching sourceTarballUrl status: ' + result.status));
if (!safe.query(result, 'body.url')) return updateError(new Error('Error fetching sourceTarballUrl response: ' + result.body));
if (!safe.query(result, 'body.url')) return updateError(new Error('Error fetching sourceTarballUrl response: ' + JSON.stringify(result.body)));
// NOTE: the args here are tied to the installer revision, box code and appstore provisioning logic
var args = {
@@ -511,18 +495,19 @@ function doUpdate(boxUpdateInfo, callback) {
// this data is opaque to the installer
data: {
boxVersionsUrl: config.get('boxVersionsUrl'),
version: boxUpdateInfo.version,
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin(),
aws: config.aws(),
backupKey: config.backupKey(),
boxVersionsUrl: config.get('boxVersionsUrl'),
fqdn: config.fqdn(),
token: config.token(),
tlsCert: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), 'utf8'),
tlsKey: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), 'utf8'),
isCustomDomain: config.isCustomDomain(),
restoreUrl: null,
restoreKey: null,
aws: config.aws()
token: config.token(),
tlsCert: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), 'utf8'),
tlsKey: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), 'utf8'),
version: boxUpdateInfo.version,
webServerOrigin: config.webServerOrigin()
}
};
@@ -530,7 +515,7 @@ function doUpdate(boxUpdateInfo, callback) {
superagent.post(INSTALLER_UPDATE_URL).send(args).end(function (error, result) {
if (error) return updateError(error);
if (result.status !== 202) return updateError(new Error('Error initiating update: ' + result.body));
if (result.status !== 202) return updateError(new Error('Error initiating update: ' + JSON.stringify(result.body)));
progress.set(progress.UPDATE, 10, 'Updating cloudron software');
@@ -548,6 +533,9 @@ function backup(callback) {
var error = locker.lock(locker.OP_FULL_BACKUP);
if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message));
// clearing backup ensures tools can 'wait' on progress
progress.clear(progress.BACKUP);
// start the backup operation in the background
backupBoxAndApps(function (error) {
if (error) console.error('backup failed.', error);
@@ -632,12 +620,12 @@ function backupBoxAndApps(callback) {
apps.backupApp(app, app.manifest.addons, function (error, backupId) {
progress.set(progress.BACKUP, step * processed, 'Backed up app at ' + app.location);
if (error && error.reason === AppsError.BAD_STATE) {
debugApp(app, 'Skipping backup (istate:%s health:%s). using lastBackupId:%s', app.installationState, app.health, app.lastBackupId);
backupId = app.lastBackupId;
if (error && error.reason !== AppsError.BAD_STATE) {
debugApp(app, 'Unable to backup', error);
return iteratorCallback(error);
}
return iteratorCallback(null, backupId);
iteratorCallback(null, backupId || null); // clear backupId if is in BAD_STATE and never backed up
});
}, function appsBackedUp(error, backupIds) {
if (error) {
@@ -645,7 +633,7 @@ function backupBoxAndApps(callback) {
return callback(error);
}
backupIds = backupIds.filter(function (id) { return id !== null; }); // remove apps that were never backed up
backupIds = backupIds.filter(function (id) { return id !== null; }); // remove apps in bad state that were never backed up
backupBoxWithAppBackupIds(backupIds, function (error, restoreKey) {
progress.set(progress.BACKUP, 100, error ? error.message : '');
+9
View File
@@ -25,6 +25,7 @@ exports = module.exports = {
// these values are derived
adminOrigin: adminOrigin,
internalAdminOrigin: internalAdminOrigin,
appFqdn: appFqdn,
zoneName: zoneName,
@@ -73,6 +74,8 @@ function initConfig() {
data.webServerOrigin = null;
data.internalPort = 3001;
data.ldapPort = 3002;
data.oauthProxyPort = 3003;
data.simpleAuthPort = 3004;
data.backupKey = 'backupKey';
data.aws = {
backupBucket: null,
@@ -98,6 +101,8 @@ function initConfig() {
};
data.token = 'APPSTORE_TOKEN';
data.aws.backupBucket = 'testbucket';
data.aws.backupPrefix = 'testprefix';
data.aws.endpoint = 'http://localhost:5353';
} else {
assert(false, 'Unknown environment. This should not happen!');
}
@@ -160,6 +165,10 @@ function adminOrigin() {
return 'https://' + appFqdn(constants.ADMIN_LOCATION);
}
function internalAdminOrigin() {
return 'http://127.0.0.1:' + get('port');
}
function token() {
return get('token');
}
+1 -5
View File
@@ -7,10 +7,6 @@ exports = module.exports = {
ADMIN_NAME: 'Settings',
ADMIN_CLIENT_ID: 'webadmin', // oauth client id
ADMIN_APPID: 'admin', // admin appid (settingsdb)
TEST_NAME: 'Test',
TEST_LOCATION: '',
TEST_CLIENT_ID: 'test'
ADMIN_APPID: 'admin' // admin appid (settingsdb)
};
+43 -3
View File
@@ -8,8 +8,11 @@ exports = module.exports = {
var apps = require('./apps.js'),
assert = require('assert'),
cloudron = require('./cloudron.js'),
config = require('./config.js'),
CronJob = require('cron').CronJob,
debug = require('debug')('box:cron'),
janitor = require('./janitor.js'),
scheduler = require('./scheduler.js'),
settings = require('./settings.js'),
updateChecker = require('./updatechecker.js');
@@ -17,7 +20,10 @@ var gAutoupdaterJob = null,
gBoxUpdateCheckerJob = null,
gAppUpdateCheckerJob = null,
gHeartbeatJob = null,
gBackupJob = null;
gBackupJob = null,
gCleanupTokensJob = null,
gDockerVolumeCleanerJob = null,
gSchedulerSyncJob = null;
var gInitialized = false;
@@ -48,6 +54,8 @@ function recreateJobs(unusedTimeZone, callback) {
if (typeof unusedTimeZone === 'function') callback = unusedTimeZone;
settings.getAll(function (error, allSettings) {
debug('Creating jobs with timezone %s', allSettings[settings.TIME_ZONE_KEY]);
if (gHeartbeatJob) gHeartbeatJob.stop();
gHeartbeatJob = new CronJob({
cronTime: '00 */1 * * * *', // every minute
@@ -80,6 +88,30 @@ function recreateJobs(unusedTimeZone, callback) {
timeZone: allSettings[settings.TIME_ZONE_KEY]
});
if (gCleanupTokensJob) gCleanupTokensJob.stop();
gCleanupTokensJob = new CronJob({
cronTime: '00 */30 * * * *', // every 30 minutes
onTick: janitor.cleanupTokens,
start: true,
timeZone: allSettings[settings.TIME_ZONE_KEY]
});
if (gDockerVolumeCleanerJob) gDockerVolumeCleanerJob.stop();
gDockerVolumeCleanerJob = new CronJob({
cronTime: '00 00 */12 * * *', // every 12 hours
onTick: janitor.cleanupDockerVolumes,
start: true,
timeZone: allSettings[settings.TIME_ZONE_KEY]
});
if (gSchedulerSyncJob) gSchedulerSyncJob.stop();
gSchedulerSyncJob = new CronJob({
cronTime: config.TEST ? '*/10 * * * * *' : '00 */1 * * * *', // every minute
onTick: scheduler.sync,
start: true,
timeZone: allSettings[settings.TIME_ZONE_KEY]
});
autoupdatePatternChanged(allSettings[settings.AUTOUPDATE_PATTERN_KEY]);
if (callback) callback();
@@ -110,7 +142,7 @@ function autoupdatePatternChanged(pattern) {
}
},
start: true,
timeZone: gBoxUpdateCheckerJob.cronTime.timeZone // hack
timeZone: gBoxUpdateCheckerJob.cronTime.zone // hack
});
}
@@ -134,8 +166,16 @@ function uninitialize(callback) {
gBackupJob.stop();
gBackupJob = null;
gCleanupTokensJob.stop();
gCleanupTokensJob = null;
gDockerVolumeCleanerJob.stop();
gDockerVolumeCleanerJob = null;
gSchedulerSyncJob.stop();
gSchedulerSyncJob = null;
gInitialized = false;
callback();
}
+1 -1
View File
@@ -65,7 +65,7 @@ function issueDeveloperToken(user, callback) {
var token = tokendb.generateToken();
var expiresAt = Date.now() + 24 * 60 * 60 * 1000; // 1 day
tokendb.add(token, tokendb.PREFIX_DEV + user.id, '', expiresAt, 'apps,settings,roleDeveloper', function (error) {
tokendb.add(token, tokendb.PREFIX_DEV + user.id, '', expiresAt, 'developer,apps,settings,users', function (error) {
if (error) return callback(new DeveloperError(DeveloperError.INTERNAL_ERROR, error));
callback(null, { token: token, expiresAt: expiresAt });
+326 -28
View File
@@ -1,42 +1,340 @@
'use strict';
var Docker = require('dockerode'),
fs = require('fs'),
os = require('os'),
path = require('path'),
url = require('url');
var addons = require('./addons.js'),
async = require('async'),
assert = require('assert'),
config = require('./config.js'),
debug = require('debug')('box:src/docker.js'),
Docker = require('dockerode'),
safe = require('safetydance'),
semver = require('semver'),
util = require('util');
exports = module.exports = (function () {
exports = module.exports = {
connection: connectionInstance(),
downloadImage: downloadImage,
createContainer: createContainer,
startContainer: startContainer,
stopContainer: stopContainer,
stopContainers: stopContainers,
deleteContainer: deleteContainer,
deleteImage: deleteImage,
deleteContainers: deleteContainers,
createSubcontainer: createSubcontainer
};
function connectionInstance() {
var docker;
var options = connectOptions(); // the real docker
if (process.env.BOX_ENV === 'test') {
// test code runs a docker proxy on this port
docker = new Docker({ host: 'http://localhost', port: 5687 });
// proxy code uses this to route to the real docker
docker.options = { socketPath: '/var/run/docker.sock' };
} else {
docker = new Docker(options);
docker = new Docker({ socketPath: '/var/run/docker.sock' });
}
// proxy code uses this to route to the real docker
docker.options = options;
return docker;
})();
function connectOptions() {
if (os.platform() === 'linux') return { socketPath: '/var/run/docker.sock' };
// boot2docker configuration
var DOCKER_CERT_PATH = process.env.DOCKER_CERT_PATH || path.join(process.env.HOME, '.boot2docker/certs/boot2docker-vm');
var DOCKER_HOST = process.env.DOCKER_HOST || 'tcp://192.168.59.103:2376';
return {
protocol: 'https',
host: url.parse(DOCKER_HOST).hostname,
port: url.parse(DOCKER_HOST).port,
ca: fs.readFileSync(path.join(DOCKER_CERT_PATH, 'ca.pem')),
cert: fs.readFileSync(path.join(DOCKER_CERT_PATH, 'cert.pem')),
key: fs.readFileSync(path.join(DOCKER_CERT_PATH, 'key.pem'))
};
}
function debugApp(app, args) {
assert(!app || typeof app === 'object');
var prefix = app ? (app.location || '(bare)') : '(no app)';
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
function targetBoxVersion(manifest) {
if ('targetBoxVersion' in manifest) return manifest.targetBoxVersion;
if ('minBoxVersion' in manifest) return manifest.minBoxVersion;
return '0.0.1';
}
function pullImage(manifest, callback) {
var docker = exports.connection;
docker.pull(manifest.dockerImage, function (err, stream) {
if (err) return callback(new Error('Error connecting to docker. statusCode: %s' + err.statusCode));
// 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 data: %j', data);
// The information here is useless because this is per layer as opposed to per image
if (data.status) {
// debugApp(app, 'progress: %s', data.status); // progressDetail { current, total }
} else if (data.error) {
debug('pullImage error detail: %s', data.errorDetail.message);
}
});
stream.on('end', function () {
debug('downloaded image %s successfully', manifest.dockerImage);
var image = docker.getImage(manifest.dockerImage);
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'));
debug('This image exposes ports: %j', data.Config.ExposedPorts);
callback(null);
});
});
stream.on('error', function (error) {
debug('error pulling image %s : %j', manifest.dockerImage, error);
callback(error);
});
});
}
function downloadImage(manifest, callback) {
assert.strictEqual(typeof manifest, 'object');
assert.strictEqual(typeof callback, 'function');
debug('downloadImage %s', manifest.dockerImage);
var attempt = 1;
async.retry({ times: 5, interval: 15000 }, function (retryCallback) {
debug('Downloading image. attempt: %s', attempt++);
pullImage(manifest, function (error) {
if (error) console.error(error);
retryCallback(error);
});
}, callback);
}
function createSubcontainer(app, cmd, callback) {
assert.strictEqual(typeof app, 'object');
assert(!cmd || util.isArray(cmd));
assert.strictEqual(typeof callback, 'function');
var docker = exports.connection,
isAppContainer = !cmd;
var manifest = app.manifest;
var exposedPorts = {}, dockerPortBindings = { };
var stdEnv = [
'CLOUDRON=1',
'WEBADMIN_ORIGIN' + '=' + config.adminOrigin(),
'API_ORIGIN' + '=' + config.adminOrigin()
];
// docker portBindings requires ports to be exposed
exposedPorts[manifest.httpPort + '/tcp'] = {};
dockerPortBindings[manifest.httpPort + '/tcp'] = [ { HostIp: '127.0.0.1', HostPort: app.httpPort + '' } ];
var portEnv = [];
for (var e in app.portBindings) {
var hostPort = app.portBindings[e];
var containerPort = manifest.tcpPorts[e].containerPort || hostPort;
exposedPorts[containerPort + '/tcp'] = {};
portEnv.push(e + '=' + hostPort);
dockerPortBindings[containerPort + '/tcp'] = [ { HostIp: '0.0.0.0', HostPort: hostPort + '' } ];
}
var memoryLimit = manifest.memoryLimit || 1024 * 1024 * 200; // 200mb by default
addons.getEnvironment(app, function (error, addonEnv) {
if (error) return callback(new Error('Error getting addon environment : ' + error));
var containerOptions = {
Hostname: config.appFqdn(app.location),
Tty: isAppContainer,
Image: app.manifest.dockerImage,
Cmd: cmd,
Env: stdEnv.concat(addonEnv).concat(portEnv),
ExposedPorts: isAppContainer ? exposedPorts : { },
Volumes: { // see also ReadonlyRootfs
'/tmp': {},
'/run': {}
},
Labels: {
"location": app.location,
"appId": app.id,
"isSubcontainer": String(!isAppContainer)
},
HostConfig: {
Binds: addons.getBindsSync(app, app.manifest.addons),
Memory: memoryLimit / 2,
MemorySwap: memoryLimit, // Memory + Swap
PortBindings: isAppContainer ? dockerPortBindings : { },
PublishAllPorts: false,
ReadonlyRootfs: semver.gte(targetBoxVersion(app.manifest), '0.0.66'), // see also Volumes in startContainer
Links: addons.getLinksSync(app, app.manifest.addons),
RestartPolicy: {
"Name": isAppContainer ? "always" : "no",
"MaximumRetryCount": 0
},
CpuShares: 512, // relative to 1024 for system processes
VolumesFrom: isAppContainer ? null : [ app.containerId + ":rw" ],
SecurityOpt: config.CLOUDRON ? [ "apparmor:docker-cloudron-app" ] : null // profile available only on cloudron
}
};
// older versions wanted a writable /var/log
if (semver.lte(targetBoxVersion(app.manifest), '0.0.71')) containerOptions.Volumes['/var/log'] = {};
debugApp(app, 'Creating container for %s', app.manifest.dockerImage);
docker.createContainer(containerOptions, callback);
});
}
function createContainer(app, callback) {
createSubcontainer(app, null, callback);
}
function startContainer(containerId, callback) {
assert.strictEqual(typeof containerId, 'string');
assert.strictEqual(typeof callback, 'function');
var docker = exports.connection;
var container = docker.getContainer(containerId);
debug('Starting container %s', containerId);
container.start(function (error) {
if (error && error.statusCode !== 304) return callback(new Error('Error starting container :' + error));
return callback(null);
});
}
function stopContainer(containerId, callback) {
assert(!containerId || typeof containerId === 'string');
assert.strictEqual(typeof callback, 'function');
if (!containerId) {
debug('No previous container to stop');
return callback();
}
var docker = exports.connection;
var container = docker.getContainer(containerId);
debug('Stopping container %s', containerId);
var options = {
t: 10 // wait for 10 seconds before killing it
};
container.stop(options, function (error) {
if (error && (error.statusCode !== 304 && error.statusCode !== 404)) return callback(new Error('Error stopping container:' + error));
debug('Waiting for container ' + containerId);
container.wait(function (error, data) {
if (error && (error.statusCode !== 304 && error.statusCode !== 404)) return callback(new Error('Error waiting on container:' + error));
debug('Container %s stopped with status code [%s]', containerId, data ? String(data.StatusCode) : '');
return callback(null);
});
});
}
function deleteContainer(containerId, callback) {
assert(!containerId || typeof containerId === 'string');
assert.strictEqual(typeof callback, 'function');
debug('deleting container %s', containerId);
if (containerId === null) return callback(null);
var docker = exports.connection;
var container = docker.getContainer(containerId);
var removeOptions = {
force: true, // kill container if it's running
v: true // removes volumes associated with the container (but not host mounts)
};
container.remove(removeOptions, function (error) {
if (error && error.statusCode === 404) return callback(null);
if (error) debug('Error removing container %s : %j', containerId, error);
callback(error);
});
}
function deleteContainers(appId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
var docker = exports.connection;
debug('deleting containers of %s', appId);
docker.listContainers({ all: 1, filters: JSON.stringify({ label: [ 'appId=' + appId ] }) }, function (error, containers) {
if (error) return callback(error);
async.eachSeries(containers, function (container, iteratorDone) {
deleteContainer(container.Id, iteratorDone);
}, callback);
});
}
function stopContainers(appId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
var docker = exports.connection;
debug('stopping containers of %s', appId);
docker.listContainers({ all: 1, filters: JSON.stringify({ label: [ 'appId=' + appId ] }) }, function (error, containers) {
if (error) return callback(error);
async.eachSeries(containers, function (container, iteratorDone) {
stopContainer(container.Id, iteratorDone);
}, callback);
});
}
function deleteImage(manifest, callback) {
assert(!manifest || typeof manifest === 'object');
assert.strictEqual(typeof callback, 'function');
var dockerImage = manifest ? manifest.dockerImage : null;
if (!dockerImage) return callback(null);
var docker = exports.connection;
docker.getImage(dockerImage).inspect(function (error, result) {
if (error && error.statusCode === 404) return callback(null);
if (error) return callback(error);
var removeOptions = {
force: true,
noprune: false
};
// delete image by id because 'docker pull' pulls down all the tags and this is the only way to delete all tags
docker.getImage(result.Id).remove(removeOptions, function (error) {
if (error && error.statusCode === 404) return callback(null);
if (error && error.statusCode === 409) return callback(null); // another container using the image
if (error) debug('Error removing image %s : %j', dockerImage, error);
callback(error);
});
});
}
+103
View File
@@ -0,0 +1,103 @@
'use strict';
var assert = require('assert'),
async = require('async'),
authcodedb = require('./authcodedb.js'),
debug = require('debug')('box:src/janitor'),
docker = require('./docker.js').connection,
tokendb = require('./tokendb.js');
exports = module.exports = {
cleanupTokens: cleanupTokens,
cleanupDockerVolumes: cleanupDockerVolumes
};
var NOOP_CALLBACK = function () { };
function ignoreError(func) {
return function (callback) {
func(function (error) {
if (error) console.error('Ignored error:', error);
callback();
});
};
}
function cleanupExpiredTokens(callback) {
assert.strictEqual(typeof callback, 'function');
tokendb.delExpired(function (error, result) {
if (error) return callback(error);
debug('Cleaned up %s expired tokens.', result);
callback(null);
});
}
function cleanupExpiredAuthCodes(callback) {
assert.strictEqual(typeof callback, 'function');
authcodedb.delExpired(function (error, result) {
if (error) return callback(error);
debug('Cleaned up %s expired authcodes.', result);
callback(null);
});
}
function cleanupTokens(callback) {
assert(!callback || typeof callback === 'function'); // callback is null when called from cronjob
debug('Cleaning up expired tokens');
async.series([
ignoreError(cleanupExpiredTokens),
ignoreError(cleanupExpiredAuthCodes)
], callback);
}
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
debug('cleanupTmpVolume %j', containerInfo.Names);
docker.getContainer(containerInfo.Id).exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true, Tty: false }, function (error, execContainer) {
if (error) return callback(new Error('Failed to exec container : ' + error.message));
execContainer.start(function(err, stream) {
if (error) return callback(new Error('Failed to start exec container : ' + error.message));
stream.on('error', callback);
stream.on('end', callback);
stream.setEncoding('utf8');
stream.pipe(process.stdout);
});
});
}
function cleanupDockerVolumes(callback) {
assert(!callback || typeof callback === 'function'); // callback is null when called from cronjob
callback = callback || NOOP_CALLBACK;
debug('Cleaning up docker volumes');
docker.listContainers({ all: 0 }, function (error, containers) {
if (error) return callback(error);
async.eachSeries(containers, function (container, iteratorDone) {
cleanupTmpVolume(container, function (error) {
if (error) debug('Error cleaning tmp: %s', error);
iteratorDone(); // intentionally ignore error
});
}, callback);
});
}
+19 -6
View File
@@ -1,7 +1,8 @@
'use strict';
exports = module.exports = {
start: start
start: start,
stop: stop
};
var assert = require('assert'),
@@ -28,7 +29,7 @@ var GROUP_USERS_DN = 'cn=users,ou=groups,dc=cloudron';
var GROUP_ADMINS_DN = 'cn=admins,ou=groups,dc=cloudron';
function start(callback) {
assert(typeof callback === 'function');
assert.strictEqual(typeof callback, 'function');
gServer = ldap.createServer({ log: gLogger });
@@ -62,7 +63,6 @@ function start(callback) {
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && req.filter.matches(tmp.attributes)) {
res.send(tmp);
debug('ldap user send:', tmp);
}
});
@@ -99,7 +99,6 @@ function start(callback) {
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && req.filter.matches(tmp.attributes)) {
res.send(tmp);
debug('ldap group send:', tmp);
}
});
@@ -107,8 +106,14 @@ function start(callback) {
});
});
gServer.bind('dc=cloudron', function(req, res, next) {
debug('ldap bind: %s', req.dn.toString());
gServer.bind('ou=apps,dc=cloudron', function(req, res, next) {
// TODO: validate password
debug('ldap application bind: %s', req.dn.toString());
res.end();
});
gServer.bind('ou=users,dc=cloudron', function(req, res, next) {
debug('ldap user bind: %s', req.dn.toString());
if (!req.dn.rdns[0].cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
@@ -123,3 +128,11 @@ function start(callback) {
gServer.listen(config.get('ldapPort'), callback);
}
function stop(callback) {
assert.strictEqual(typeof callback, 'function');
gServer.close();
callback();
}
+2 -2
View File
@@ -2,7 +2,7 @@
Dear Cloudron Team,
unfortunately the <%= program %> on <%= fqdn %> crashed unexpectedly!
Unfortunately <%= program %> on <%= fqdn %> crashed unexpectedly!
Please see some excerpt of the logs below.
@@ -11,7 +11,7 @@ Your Cloudron
-------------------------------------
<%= context %>
<%- context %>
<% } else { %>
+3 -3
View File
@@ -28,7 +28,7 @@ var assert = require('assert'),
config = require('./config.js'),
debug = require('debug')('box:mailer'),
digitalocean = require('./digitalocean.js'),
docker = require('./docker.js'),
docker = require('./docker.js').connection,
ejs = require('ejs'),
nodemailer = require('nodemailer'),
path = require('path'),
@@ -87,13 +87,13 @@ function processQueue() {
var transport = nodemailer.createTransport(smtpTransport({
host: mailServerIp,
port: 25
port: 2500 // this value comes from mail container
}));
var mailQueueCopy = gMailQueue;
gMailQueue = [ ];
debug('Processing mail queue of size %d', mailQueueCopy.length);
debug('Processing mail queue of size %d (through %s:2500)', mailQueueCopy.length, mailServerIp);
async.mapSeries(mailQueueCopy, function iterator(mailOptions, callback) {
transport.sendMail(mailOptions, function (error) {
-8
View File
@@ -1,8 +0,0 @@
'use strict';
module.exports = function contentType(type) {
return function (req, res, next) {
res.setHeader('Content-Type', type);
next();
};
};
-1
View File
@@ -1,7 +1,6 @@
'use strict';
exports = module.exports = {
contentType: require('./contentType'),
cookieParser: require('cookie-parser'),
cors: require('./cors'),
csrf: require('csurf'),
+1 -3
View File
@@ -1,4 +1,4 @@
<% include header %>
<!-- callback tester -->
<script>
@@ -43,5 +43,3 @@ if (code && redirectURI) {
}
</script>
<% include footer %>;
-38
View File
@@ -1,38 +0,0 @@
<% include header %>
<form action="/api/v1/oauth/dialog/authorize/decision" method="post">
<input name="transaction_id" type="hidden" value="<%= transactionID %>">
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
<div class="row">
<div class="col-md-3"></div>
<div class="col-md-6">
<div class="container-fluid">
<div class="row">
<div class="col-sm-12">
Hi <%= user.username %>!
</div>
</div>
<div class="row">
<div class="col-sm-12">
<b><%= client.name %></b> is requesting access to your account.
</div>
</div>
<div class="row">
<div class="col-sm-12">
Do you approve?
</div>
</div>
<div class="row">
<div class="col-sm-12">
<input class="btn btn-danger btn-outline" type="submit" value="Deny" name="cancel" id="deny"/>
<input class="btn btn-success btn-outline" type="submit" value="Allow"/>
</div>
</div>
</div>
</div>
<div class="col-md-3"></div>
</div>
</form>
<% include footer %>
+2
View File
@@ -1,5 +1,7 @@
<% include header %>
<!-- error tester -->
<br/>
<div class="container">
+9
View File
@@ -26,3 +26,12 @@
</head>
<body class="oauth">
<!-- Navigation -->
<nav class="navbar navbar-default navbar-static-top shadow" role="navigation" style="margin-bottom: 0">
<div class="container-fluid">
<div class="navbar-header">
<span class="navbar-brand navbar-brand-icon"><img src="/api/v1/cloudron/avatar" width="40" height="40"/></span>
</div>
</div>
</nav>
+3 -1
View File
@@ -1,5 +1,7 @@
<% include header %>
<!-- login tester -->
<div class="container">
<div class="row">
<div class="col-md-6 col-md-offset-3">
@@ -7,7 +9,7 @@
<div class="row">
<div class="col-md-12" style="text-align: center;">
<img width="128" height="128" src="<%= applicationLogo %>"/>
<h1>Login to <%= applicationName %> on <%= cloudronName %></h1>
<h1><small>Login to</small> <%= applicationName %></h1>
<br/>
</div>
</div>
+192
View File
@@ -0,0 +1,192 @@
'use strict';
exports = module.exports = {
start: start,
stop: stop
};
var appdb = require('./appdb.js'),
assert = require('assert'),
clientdb = require('./clientdb.js'),
config = require('./config.js'),
debug = require('debug')('box:proxy'),
express = require('express'),
http = require('http'),
proxy = require('proxy-middleware'),
session = require('cookie-session'),
superagent = require('superagent'),
url = require('url'),
uuid = require('node-uuid');
var gSessions = {};
var gProxyMiddlewareCache = {};
var gHttpServer = null;
var CALLBACK_URI = '/callback';
function attachSessionData(req, res, next) {
assert.strictEqual(typeof req.session, 'object');
if (!req.session.id || !gSessions[req.session.id]) {
req.session.id = uuid.v4();
gSessions[req.session.id] = {};
}
// attach the session data to the requeset
req.sessionData = gSessions[req.session.id];
next();
}
function verifySession(req, res, next) {
assert.strictEqual(typeof req.sessionData, 'object');
if (!req.sessionData.accessToken) {
req.authenticated = false;
return next();
}
// use http admin origin so that it works with self-signed certs
superagent
.get(config.internalAdminOrigin() + '/api/v1/profile')
.query({ access_token: req.sessionData.accessToken})
.end(function (error, result) {
if (error) {
console.error(error);
req.authenticated = false;
} else if (result.statusCode !== 200) {
req.sessionData.accessToken = null;
req.authenticated = false;
} else {
req.authenticated = true;
}
next();
});
}
function authenticate(req, res, next) {
// proceed if we are authenticated
if (req.authenticated) return next();
if (req.path === CALLBACK_URI && req.sessionData.returnTo) {
// exchange auth code for an access token
var query = {
response_type: 'token',
client_id: req.sessionData.clientId
};
var data = {
grant_type: 'authorization_code',
code: req.query.code,
redirect_uri: req.sessionData.returnTo,
client_id: req.sessionData.clientId,
client_secret: req.sessionData.clientSecret
};
// use http admin origin so that it works with self-signed certs
superagent
.post(config.internalAdminOrigin() + '/api/v1/oauth/token')
.query(query).send(data)
.end(function (error, result) {
if (error) {
console.error(error);
return res.send(500, 'Unable to contact the oauth server.');
}
if (result.statusCode !== 200) {
console.error('Failed to exchange auth code for a token.', result.statusCode, result.body);
return res.send(500, 'Failed to exchange auth code for a token.');
}
req.sessionData.accessToken = result.body.access_token;
debug('user verified.');
// now redirect to the actual initially requested URL
res.redirect(req.sessionData.returnTo);
});
} else {
var port = parseInt(req.headers['x-cloudron-proxy-port'], 10);
if (!Number.isFinite(port)) {
console.error('Failed to parse nginx proxy header to get app port.');
return res.send(500, 'Routing error. No forwarded port.');
}
debug('begin verifying user for app on port %s.', port);
appdb.getByHttpPort(port, function (error, result) {
if (error) {
console.error('Unknown app.', error);
return res.send(500, 'Unknown app.');
}
clientdb.getByAppIdAndType(result.id, clientdb.TYPE_PROXY, function (error, result) {
if (error) {
console.error('Unkonwn OAuth client.', error);
return res.send(500, 'Unknown OAuth client.');
}
req.sessionData.port = port;
req.sessionData.returnTo = result.redirectURI + req.path;
req.sessionData.clientId = result.id;
req.sessionData.clientSecret = result.clientSecret;
var callbackUrl = result.redirectURI + CALLBACK_URI;
var scope = 'profile';
var oauthLogin = config.adminOrigin() + '/api/v1/oauth/dialog/authorize?response_type=code&client_id=' + result.id + '&redirect_uri=' + callbackUrl + '&scope=' + scope;
debug('begin OAuth flow for client %s.', result.name);
// begin the OAuth flow
res.redirect(oauthLogin);
});
});
}
}
function forwardRequestToApp(req, res, next) {
var port = req.sessionData.port;
debug('proxy request for port %s with path %s.', port, req.path);
var proxyMiddleware = gProxyMiddlewareCache[port];
if (!proxyMiddleware) {
console.log('Adding proxy middleware for port %d', port);
proxyMiddleware = proxy(url.parse('http://127.0.0.1:' + port));
gProxyMiddlewareCache[port] = proxyMiddleware;
}
proxyMiddleware(req, res, next);
}
function initializeServer() {
var app = express();
var httpServer = http.createServer(app);
httpServer.on('error', console.error);
app
.use(session({ keys: ['blue', 'cheese', 'is', 'something'] }))
.use(attachSessionData)
.use(verifySession)
.use(authenticate)
.use(forwardRequestToApp);
return httpServer;
}
function start(callback) {
assert.strictEqual(typeof callback, 'function');
gHttpServer = initializeServer();
gHttpServer.listen(config.get('oauthProxyPort'), callback);
}
function stop(callback) {
assert.strictEqual(typeof callback, 'function');
gHttpServer.close(callback);
}
+4 -1
View File
@@ -12,6 +12,7 @@ exports = module.exports = {
NGINX_CERT_DIR: path.join(config.baseDir(), 'data/nginx/cert'),
ADDON_CONFIG_DIR: path.join(config.baseDir(), 'data/addons'),
SCHEDULER_FILE: path.join(config.baseDir(), 'data/addons/scheduler.json'),
COLLECTD_APPCONFIG_DIR: path.join(config.baseDir(), 'data/collectd/collectd.conf.d'),
@@ -24,5 +25,7 @@ exports = module.exports = {
CLOUDRON_AVATAR_FILE: path.join(config.baseDir(), 'data/box/avatar.png'),
CLOUDRON_DEFAULT_AVATAR_FILE: path.join(__dirname + '/../assets/avatar.png'),
FAVICON_FILE: path.join(__dirname + '/../assets/favicon.ico')
FAVICON_FILE: path.join(__dirname + '/../assets/favicon.ico'),
UPDATE_CHECKER_FILE: path.join(config.baseDir(), 'data/box/updatechecker.json')
};
+11 -7
View File
@@ -43,6 +43,7 @@ function removeInternalAppFields(app) {
health: app.health,
location: app.location,
accessRestriction: app.accessRestriction,
oauthProxy: app.oauthProxy,
lastBackupId: app.lastBackupId,
manifest: app.manifest,
portBindings: app.portBindings,
@@ -113,20 +114,22 @@ function installApp(req, res, next) {
if (typeof data.appStoreId !== 'string') return next(new HttpError(400, 'appStoreId is required'));
if (typeof data.location !== 'string') return next(new HttpError(400, 'location is required'));
if (('portBindings' in data) && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
if (typeof data.accessRestriction !== 'string') return next(new HttpError(400, 'accessRestriction is required'));
if (typeof data.accessRestriction !== 'object') return next(new HttpError(400, 'accessRestriction is required'));
if (typeof data.oauthProxy !== 'boolean') return next(new HttpError(400, 'oauthProxy must be a boolean'));
if ('icon' in data && typeof data.icon !== 'string') return next(new HttpError(400, 'icon is not a string'));
// allow tests to provide an appId for testing
var appId = (process.env.BOX_ENV === 'test' && typeof data.appId === 'string') ? data.appId : uuid.v4();
debug('Installing app id:%s storeid:%s loc:%s port:%j restrict:%s manifest:%j', appId, data.appStoreId, data.location, data.portBindings, data.accessRestriction, data.manifest);
debug('Installing app id:%s storeid:%s loc:%s port:%j accessRestriction:%j oauthproxy:%s manifest:%j', appId, data.appStoreId, data.location, data.portBindings, data.accessRestriction, data.oauthProxy, data.manifest);
apps.install(appId, data.appStoreId, data.manifest, data.location, data.portBindings || null, data.accessRestriction, data.icon || null, function (error) {
apps.install(appId, data.appStoreId, data.manifest, data.location, data.portBindings || null, data.accessRestriction, data.oauthProxy, data.icon || null, 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.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.BILLING_REQUIRED) return next(new HttpError(402, 'Billing required'));
if (error && error.reason === AppsError.USER_REQUIRED) return next(new HttpError(400, 'accessRestriction must specify one user'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, { id: appId } ));
@@ -149,11 +152,12 @@ function configureApp(req, res, next) {
if (!data) return next(new HttpError(400, 'Cannot parse data field'));
if (typeof data.location !== 'string') return next(new HttpError(400, 'location is required'));
if (('portBindings' in data) && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
if (typeof data.accessRestriction !== 'string') return next(new HttpError(400, 'accessRestriction is required'));
if (typeof data.accessRestriction !== 'object') return next(new HttpError(400, 'accessRestriction is required'));
if (typeof data.oauthProxy !== 'boolean') return next(new HttpError(400, 'oauthProxy must be a boolean'));
debug('Configuring app id:%s location:%s bindings:%j', req.params.id, data.location, data.portBindings);
debug('Configuring app id:%s location:%s bindings:%j accessRestriction:%j oauthProxy:%s', req.params.id, data.location, data.portBindings, data.accessRestriction, data.oauthProxy);
apps.configure(req.params.id, data.location, data.portBindings || null, data.accessRestriction, function (error) {
apps.configure(req.params.id, data.location, data.portBindings || null, data.accessRestriction, data.oauthProxy, 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.'));
@@ -253,7 +257,7 @@ function updateApp(req, res, next) {
if ('icon' in data && typeof data.icon !== 'string') return next(new HttpError(400, 'icon is not a string'));
if ('force' in data && typeof data.force !== 'boolean') return next(new HttpError(400, 'force must be a boolean'));
debug('Update app id:%s to manifest:%j', req.params.id, data.manifest);
debug('Update app id:%s to manifest:%j with portBindings:%j', req.params.id, data.manifest, data.portBindings);
apps.update(req.params.id, data.force || false, data.manifest, data.portBindings, data.icon, function (error) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
+4 -23
View File
@@ -5,7 +5,6 @@
exports = module.exports = {
add: add,
get: get,
update: update,
del: del,
getAllByUserId: getAllByUserId,
getClientTokens: getClientTokens,
@@ -13,12 +12,13 @@ exports = module.exports = {
};
var assert = require('assert'),
validUrl = require('valid-url'),
clientdb = require('../clientdb.js'),
clients = require('../clients.js'),
ClientsError = clients.ClientsError,
DatabaseError = require('../databaseerror.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
HttpSuccess = require('connect-lastmile').HttpSuccess,
validUrl = require('valid-url');
function add(req, res, next) {
var data = req.body;
@@ -29,10 +29,7 @@ function add(req, res, next) {
if (typeof data.scope !== 'string' || !data.scope) return next(new HttpError(400, 'scope is required'));
if (!validUrl.isWebUri(data.redirectURI)) return next(new HttpError(400, 'redirectURI must be a valid uri'));
// prefix as this route only allows external apps for developers
var appId = 'external-' + data.appId;
clients.add(appId, data.redirectURI, data.scope, function (error, result) {
clients.add(data.appId, clientdb.TYPE_EXTERNAL, data.redirectURI, data.scope, function (error, result) {
if (error && error.reason === ClientsError.INVALID_SCOPE) return next(new HttpError(400, 'Invalid scope'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201, result));
@@ -49,22 +46,6 @@ function get(req, res, next) {
});
}
function update(req, res, next) {
assert.strictEqual(typeof req.params.clientId, 'string');
var data = req.body;
if (!data) return next(new HttpError(400, 'Cannot parse data field'));
if (typeof data.appId !== 'string' || !data.appId) return next(new HttpError(400, 'appId is required'));
if (typeof data.redirectURI !== 'string' || !data.redirectURI) return next(new HttpError(400, 'redirectURI is required'));
if (!validUrl.isWebUri(data.redirectURI)) return next(new HttpError(400, 'redirectURI must be a valid uri'));
clients.update(req.params.clientId, data.appId, data.redirectURI, function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, result));
});
}
function del(req, res, next) {
assert.strictEqual(typeof req.params.clientId, 'string');
+1 -4
View File
@@ -44,22 +44,19 @@ function activate(req, res, next) {
if (typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be string'));
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be string'));
if (typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string'));
if ('name' in req.body && typeof req.body.name !== 'string') return next(new HttpError(400, 'name must be a string'));
var username = req.body.username;
var password = req.body.password;
var email = req.body.email;
var name = req.body.name || null;
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
debug('activate: username:%s ip:%s', username, ip);
cloudron.activate(username, password, email, name, ip, function (error, info) {
cloudron.activate(username, password, email, ip, function (error, info) {
if (error && error.reason === CloudronError.ALREADY_PROVISIONED) return next(new HttpError(409, 'Already setup'));
if (error && error.reason === CloudronError.BAD_USERNAME) return next(new HttpError(400, 'Bad username'));
if (error && error.reason === CloudronError.BAD_PASSWORD) return next(new HttpError(400, 'Bad password'));
if (error && error.reason === CloudronError.BAD_EMAIL) return next(new HttpError(400, 'Bad email'));
if (error && error.reason === CloudronError.BAD_NAME) return next(new HttpError(400, 'Bad name'));
if (error) return next(new HttpError(500, error));
// Now let the api server know we got activated
+7 -4
View File
@@ -7,6 +7,7 @@ exports = module.exports = {
};
var cloudron = require('../cloudron.js'),
CloudronError = require('../cloudron.js').CloudronError,
debug = require('debug')('box:routes/internal'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
@@ -14,10 +15,12 @@ var cloudron = require('../cloudron.js'),
function backup(req, res, next) {
debug('trigger backup');
// note that cloudron.backup only waits for backup initiation and not for backup to complete
// backup progress can be checked up ny polling the progress api call
cloudron.backup(function (error) {
if (error) debug('Internal route backup failed', error);
});
if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
// we always succeed to trigger a backup
next(new HttpSuccess(202, {}));
next(new HttpSuccess(202, {}));
});
}
+104 -161
View File
@@ -3,6 +3,7 @@
'use strict';
var assert = require('assert'),
apps = require('../apps'),
authcodedb = require('../authcodedb'),
clientdb = require('../clientdb'),
config = require('../config.js'),
@@ -27,34 +28,21 @@ var assert = require('assert'),
// create OAuth 2.0 server
var gServer = oauth2orize.createServer();
// Register serialialization and deserialization functions.
//
// When a client redirects a user to user authorization endpoint, an
// authorization transaction is initiated. To complete the transaction, the
// user must authenticate and approve the authorization request. Because this
// may involve multiple HTTP request/response exchanges, the transaction is
// stored in the session.
//
// An application must supply serialization functions, which determine how the
// client object is serialized into the session. Typically this will be a
// simple matter of serializing the client's ID, and deserializing by finding
// the client by ID from the database.
// The client id is stored in the session and can thus be retrieved for each
// step in the oauth flow transaction, which involves multiple http requests.
gServer.serializeClient(function (client, callback) {
debug('server serialize:', client);
return callback(null, client.id);
});
gServer.deserializeClient(function (id, callback) {
debug('server deserialize:', id);
clientdb.get(id, function (error, client) {
if (error) { return callback(error); }
return callback(null, client);
});
clientdb.get(id, callback);
});
// Register supported grant types.
// Grant authorization codes. The callback takes the `client` requesting
@@ -64,21 +52,17 @@ gServer.deserializeClient(function (id, callback) {
// the application. The application issues a code, which is bound to these
// values, and will be exchanged for an access token.
// we use , (comma) as scope separator
gServer.grant(oauth2orize.grant.code({ scopeSeparator: ',' }, function (client, redirectURI, user, ares, callback) {
debug('grant code:', client, redirectURI, user.id, ares);
debug('grant code:', client.id, redirectURI, user.id, ares);
var code = hat(256);
var expiresAt = Date.now() + 60 * 60000; // 1 hour
var scopes = client.scope ? client.scope.split(',') : ['profile','roleUser'];
if (scopes.indexOf('roleAdmin') !== -1 && !user.admin) {
debug('grant code: not allowed, you need to be admin');
return callback(new Error('Admin capabilities required'));
}
authcodedb.add(code, client.id, user.username, expiresAt, function (error) {
if (error) return callback(error);
debug('grant code: new auth code for client %s code %s', client.id, code);
callback(null, code);
});
}));
@@ -93,7 +77,7 @@ gServer.grant(oauth2orize.grant.token({ scopeSeparator: ',' }, function (client,
tokendb.add(token, tokendb.PREFIX_USER + user.id, client.id, expires, client.scope, function (error) {
if (error) return callback(error);
debug('new access token for client ' + client.id + ' token ' + token);
debug('grant token: new access token for client %s token %s', client.id, token);
callback(null, token);
});
@@ -123,7 +107,7 @@ gServer.exchange(oauth2orize.exchange.code(function (client, code, redirectURI,
tokendb.add(token, tokendb.PREFIX_USER + authCode.userId, authCode.clientId, expires, client.scope, function (error) {
if (error) return callback(error);
debug('new access token for client ' + client.id + ' token ' + token);
debug('exchange: new access token for client %s token %s', client.id, token);
callback(null, token);
});
@@ -148,24 +132,34 @@ session.ensureLoggedIn = function (redirectTo) {
};
};
function renderTemplate(res, template, data) {
assert.strictEqual(typeof res, 'object');
assert.strictEqual(typeof template, 'string');
assert.strictEqual(typeof data, 'object');
res.render(template, data);
}
function sendErrorPageOrRedirect(req, res, message) {
assert.strictEqual(typeof req, 'object');
assert.strictEqual(typeof res, 'object');
assert.strictEqual(typeof message, 'string');
debug('sendErrorPageOrRedirect: returnTo "%s".', req.query.returnTo, message);
debug('sendErrorPageOrRedirect: returnTo %s.', req.query.returnTo, message);
if (typeof req.query.returnTo !== 'string') {
res.render('error', {
renderTemplate(res, 'error', {
adminOrigin: config.adminOrigin(),
message: message
});
} else {
var u = url.parse(req.query.returnTo);
if (!u.protocol || !u.host) return res.render('error', {
adminOrigin: config.adminOrigin(),
message: 'Invalid request. returnTo query is not a valid URI. ' + message
});
if (!u.protocol || !u.host) {
return renderTemplate(res, 'error', {
adminOrigin: config.adminOrigin(),
message: 'Invalid request. returnTo query is not a valid URI. ' + message
});
}
res.redirect(util.format('%s//%s', u.protocol, u.host));
}
@@ -178,65 +172,49 @@ function sendError(req, res, message) {
assert.strictEqual(typeof res, 'object');
assert.strictEqual(typeof message, 'string');
res.render('error', {
renderTemplate(res, 'error', {
adminOrigin: config.adminOrigin(),
message: message
});
}
// Main login form username and password
// -> GET /api/v1/session/login
function loginForm(req, res) {
if (typeof req.session.returnTo !== 'string') return sendErrorPageOrRedirect(req, res, 'Invalid login request. No returnTo provided.');
var u = url.parse(req.session.returnTo, true);
if (!u.query.client_id) return sendErrorPageOrRedirect(req, res, 'Invalid login request. No client_id provided.');
var cloudronName = '';
function render(applicationName, applicationLogo) {
res.render('login', {
renderTemplate(res, 'login', {
adminOrigin: config.adminOrigin(),
csrf: req.csrfToken(),
cloudronName: cloudronName,
applicationName: applicationName,
applicationLogo: applicationLogo,
error: req.query.error || null
});
}
settings.getCloudronName(function (error, name) {
if (error) return sendError(req, res, 'Internal Error');
clientdb.get(u.query.client_id, function (error, result) {
if (error) return sendError(req, res, 'Unknown OAuth client');
cloudronName = name;
switch (result.type) {
case clientdb.TYPE_ADMIN: return render(constants.ADMIN_NAME, '/api/v1/cloudron/avatar');
case clientdb.TYPE_EXTERNAL: return render('External Application', '/api/v1/cloudron/avatar');
case clientdb.TYPE_SIMPLE_AUTH: return sendError(req, res, 'Unknown OAuth client');
default: break;
}
clientdb.get(u.query.client_id, function (error, result) {
if (error) return sendError(req, res, 'Unknown OAuth client');
appdb.get(result.appId, function (error, result) {
if (error) return sendErrorPageOrRedirect(req, res, 'Unknown Application for those OAuth credentials');
// Handle our different types of oauth clients
var appId = result.appId;
if (appId === constants.ADMIN_CLIENT_ID) {
return render(constants.ADMIN_NAME, '/api/v1/cloudron/avatar');
} else if (appId === constants.TEST_CLIENT_ID) {
return render(constants.TEST_NAME, '/api/v1/cloudron/avatar');
} else if (appId.indexOf('external-') === 0) {
return render('External Application', '/api/v1/cloudron/avatar');
} else if (appId.indexOf('addon-') === 0) {
appId = appId.slice('addon-'.length);
} else if (appId.indexOf('proxy-') === 0) {
appId = appId.slice('proxy-'.length);
}
appdb.get(appId, function (error, result) {
if (error) return sendErrorPageOrRedirect(req, res, 'Unknown Application for those OAuth credentials');
var applicationName = result.location || config.fqdn();
render(applicationName, '/api/v1/cloudron/avatar');
});
var applicationName = result.location || config.fqdn();
render(applicationName, '/api/v1/apps/' + result.id + '/icon');
});
});
}
// performs the login POST from the login form
// -> POST /api/v1/session/login
function login(req, res) {
var returnTo = req.session.returnTo || req.query.returnTo;
@@ -248,7 +226,7 @@ function login(req, res) {
});
}
// ends the current session
// -> GET /api/v1/session/logout
function logout(req, res) {
req.logout();
@@ -259,7 +237,7 @@ function logout(req, res) {
// Form to enter email address to send a password reset request mail
// -> GET /api/v1/session/password/resetRequest.html
function passwordResetRequestSite(req, res) {
res.render('password_reset_request', { adminOrigin: config.adminOrigin(), csrf: req.csrfToken() });
renderTemplate(res, 'password_reset_request', { adminOrigin: config.adminOrigin(), csrf: req.csrfToken() });
}
// This route is used for above form submission
@@ -283,19 +261,22 @@ function passwordResetRequest(req, res, next) {
// -> GET /api/v1/session/password/sent.html
function passwordSentSite(req, res) {
res.render('password_reset_sent', { adminOrigin: config.adminOrigin() });
renderTemplate(res, 'password_reset_sent', { adminOrigin: config.adminOrigin() });
}
// -> GET /api/v1/session/password/setup.html
function passwordSetupSite(req, res, next) {
if (!req.query.reset_token) return next(new HttpError(400, 'Missing reset_token'));
debug('passwordSetupSite: with token %s.', req.query.reset_token);
user.getByResetToken(req.query.reset_token, function (error, user) {
if (error) return next(new HttpError(401, 'Invalid reset_token'));
res.render('password_setup', { adminOrigin: config.adminOrigin(), user: user, csrf: req.csrfToken(), resetToken: req.query.reset_token });
renderTemplate(res, 'password_setup', {
adminOrigin: config.adminOrigin(),
user: user,
csrf: req.csrfToken(),
resetToken: req.query.reset_token
});
});
}
@@ -303,12 +284,15 @@ function passwordSetupSite(req, res, next) {
function passwordResetSite(req, res, next) {
if (!req.query.reset_token) return next(new HttpError(400, 'Missing reset_token'));
debug('passwordResetSite: with token %s.', req.query.reset_token);
user.getByResetToken(req.query.reset_token, function (error, user) {
if (error) return next(new HttpError(401, 'Invalid reset_token'));
res.render('password_reset', { adminOrigin: config.adminOrigin(), user: user, csrf: req.csrfToken(), resetToken: req.query.reset_token });
renderTemplate(res, 'password_reset', {
adminOrigin: config.adminOrigin(),
user: user,
csrf: req.csrfToken(),
resetToken: req.query.reset_token
});
});
}
@@ -334,50 +318,28 @@ function passwordReset(req, res, next) {
}
/*
The callback page takes the redirectURI and the authCode and redirects the browser accordingly
*/
// The callback page takes the redirectURI and the authCode and redirects the browser accordingly
//
// -> GET /api/v1/session/callback
var callback = [
session.ensureLoggedIn('/api/v1/session/login'),
function (req, res) {
debug('callback: with callback server ' + req.query.redirectURI);
res.render('callback', { adminOrigin: config.adminOrigin(), callbackServer: req.query.redirectURI });
renderTemplate(res, 'callback', { adminOrigin: config.adminOrigin(), callbackServer: req.query.redirectURI });
}
];
/*
This indicates a missing OAuth client session or invalid client ID
*/
var error = [
session.ensureLoggedIn('/api/v1/session/login'),
function (req, res) {
sendErrorPageOrRedirect(req, res, 'Invalid OAuth Client');
}
];
/*
The authorization endpoint is the entry point for an OAuth login.
Each app would start OAuth by redirecting the user to:
/api/v1/oauth/dialog/authorize?response_type=code&client_id=<clientId>&redirect_uri=<callbackURL>&scope=<ignored>
- First, this will ensure the user is logged in.
- Then in normal OAuth it would ask the user for permissions to the scopes, which we will do on app installation
- Then it will redirect the browser to the given <callbackURL> containing the authcode in the query
Scopes are set by the app during installation, the ones given on OAuth transaction start are simply ignored.
*/
// The authorization endpoint is the entry point for an OAuth login.
//
// Each app would start OAuth by redirecting the user to:
//
// /api/v1/oauth/dialog/authorize?response_type=code&client_id=<clientId>&redirect_uri=<callbackURL>&scope=<ignored>
//
// - First, this will ensure the user is logged in.
// - Then it will redirect the browser to the given <callbackURL> containing the authcode in the query
//
// -> GET /api/v1/oauth/dialog/authorize
var authorization = [
// extract the returnTo origin and set as query param
function (req, res, next) {
if (!req.query.redirect_uri) return sendErrorPageOrRedirect(req, res, 'Invalid request. redirect_uri query param is not set.');
if (!req.query.client_id) return sendErrorPageOrRedirect(req, res, 'Invalid request. client_id query param is not set.');
@@ -386,10 +348,10 @@ var authorization = [
session.ensureLoggedIn('/api/v1/session/login?returnTo=' + req.query.redirect_uri)(req, res, next);
},
gServer.authorization(function (clientID, redirectURI, callback) {
debug('authorization: client %s with callback to %s.', clientID, redirectURI);
gServer.authorization({}, function (clientId, redirectURI, callback) {
debug('authorization: client %s with callback to %s.', clientId, redirectURI);
clientdb.get(clientID, function (error, client) {
clientdb.get(clientId, function (error, client) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
@@ -400,47 +362,33 @@ var authorization = [
callback(null, client, '/api/v1/session/callback?redirectURI=' + url.resolve(redirectOrigin, redirectPath));
});
}),
// Until we have OAuth scopes, skip decision dialog
// OAuth sopes skip START
function (req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.oauth2, 'object');
// Handle our different types of oauth clients
var type = req.oauth2.client.type;
var scopes = req.oauth2.client.scope ? req.oauth2.client.scope.split(',') : ['profile','roleUser'];
if (type === clientdb.TYPE_ADMIN) return next();
if (type === clientdb.TYPE_EXTERNAL) return next();
if (type === clientdb.TYPE_SIMPLE_AUTH) return sendError(req, res, 'Unkonwn OAuth client.');
if (scopes.indexOf('roleAdmin') !== -1 && !req.user.admin) {
return sendErrorPageOrRedirect(req, res, 'Admin capabilities required');
}
appdb.get(req.oauth2.client.appId, function (error, appObject) {
if (error) return sendErrorPageOrRedirect(req, res, 'Invalid request. Unknown app for this client_id.');
req.body.transaction_id = req.oauth2.transactionID;
next();
if (!apps.hasAccessTo(appObject, req.oauth2.user)) return sendErrorPageOrRedirect(req, res, 'No access to this app.');
next();
});
},
gServer.decision(function(req, done) {
debug('decision: with scope', req.oauth2.req.scope);
return done(null, { scope: req.oauth2.req.scope });
})
// OAuth sopes skip END
// function (req, res) {
// res.render('dialog', { transactionID: req.oauth2.transactionID, user: req.user, client: req.oauth2.client, csrf: req.csrfToken() });
// }
];
// this triggers the above grant middleware and handles the user's decision if he accepts the access
var decision = [
session.ensureLoggedIn('/api/v1/session/login'),
gServer.decision()
gServer.decision({ loadTransaction: false })
];
/*
The token endpoint allows an OAuth client to exchange an authcode with an accesstoken.
Authcodes are obtained using the authorization endpoint. The route is authenticated by
providing a Basic auth with clientID as username and clientSecret as password.
An authcode is only good for one such exchange to an accesstoken.
*/
// The token endpoint allows an OAuth client to exchange an authcode with an accesstoken.
//
// Authcodes are obtained using the authorization endpoint. The route is authenticated by
// providing a Basic auth with clientID as username and clientSecret as password.
// An authcode is only good for one such exchange to an accesstoken.
//
// -> POST /api/v1/oauth/token
var token = [
passport.authenticate(['basic', 'oauth2-client-password'], { session: false }),
gServer.token(),
@@ -448,18 +396,15 @@ var token = [
];
/*
The scope middleware provides an auth middleware for routes.
It is used for API routes, which are authenticated using accesstokens.
Those accesstokens carry OAuth scopes and the middleware takes the required
scope as an argument and will verify the accesstoken against it.
See server.js:
var profileScope = routes.oauth2.scope('profile');
*/
// The scope middleware provides an auth middleware for routes.
//
// It is used for API routes, which are authenticated using accesstokens.
// Those accesstokens carry OAuth scopes and the middleware takes the required
// scope as an argument and will verify the accesstoken against it.
//
// See server.js:
// var profileScope = routes.oauth2.scope('profile');
//
function scope(requestedScope) {
assert.strictEqual(typeof requestedScope, 'string');
@@ -501,7 +446,6 @@ exports = module.exports = {
login: login,
logout: logout,
callback: callback,
error: error,
passwordResetRequestSite: passwordResetRequestSite,
passwordResetRequest: passwordResetRequest,
passwordSentSite: passwordSentSite,
@@ -509,7 +453,6 @@ exports = module.exports = {
passwordSetupSite: passwordSetupSite,
passwordReset: passwordReset,
authorization: authorization,
decision: decision,
token: token,
scope: scope,
csrf: csrf
+233 -89
View File
@@ -16,22 +16,21 @@ var appdb = require('../../appdb.js'),
config = require('../../config.js'),
constants = require('../../constants.js'),
database = require('../../database.js'),
docker = require('../../docker.js'),
docker = require('../../docker.js').connection,
expect = require('expect.js'),
fs = require('fs'),
hock = require('hock'),
http = require('http'),
https = require('https'),
js2xml = require('js2xmlparser'),
net = require('net'),
nock = require('nock'),
os = require('os'),
paths = require('../../paths.js'),
redis = require('redis'),
request = require('superagent'),
safe = require('safetydance'),
server = require('../../server.js'),
settings = require('../../settings.js'),
sysinfo = require('../../sysinfo.js'),
tokendb = require('../../tokendb.js'),
url = require('url'),
util = require('util'),
@@ -40,17 +39,43 @@ var appdb = require('../../appdb.js'),
var SERVER_URL = 'http://localhost:' + config.get('port');
// Test image information
var TEST_IMAGE_REPO = 'cloudron/test';
var TEST_IMAGE_TAG = '9.0.0';
var TEST_IMAGE_ID = child_process.execSync('docker inspect --format={{.Id}} ' + TEST_IMAGE_REPO + ':' + TEST_IMAGE_TAG).toString('utf8').trim();
var APP_STORE_ID = 'test', APP_ID;
var APP_LOCATION = 'appslocation';
var APP_LOCATION_2 = 'appslocationtwo';
var APP_LOCATION_NEW = 'appslocationnew';
var APP_MANIFEST = JSON.parse(fs.readFileSync(__dirname + '/../../../../test-app/CloudronManifest.json', 'utf8'));
APP_MANIFEST.dockerImage = 'girish/test:0.2.0';
APP_MANIFEST.dockerImage = TEST_IMAGE_REPO + ':' + TEST_IMAGE_TAG;
var APP_MANIFEST_1 = JSON.parse(fs.readFileSync(__dirname + '/../../../../test-app/CloudronManifest.json', 'utf8'));
APP_MANIFEST_1.dockerImage = TEST_IMAGE_REPO + ':' + TEST_IMAGE_TAG;
APP_MANIFEST_1.singleUser = true;
var USERNAME = 'admin', PASSWORD = 'password', EMAIL ='admin@me.com';
var USERNAME_1 = 'user', PASSWORD_1 = 'password', EMAIL_1 ='user@me.com';
var token = null; // authentication token
var token_1 = null;
var awsHostedZones = {
HostedZones: [{
Id: '/hostedzone/ZONEID',
Name: 'localhost.',
CallerReference: '305AFD59-9D73-4502-B020-F4E6F889CB30',
ResourceRecordSetCount: 2,
ChangeInfo: {
Id: '/change/CKRTFJA0ANHXB',
Status: 'INSYNC'
}
}],
IsTruncated: false,
MaxItems: '100'
};
function startDockerProxy(interceptor, callback) {
assert.strictEqual(typeof interceptor, 'function');
@@ -79,10 +104,12 @@ function startDockerProxy(interceptor, callback) {
function setup(done) {
async.series([
server.start.bind(server),
// first clear, then start server. otherwise, taskmanager spins up tasks for obsolete appIds
database.initialize,
database._clear,
server.start.bind(server),
function (callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
@@ -104,11 +131,11 @@ function setup(done) {
});
},
child_process.exec.bind(null, __dirname + '/start_addons.sh'),
function (callback) {
callback(null);
console.log('Starting addons, this can take 10 seconds');
child_process.exec(__dirname + '/start_addons.sh', callback);
},
function (callback) {
request.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
@@ -129,12 +156,13 @@ function setup(done) {
}
function cleanup(done) {
// 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([
database._clear,
server.stop,
child_process.exec.bind(null, 'docker rm -f mysql; docker rm -f postgresql; docker rm -f mongodb')
function (callback) { setTimeout(callback, 2000); }, // give taskmanager tasks couple of seconds to finish
child_process.exec.bind(null, 'docker rm -f mysql; docker rm -f postgresql; docker rm -f mongodb; docker rm -f mail')
], done);
}
@@ -143,10 +171,19 @@ describe('App API', function () {
var dockerProxy;
before(function (done) {
dockerProxy = startDockerProxy(function interceptor() { return false; }, function () {
dockerProxy = startDockerProxy(function interceptor(req, res) {
if (req.method === 'DELETE' && req.url === '/images/' + TEST_IMAGE_ID + '?force=true&noprune=false') {
res.writeHead(200);
res.end();
return true;
}
return false;
}, function () {
setup(done);
});
});
after(function (done) {
APP_ID = null;
cleanup(function () {
@@ -189,7 +226,7 @@ describe('App API', function () {
it('app install fails - invalid location', function (done) {
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: '!awesome', accessRestriction: '' })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: '!awesome', accessRestriction: null, oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('Hostname can only contain alphanumerics and hyphen');
@@ -200,7 +237,7 @@ describe('App API', function () {
it('app install fails - invalid location type', function (done) {
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: 42, accessRestriction: '' })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: 42, accessRestriction: null, oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('location is required');
@@ -211,7 +248,7 @@ describe('App API', function () {
it('app install fails - reserved admin location', function (done) {
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: constants.ADMIN_LOCATION, accessRestriction: '' })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: constants.ADMIN_LOCATION, accessRestriction: null, oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql(constants.ADMIN_LOCATION + ' is reserved');
@@ -222,7 +259,7 @@ describe('App API', function () {
it('app install fails - reserved api location', function (done) {
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: constants.API_LOCATION, accessRestriction: '' })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: constants.API_LOCATION, accessRestriction: null, oauthProxy: true })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql(constants.API_LOCATION + ' is reserved');
@@ -233,7 +270,7 @@ describe('App API', function () {
it('app install fails - portBindings must be object', function (done) {
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: 23, accessRestriction: '' })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: 23, accessRestriction: null, oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('portBindings must be an object');
@@ -244,7 +281,7 @@ describe('App API', function () {
it('app install fails - accessRestriction is required', function (done) {
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: {} })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: {}, oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('accessRestriction is required');
@@ -252,10 +289,54 @@ describe('App API', function () {
});
});
it('app install fails - accessRestriction type is wrong', function (done) {
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: {}, accessRestriction: '', oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('accessRestriction is required');
done(err);
});
});
it('app install fails - accessRestriction no users not allowed', function (done) {
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST_1, password: PASSWORD, location: APP_LOCATION, portBindings: {}, accessRestriction: null, oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('accessRestriction must specify one user');
done(err);
});
});
it('app install fails - accessRestriction too many users not allowed', function (done) {
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST_1, password: PASSWORD, location: APP_LOCATION, portBindings: {}, accessRestriction: { users: [ 'one', 'two' ] }, oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('accessRestriction must specify one user');
done(err);
});
});
it('app install fails - oauthProxy is required', function (done) {
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: {}, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('oauthProxy must be a boolean');
done(err);
});
});
it('app install fails for non admin', function (done) {
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token_1 })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: '' })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: null, oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done(err);
@@ -267,7 +348,7 @@ describe('App API', function () {
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: '' })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: null, oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(402);
expect(fake.isDone()).to.be.ok();
@@ -280,7 +361,7 @@ describe('App API', function () {
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: '' })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: null, oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(res.body.id).to.be.a('string');
@@ -295,7 +376,7 @@ describe('App API', function () {
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: '' })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: null, oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
expect(fake.isDone()).to.be.ok();
@@ -419,7 +500,7 @@ describe('App API', function () {
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION_2, portBindings: null, accessRestriction: '' })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION_2, portBindings: null, accessRestriction: null, oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(res.body.id).to.be.a('string');
@@ -448,7 +529,7 @@ describe('App API', function () {
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, location: APP_LOCATION+APP_LOCATION, portBindings: null, accessRestriction: '' })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, location: APP_LOCATION+APP_LOCATION, portBindings: null, accessRestriction: null, oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(res.body.id).to.be.a('string');
@@ -473,7 +554,8 @@ describe('App API', function () {
describe('App installation', function () {
this.timeout(50000);
var hockInstance = hock.createHock({ throwOnUnmatched: false }), hockServer, dockerProxy;
var apiHockInstance = hock.createHock({ throwOnUnmatched: false }), apiHockServer, dockerProxy;
var awsHockInstance = hock.createHock({ throwOnUnmatched: false }), awsHockServer;
var imageDeleted = false, imageCreated = false;
before(function (done) {
@@ -482,12 +564,12 @@ describe('App installation', function () {
async.series([
function (callback) {
dockerProxy = startDockerProxy(function interceptor(req, res) {
if (req.method === 'POST' && req.url === '/images/create?fromImage=girish%2Ftest&tag=0.2.0') {
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/c7ddfc8fb7cd8a14d4d70153a199ff0c6e9b709807aeec5a7b799d60618731d1?force=true&noprune=false') {
} else if (req.method === 'DELETE' && req.url === '/images/' + TEST_IMAGE_ID + '?force=true&noprune=false') {
imageDeleted = true;
res.writeHead(200);
res.end();
@@ -500,28 +582,42 @@ describe('App installation', function () {
setup,
function (callback) {
hockInstance
apiHockInstance
.get('/api/v1/apps/' + APP_STORE_ID + '/versions/' + APP_MANIFEST.version + '/icon')
.replyWithFile(200, path.resolve(__dirname, '../../../webadmin/src/img/appicon_fallback.png'))
.post('/api/v1/subdomains?token=' + config.token(), { records: [ { subdomain: APP_LOCATION, type: 'A', value: sysinfo.getIp() } ] })
.reply(201, { ids: [ 'dnsrecordid' ] }, { 'Content-Type': 'application/json' })
.delete('/api/v1/subdomains/dnsrecordid?token=' + config.token())
.reply(204, { }, { 'Content-Type': 'application/json' });
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=APPSTORE_TOKEN')
.max(Infinity)
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } }, { 'Content-Type': 'application/json' });
var port = parseInt(url.parse(config.apiServerOrigin()).port, 10);
hockServer = http.createServer(hockInstance.handler).listen(port, callback);
apiHockServer = http.createServer(apiHockInstance.handler).listen(port, callback);
},
function (callback) {
awsHockInstance
.get('/2013-04-01/hostedzone')
.max(Infinity)
.reply(200, js2xml('ListHostedZonesResponse', awsHostedZones, { arrayMap: { HostedZones: 'HostedZone'} }), { 'Content-Type': 'application/xml' })
.filteringRequestBody(function (unusedBody) { return ''; }) // strip out body
.post('/2013-04-01/hostedzone/ZONEID/rrset/')
.max(Infinity)
.reply(200, js2xml('ChangeResourceRecordSetsResponse', { ChangeInfo: { Id: 'dnsrecordid', Status: 'INSYNC' } }), { 'Content-Type': 'application/xml' });
var port = parseInt(url.parse(config.aws().endpoint).port, 10);
awsHockServer = http.createServer(awsHockInstance.handler).listen(port, callback);
}
], done);
});
after(function (done) {
APP_ID = null;
cleanup(function (error) {
if (error) return done(error);
hockServer.close(function () {
dockerProxy.close(done);
});
});
async.series([
cleanup,
apiHockServer.close.bind(apiHockServer),
awsHockServer.close.bind(awsHockServer),
dockerProxy.close.bind(dockerProxy)
], done);
});
var appResult = null /* the json response */, appEntry = null /* entry from database */;
@@ -544,7 +640,7 @@ describe('App installation', function () {
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appId: APP_ID, appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: '' })
.send({ appId: APP_ID, appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: null, oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(fake.isDone()).to.be.ok();
@@ -576,9 +672,9 @@ describe('App installation', function () {
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=1');
clientdb.getByAppId('addon-' + appResult.id, function (error, client) {
clientdb.getByAppIdAndType(appResult.id, clientdb.TYPE_OAUTH, function (error, client) {
expect(error).to.not.be.ok();
expect(client.id.length).to.be(46); // cid-addon- + 32 hex chars (128 bits) + 4 hyphens
expect(client.id.length).to.be(40); // cid- + 32 hex chars (128 bits) + 4 hyphens
expect(client.clientSecret.length).to.be(64); // 32 hex chars (256 bits)
expect(data.Config.Env).to.contain('OAUTH_CLIENT_ID=' + client.id);
expect(data.Config.Env).to.contain('OAUTH_CLIENT_SECRET=' + client.clientSecret);
@@ -617,7 +713,14 @@ describe('App installation', function () {
it('installation - running container has volume mounted', function (done) {
docker.getContainer(appEntry.containerId).inspect(function (error, data) {
expect(error).to.not.be.ok();
expect(data.Volumes['/app/data']).to.eql(paths.DATA_DIR + '/' + APP_ID + '/data');
// support newer docker versions
if (data.Volumes) {
expect(data.Volumes['/app/data']).to.eql(paths.DATA_DIR + '/' + APP_ID + '/data');
} else {
expect(data.Mounts.filter(function (mount) { return mount.Destination === '/app/data'; })[0].Source).to.eql(paths.DATA_DIR + '/' + APP_ID + '/data');
}
done();
});
});
@@ -654,10 +757,7 @@ describe('App installation', function () {
expect(urlp.hostname).to.be('redis-' + APP_ID);
var isMac = os.platform() === 'darwin';
var client =
isMac ? redis.createClient(parseInt(exportedRedisPort, 10), '127.0.0.1', { auth_pass: password })
: redis.createClient(parseInt(urlp.port, 10), redisIp, { auth_pass: password });
var client = redis.createClient(parseInt(urlp.port, 10), redisIp, { auth_pass: password });
client.on('error', done);
client.set('key', 'value');
client.get('key', function (err, reply) {
@@ -693,7 +793,7 @@ describe('App installation', function () {
child_process.exec('docker exec ' + appContainer.id + ' ' + cmd, { timeout: 5000 }, function (error, stdout, stderr) {
expect(!error).to.be.ok();
expect(stdout.length).to.be(0);
expect(stderr.length).to.be(0);
// expect(stderr.length).to.be(0); // "Warning: Using a password on the command line interface can be insecure."
done();
});
});
@@ -729,6 +829,14 @@ describe('App installation', function () {
});
});
it('installation - scheduler', function (done) {
async.retry({ times: 100, interval: 1000 }, function (retryCallback) {
if (fs.existsSync(paths.DATA_DIR + '/' + APP_ID + '/data/every_minute.env')) return retryCallback();
retryCallback(new Error('not run yet'));
}, done);
});
it('logs - stdout and stderr', function (done) {
request.get(SERVER_URL + '/api/v1/apps/' + APP_ID + '/logs')
.query({ access_token: token })
@@ -882,9 +990,13 @@ describe('App installation', function () {
});
it('uninstalled - unregistered subdomain', function (done) {
hockInstance.done(function (error) { // checks if all the hockServer APIs were called
apiHockInstance.done(function (error) { // checks if all the apiHockServer APIs were called
expect(!error).to.be.ok();
done();
awsHockInstance.done(function (error) {
expect(!error).to.be.ok();
done();
});
});
});
@@ -904,7 +1016,8 @@ describe('App installation', function () {
describe('App installation - port bindings', function () {
this.timeout(50000);
var hockInstance = hock.createHock({ throwOnUnmatched: false }), hockServer, dockerProxy;
var apiHockInstance = hock.createHock({ throwOnUnmatched: false }), apiHockServer, dockerProxy;
var awsHockInstance = hock.createHock({ throwOnUnmatched: false }), awsHockServer;
var imageDeleted = false, imageCreated = false;
before(function (done) {
@@ -912,12 +1025,12 @@ describe('App installation - port bindings', function () {
async.series([
function (callback) {
dockerProxy = startDockerProxy(function interceptor(req, res) {
if (req.method === 'POST' && req.url === '/images/create?fromImage=girish%2Ftest&tag=0.2.0') {
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/c7ddfc8fb7cd8a14d4d70153a199ff0c6e9b709807aeec5a7b799d60618731d1?force=true&noprune=false') {
} else if (req.method === 'DELETE' && req.url === '/images/' + TEST_IMAGE_ID + '?force=true&noprune=false') {
imageDeleted = true;
res.writeHead(200);
res.end();
@@ -930,35 +1043,41 @@ describe('App installation - port bindings', function () {
setup,
function (callback) {
hockInstance
// app install
apiHockInstance
.get('/api/v1/apps/' + APP_STORE_ID + '/versions/' + APP_MANIFEST.version + '/icon')
.replyWithFile(200, path.resolve(__dirname, '../../../webadmin/src/img/appicon_fallback.png'))
.post('/api/v1/subdomains?token=' + config.token(), { records: [ { subdomain: APP_LOCATION, type: 'A', value: sysinfo.getIp() } ] })
.reply(201, { ids: [ 'dnsrecordid' ] }, { 'Content-Type': 'application/json' })
// app configure
.delete('/api/v1/subdomains/dnsrecordid?token=' + config.token())
.reply(204, { }, { 'Content-Type': 'application/json' })
.post('/api/v1/subdomains?token=' + config.token(), { records: [ { subdomain: APP_LOCATION_NEW, type: 'A', value: sysinfo.getIp() } ] })
.reply(201, { ids: [ 'anotherdnsid' ] }, { 'Content-Type': 'application/json' })
// app remove
.delete('/api/v1/subdomains/anotherdnsid?token=' + config.token())
.reply(204, { }, { 'Content-Type': 'application/json' });
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=APPSTORE_TOKEN')
.max(Infinity)
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } }, { 'Content-Type': 'application/json' });
var port = parseInt(url.parse(config.apiServerOrigin()).port, 10);
hockServer = http.createServer(hockInstance.handler).listen(port, callback);
apiHockServer = http.createServer(apiHockInstance.handler).listen(port, callback);
},
function (callback) {
awsHockInstance
.get('/2013-04-01/hostedzone')
.max(Infinity)
.reply(200, js2xml('ListHostedZonesResponse', awsHostedZones, { arrayMap: { HostedZones: 'HostedZone'} }), { 'Content-Type': 'application/xml' })
.filteringRequestBody(function (unusedBody) { return ''; }) // strip out body
.post('/2013-04-01/hostedzone/ZONEID/rrset/')
.max(Infinity)
.reply(200, js2xml('ChangeResourceRecordSetsResponse', { ChangeInfo: { Id: 'dnsrecordid', Status: 'INSYNC' } }), { 'Content-Type': 'application/xml' });
var port = parseInt(url.parse(config.aws().endpoint).port, 10);
awsHockServer = http.createServer(awsHockInstance.handler).listen(port, callback);
}
], done);
});
after(function (done) {
APP_ID = null;
cleanup(function (error) {
if (error) return done(error);
hockServer.close(function () {
dockerProxy.close(done);
});
});
async.series([
cleanup,
apiHockServer.close.bind(apiHockServer),
awsHockServer.close.bind(awsHockServer),
dockerProxy.close.bind(dockerProxy)
], done);
});
var appResult = null, appEntry = null;
@@ -981,7 +1100,7 @@ describe('App installation - port bindings', function () {
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appId: APP_ID, appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: { ECHO_SERVER_PORT: 7171 }, accessRestriction: '' })
.send({ appId: APP_ID, appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: { ECHO_SERVER_PORT: 7171 }, accessRestriction: null, oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(fake.isDone()).to.be.ok();
@@ -1061,7 +1180,14 @@ describe('App installation - port bindings', function () {
it('installation - running container has volume mounted', function (done) {
docker.getContainer(appEntry.containerId).inspect(function (error, data) {
expect(error).to.not.be.ok();
expect(data.Volumes['/app/data']).to.eql(paths.DATA_DIR + '/' + APP_ID + '/data');
// support newer docker versions
if (data.Volumes) {
expect(data.Volumes['/app/data']).to.eql(paths.DATA_DIR + '/' + APP_ID + '/data');
} else {
expect(data.Mounts.filter(function (mount) { return mount.Destination === '/app/data'; })[0].Source).to.eql(paths.DATA_DIR + '/' + APP_ID + '/data');
}
done();
});
});
@@ -1099,10 +1225,7 @@ describe('App installation - port bindings', function () {
expect(data.Config.Env).to.contain('REDIS_PASSWORD=' + password);
function checkRedis() {
var isMac = os.platform() === 'darwin';
var client =
isMac ? redis.createClient(parseInt(exportedRedisPort, 10), '127.0.0.1', { auth_pass: password })
: redis.createClient(parseInt(urlp.port, 10), redisIp, { auth_pass: password });
var client = redis.createClient(parseInt(urlp.port, 10), redisIp, { auth_pass: password });
client.on('error', done);
client.set('key', 'value');
client.get('key', function (err, reply) {
@@ -1135,7 +1258,7 @@ describe('App installation - port bindings', function () {
it('cannot reconfigure app with missing location', function (done) {
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token })
.send({ appId: APP_ID, password: PASSWORD, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: 'roleAdmin' })
.send({ appId: APP_ID, password: PASSWORD, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null, oauthProxy: true })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
@@ -1145,7 +1268,17 @@ describe('App installation - port bindings', function () {
it('cannot reconfigure app with missing accessRestriction', function (done) {
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token })
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 } })
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('cannot reconfigure app with missing oauthProxy', function (done) {
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token })
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
@@ -1155,7 +1288,7 @@ describe('App installation - port bindings', function () {
it('non admin cannot reconfigure app', function (done) {
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token_1 })
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: 'roleAdmin' })
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null, oauthProxy: true })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
@@ -1165,7 +1298,7 @@ describe('App installation - port bindings', function () {
it('can reconfigure app', function (done) {
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token })
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: 'roleAdmin' })
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null, oauthProxy: true })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
checkConfigureStatus(0, done);
@@ -1224,10 +1357,7 @@ describe('App installation - port bindings', function () {
expect(data.Config.Env).to.contain('REDIS_HOST=redis-' + APP_ID);
expect(data.Config.Env).to.contain('REDIS_PASSWORD=' + password);
var isMac = os.platform() === 'darwin';
var client =
isMac ? redis.createClient(parseInt(exportedRedisPort, 10), '127.0.0.1', { auth_pass: password })
: redis.createClient(parseInt(urlp.port, 10), redisIp, { auth_pass: password });
var client = redis.createClient(parseInt(urlp.port, 10), redisIp, { auth_pass: password });
client.on('error', done);
client.set('key', 'value');
client.get('key', function (err, reply) {
@@ -1239,6 +1369,16 @@ describe('App installation - port bindings', function () {
});
});
it('scheduler works after reconfiguration', function (done) {
async.retry({ times: 100, interval: 1000 }, function (callback) {
var data = safe.fs.readFileSync(paths.DATA_DIR + '/' + APP_ID + '/data/every_minute.env', 'utf8');
if (data && data.indexOf('ECHO_SERVER_PORT=7172') !== -1) return callback();
callback(new Error('not run yet'));
}, done);
});
it('can stop app', function (done) {
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/stop')
.query({ access_token: token })
@@ -1302,9 +1442,13 @@ describe('App installation - port bindings', function () {
});
it('uninstalled - unregistered subdomain', function (done) {
hockInstance.done(function (error) { // checks if all the hockServer APIs were called
apiHockInstance.done(function (error) { // checks if all the apiHockServer APIs were called
expect(!error).to.be.ok();
done();
awsHockInstance.done(function (error) {
expect(!error).to.be.ok();
done();
});
});
});
+3 -3
View File
@@ -51,7 +51,7 @@ function setup(done) {
function addApp(callback) {
var manifest = { version: '0.0.1', manifestVersion: 1, dockerImage: 'foo', healthCheckPath: '/', httpPort: 3, title: 'ok', addons: { } };
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, '' /* accessRestriction */, callback);
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, null /* accessRestriction */, false /* oauthProxy */, callback);
}
], done);
}
@@ -119,8 +119,8 @@ describe('Backups API', function () {
it('succeeds', function (done) {
var scope = nock(config.apiServerOrigin())
.get('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=APPSTORE_TOKEN')
.reply(201, { accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', sessionToken: 'sessionToken' });
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=APPSTORE_TOKEN')
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
request.post(SERVER_URL + '/api/v1/backups')
.query({ access_token: token })
+12 -183
View File
@@ -71,7 +71,7 @@ describe('OAuth Clients API', function () {
it('fails', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: 'profile,roleUser' })
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: 'profile' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(412);
@@ -87,7 +87,7 @@ describe('OAuth Clients API', function () {
it('fails without token', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: 'profile,roleUser' })
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: 'profile' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
@@ -98,7 +98,7 @@ describe('OAuth Clients API', function () {
it('fails without appId', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ redirectURI: 'http://foobar.com', scope: 'profile,roleUser' })
.send({ redirectURI: 'http://foobar.com', scope: 'profile' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
@@ -109,7 +109,7 @@ describe('OAuth Clients API', function () {
it('fails with empty appId', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ appId: '', redirectURI: 'http://foobar.com', scope: 'profile,roleUser' })
.send({ appId: '', redirectURI: 'http://foobar.com', scope: 'profile' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
@@ -142,7 +142,7 @@ describe('OAuth Clients API', function () {
it('fails without redirectURI', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ appId: 'someApp', scope: 'profile,roleUser' })
.send({ appId: 'someApp', scope: 'profile' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
@@ -153,7 +153,7 @@ describe('OAuth Clients API', function () {
it('fails with empty redirectURI', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: '', scope: 'profile,roleUser' })
.send({ appId: 'someApp', redirectURI: '', scope: 'profile' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
@@ -164,7 +164,7 @@ describe('OAuth Clients API', function () {
it('fails with malformed redirectURI', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: 'foobar', scope: 'profile,roleUser' })
.send({ appId: 'someApp', redirectURI: 'foobar', scope: 'profile' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
@@ -175,7 +175,7 @@ describe('OAuth Clients API', function () {
it('succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: 'profile,roleUser' })
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: 'profile' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(201);
@@ -195,7 +195,7 @@ describe('OAuth Clients API', function () {
id: '',
appId: 'someAppId-0',
redirectURI: 'http://some.callback0',
scope: 'profile,roleUser'
scope: 'profile'
};
before(function (done) {
@@ -297,183 +297,12 @@ describe('OAuth Clients API', function () {
});
});
describe('update', function () {
var CLIENT_0 = {
id: '',
appId: 'someAppId-0',
redirectURI: 'http://some.callback0',
scope: 'profile,roleUser'
};
var CLIENT_1 = {
id: '',
appId: 'someAppId-1',
redirectURI: 'http://some.callback1',
scope: 'profile,roleUser'
};
before(function (done) {
async.series([
server.start.bind(null),
database._clear.bind(null),
function (callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result).to.be.ok();
expect(result.statusCode).to.equal(201);
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
// stash token for further use
token = result.body.token;
callback();
});
},
settings.setDeveloperMode.bind(null, true),
function (callback) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ appId: CLIENT_0.appId, redirectURI: CLIENT_0.redirectURI, scope: CLIENT_0.scope })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(201);
CLIENT_0 = result.body;
callback();
});
}
], done);
});
after(cleanup);
describe('without developer mode', function () {
before(function (done) {
settings.setDeveloperMode(false, done);
});
it('fails', function (done) {
superagent.put(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.send({ appId: CLIENT_1.appId, redirectURI: CLIENT_1.redirectURI })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(412);
done();
});
});
});
describe('with developer mode', function () {
before(function (done) {
settings.setDeveloperMode(true, done);
});
it('fails without token', function (done) {
superagent.put(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.send({ appId: 'someApp', redirectURI: 'http://foobar.com' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails without appId', function (done) {
superagent.put(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.send({ redirectURI: CLIENT_1.redirectURI })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with empty appId', function (done) {
superagent.put(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.send({ appId: '', redirectURI: CLIENT_1.redirectURI })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails without redirectURI', function (done) {
superagent.put(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.send({ appId: CLIENT_1.appId })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with empty redirectURI', function (done) {
superagent.put(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.send({ appId: CLIENT_1.appId, redirectURI: '' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with malformed redirectURI', function (done) {
superagent.put(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.send({ appId: CLIENT_1.appId, redirectURI: 'foobar' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.send({ appId: CLIENT_1.appId, redirectURI: CLIENT_1.redirectURI })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(202);
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(200);
expect(result.body.appId).to.equal(CLIENT_1.appId);
expect(result.body.redirectURI).to.equal(CLIENT_1.redirectURI);
done();
});
});
});
});
});
describe('del', function () {
var CLIENT_0 = {
id: '',
appId: 'someAppId-0',
redirectURI: 'http://some.callback0',
scope: 'profile,roleUser'
scope: 'profile'
};
before(function (done) {
@@ -591,8 +420,8 @@ describe('Clients', function () {
email: 'some@email.com',
admin: true,
salt: 'somesalt',
createdAt: (new Date()).toUTCString(),
modifiedAt: (new Date()).toUTCString(),
createdAt: (new Date()).toISOString(),
modifiedAt: (new Date()).toISOString(),
resetToken: hat(256)
};
+22 -9
View File
@@ -451,17 +451,19 @@ describe('Cloudron', function () {
it('fails when in wrong state', function (done) {
var scope2 = nock(config.apiServerOrigin())
.get('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=APPSTORE_TOKEN')
.reply(201, { credentials: { accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', sessionToken: 'sessionToken' } });
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=APPSTORE_TOKEN')
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
var scope3 = nock(config.apiServerOrigin())
.filteringRequestBody(function () { return false; })
.post('/api/v1/boxes/' + config.fqdn() + '/backupDone?token=APPSTORE_TOKEN')
.post('/api/v1/boxes/' + config.fqdn() + '/backupDone?token=APPSTORE_TOKEN', function (body) {
return body.boxVersion && body.restoreKey && !body.appId && !body.appVersion && body.appBackupIds.length === 0;
})
.reply(200, { id: 'someid' });
var scope1 = nock(config.apiServerOrigin())
.filteringRequestBody(function () { return false; })
.post('/api/v1/boxes/' + config.fqdn() + '/migrate?token=APPSTORE_TOKEN', { }).reply(409, {});
.post('/api/v1/boxes/' + config.fqdn() + '/migrate?token=APPSTORE_TOKEN', function (body) {
return body.size && body.region && body.restoreKey;
}).reply(409, {});
injectShellMock();
@@ -487,8 +489,19 @@ describe('Cloudron', function () {
it('succeeds', function (done) {
var scope1 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/migrate?token=APPSTORE_TOKEN', { size: 'small', region: 'sfo', restoreKey: 'someid' }).reply(202, {});
var scope2 = nock(config.apiServerOrigin()).put('/api/v1/boxes/' + config.fqdn() + '/backupurl?token=APPSTORE_TOKEN', { boxVersion: '0.5.0', appId: null, appVersion: null, appBackupIds: [] }).reply(201, { id: 'someid', url: 'http://foobar', backupKey: 'somerestorekey' });
var scope1 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/migrate?token=APPSTORE_TOKEN', function (body) {
return body.size && body.region && body.restoreKey;
}).reply(202, {});
var scope2 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/' + config.fqdn() + '/backupDone?token=APPSTORE_TOKEN', function (body) {
return body.boxVersion && body.restoreKey && !body.appId && !body.appVersion && body.appBackupIds.length === 0;
})
.reply(200, { id: 'someid' });
var scope3 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=APPSTORE_TOKEN')
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
injectShellMock();
@@ -500,7 +513,7 @@ describe('Cloudron', function () {
expect(result.statusCode).to.equal(202);
function checkAppstoreServerCalled() {
if (scope1.isDone() && scope2.isDone()) {
if (scope1.isDone() && scope2.isDone() && scope3.isDone()) {
restoreShellMock();
return done();
}
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -54,7 +54,7 @@ function setup(done) {
function addApp(callback) {
var manifest = { version: '0.0.1', manifestVersion: 1, dockerImage: 'foo', healthCheckPath: '/', httpPort: 3, title: 'ok' };
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, '' /* accessRestriction */, callback);
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, null /* accessRestriction */, false /* oauthProxy */, callback);
}
], done);
}
+421
View File
@@ -0,0 +1,421 @@
/* jslint node:true */
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
'use strict';
var clientdb = require('../../clientdb.js'),
appdb = require('../../appdb.js'),
async = require('async'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
request = require('superagent'),
server = require('../../server.js'),
simpleauth = require('../../simpleauth.js'),
nock = require('nock');
describe('SimpleAuth API', function () {
var SERVER_URL = 'http://localhost:' + config.get('port');
var SIMPLE_AUTH_ORIGIN = 'http://localhost:' + config.get('simpleAuthPort');
var USERNAME = 'admin', PASSWORD = 'password', EMAIL ='silly@me.com';
var APP_0 = {
id: 'app0',
appStoreId: '',
manifest: { version: '0.1.0', addons: { } },
location: 'test0',
portBindings: {},
accessRestriction: { users: [ 'foobar', 'someone'] },
oauthProxy: true
};
var APP_1 = {
id: 'app1',
appStoreId: '',
manifest: { version: '0.1.0', addons: { } },
location: 'test1',
portBindings: {},
accessRestriction: { users: [ 'foobar', USERNAME, 'someone' ] },
oauthProxy: true
};
var APP_2 = {
id: 'app2',
appStoreId: '',
manifest: { version: '0.1.0', addons: { } },
location: 'test2',
portBindings: {},
accessRestriction: null,
oauthProxy: true
};
var CLIENT_0 = {
id: 'someclientid',
appId: 'someappid',
type: clientdb.TYPE_SIMPLE_AUTH,
clientSecret: 'someclientsecret',
redirectURI: '',
scope: 'user,profile'
};
var CLIENT_1 = {
id: 'someclientid1',
appId: APP_0.id,
type: clientdb.TYPE_SIMPLE_AUTH,
clientSecret: 'someclientsecret1',
redirectURI: '',
scope: 'user,profile'
};
var CLIENT_2 = {
id: 'someclientid2',
appId: APP_1.id,
type: clientdb.TYPE_SIMPLE_AUTH,
clientSecret: 'someclientsecret2',
redirectURI: '',
scope: 'user,profile'
};
var CLIENT_3 = {
id: 'someclientid3',
appId: APP_2.id,
type: clientdb.TYPE_SIMPLE_AUTH,
clientSecret: 'someclientsecret3',
redirectURI: '',
scope: 'user,profile'
};
var CLIENT_4 = {
id: 'someclientid4',
appId: APP_2.id,
type: clientdb.TYPE_OAUTH,
clientSecret: 'someclientsecret4',
redirectURI: '',
scope: 'user,profile'
};
before(function (done) {
async.series([
server.start.bind(server),
simpleauth.start.bind(simpleauth),
database._clear,
function createAdmin(callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
request.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
callback();
});
},
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),
clientdb.add.bind(null, CLIENT_3.id, CLIENT_3.appId, CLIENT_3.type, CLIENT_3.clientSecret, CLIENT_3.redirectURI, CLIENT_3.scope),
clientdb.add.bind(null, CLIENT_4.id, CLIENT_4.appId, CLIENT_4.type, CLIENT_4.clientSecret, CLIENT_4.redirectURI, CLIENT_4.scope),
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.oauthProxy),
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.portBindings, APP_1.accessRestriction, APP_1.oauthProxy),
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.portBindings, APP_2.accessRestriction, APP_2.oauthProxy)
], done);
});
after(function (done) {
async.series([
database._clear,
simpleauth.stop.bind(simpleauth),
server.stop.bind(server)
], done);
});
describe('login', function () {
it('cannot login without clientId', function (done) {
var body = {};
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(400);
done();
});
});
it('cannot login without username', function (done) {
var body = {
clientId: 'someclientid'
};
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(400);
done();
});
});
it('cannot login without password', function (done) {
var body = {
clientId: 'someclientid',
username: USERNAME
};
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(400);
done();
});
});
it('cannot login with unkown clientId', function (done) {
var body = {
clientId: CLIENT_0.id+CLIENT_0.id,
username: USERNAME,
password: PASSWORD
};
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(401);
done();
});
});
it('cannot login with unkown user', function (done) {
var body = {
clientId: CLIENT_0.id,
username: USERNAME+USERNAME,
password: PASSWORD
};
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(401);
done();
});
});
it('cannot login with empty password', function (done) {
var body = {
clientId: CLIENT_0.id,
username: USERNAME,
password: ''
};
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(401);
done();
});
});
it('cannot login with wrong password', function (done) {
var body = {
clientId: CLIENT_0.id,
username: USERNAME,
password: PASSWORD+PASSWORD
};
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails for unkown app', function (done) {
var body = {
clientId: CLIENT_0.id,
username: USERNAME,
password: PASSWORD
};
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails for disallowed app', function (done) {
var body = {
clientId: CLIENT_1.id,
username: USERNAME,
password: PASSWORD
};
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(401);
done();
});
});
it('succeeds for allowed app', function (done) {
var body = {
clientId: CLIENT_2.id,
username: USERNAME,
password: PASSWORD
};
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(200);
expect(result.body.accessToken).to.be.a('string');
expect(result.body.user).to.be.an('object');
expect(result.body.user.id).to.be.a('string');
expect(result.body.user.username).to.be.a('string');
expect(result.body.user.email).to.be.a('string');
expect(result.body.user.admin).to.be.a('boolean');
request.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: result.body.accessToken })
.end(function (error, result) {
expect(error).to.be(null);
expect(result.body).to.be.an('object');
expect(result.body.username).to.eql(USERNAME);
done();
});
});
});
it('succeeds for app without accessRestriction', function (done) {
var body = {
clientId: CLIENT_3.id,
username: USERNAME,
password: PASSWORD
};
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(200);
expect(result.body.accessToken).to.be.a('string');
expect(result.body.user).to.be.an('object');
expect(result.body.user.id).to.be.a('string');
expect(result.body.user.username).to.be.a('string');
expect(result.body.user.email).to.be.a('string');
expect(result.body.user.admin).to.be.a('boolean');
request.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: result.body.accessToken })
.end(function (error, result) {
expect(error).to.be(null);
expect(result.body).to.be.an('object');
expect(result.body.username).to.eql(USERNAME);
done();
});
});
});
it('fails for wrong client credentials', function (done) {
var body = {
clientId: CLIENT_4.id,
username: USERNAME,
password: PASSWORD
};
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(401);
done();
});
});
});
describe('logout', function () {
var accessToken;
before(function (done) {
var body = {
clientId: CLIENT_3.id,
username: USERNAME,
password: PASSWORD
};
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(200);
accessToken = result.body.accessToken;
done();
});
});
it('fails without access_token', function (done) {
request.get(SIMPLE_AUTH_ORIGIN + '/api/v1/logout')
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with unkonwn access_token', function (done) {
request.get(SIMPLE_AUTH_ORIGIN + '/api/v1/logout')
.query({ access_token: accessToken+accessToken })
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(401);
done();
});
});
it('succeeds', function (done) {
request.get(SIMPLE_AUTH_ORIGIN + '/api/v1/logout')
.query({ access_token: accessToken })
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(200);
request.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: accessToken })
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(401);
done();
});
});
});
});
});
+25 -8
View File
@@ -14,7 +14,7 @@ root_password=secret
start_postgresql() {
postgresql_vars="POSTGRESQL_ROOT_PASSWORD=${root_password}; POSTGRESQL_ROOT_HOST=172.17.0.0/255.255.0.0"
if which boot2docker >/dev/null; then
if which boot2docker >/dev/null 2>&1; then
boot2docker ssh "sudo rm -rf /tmp/postgresql_vars.sh"
boot2docker ssh "echo \"${postgresql_vars}\" > /tmp/postgresql_vars.sh"
else
@@ -24,13 +24,15 @@ start_postgresql() {
docker rm -f postgresql 2>/dev/null 1>&2 || true
docker run -dtP --name=postgresql -v "${postgresqldatadir}:/var/lib/postgresql" -v /tmp/postgresql_vars.sh:/etc/postgresql/postgresql_vars.sh "${POSTGRESQL_IMAGE}" >/dev/null
docker run -dtP --name=postgresql -v "${postgresqldatadir}:/var/lib/postgresql" \
--read-only -v /tmp -v /run \
-v /tmp/postgresql_vars.sh:/etc/postgresql/postgresql_vars.sh "${POSTGRESQL_IMAGE}" >/dev/null
}
start_mysql() {
local mysql_vars="MYSQL_ROOT_PASSWORD=${root_password}; MYSQL_ROOT_HOST=172.17.0.0/255.255.0.0"
if which boot2docker >/dev/null; then
if which boot2docker >/dev/null 2>&1; then
boot2docker ssh "sudo rm -rf /tmp/mysql_vars.sh"
boot2docker ssh "echo \"${mysql_vars}\" > /tmp/mysql_vars.sh"
else
@@ -40,13 +42,15 @@ start_mysql() {
docker rm -f mysql 2>/dev/null 1>&2 || true
docker run -dP --name=mysql -v "${mysqldatadir}:/var/lib/mysql" -v /tmp/mysql_vars.sh:/etc/mysql/mysql_vars.sh "${MYSQL_IMAGE}" >/dev/null
docker run -dP --name=mysql -v "${mysqldatadir}:/var/lib/mysql" \
--read-only -v /tmp -v /run \
-v /tmp/mysql_vars.sh:/etc/mysql/mysql_vars.sh "${MYSQL_IMAGE}" >/dev/null
}
start_mongodb() {
local mongodb_vars="MONGODB_ROOT_PASSWORD=${root_password}"
if which boot2docker >/dev/null; then
if which boot2docker >/dev/null 2>&1; then
boot2docker ssh "sudo rm -rf /tmp/mongodb_vars.sh"
boot2docker ssh "echo \"${mongodb_vars}\" > /tmp/mongodb_vars.sh"
else
@@ -56,16 +60,29 @@ start_mongodb() {
docker rm -f mongodb 2>/dev/null 1>&2 || true
docker run -dP --name=mongodb -v "${mongodbdatadir}:/var/lib/mongodb" -v /tmp/mongodb_vars.sh:/etc/mongodb_vars.sh "${MONGODB_IMAGE}" >/dev/null
docker run -dP --name=mongodb -v "${mongodbdatadir}:/var/lib/mongodb" \
--read-only -v /tmp -v /run \
-v /tmp/mongodb_vars.sh:/etc/mongodb_vars.sh "${MONGODB_IMAGE}" >/dev/null
}
start_mail() {
local mongodb_vars="MONGODB_ROOT_PASSWORD=${root_password}"
docker rm -f mail 2>/dev/null 1>&2 || true
docker run -dP --name=mail -e DOMAIN_NAME="localhost" \
--read-only -v /tmp -v /run \
-v /tmp/maildata:/app/data "${MAIL_IMAGE}" >/dev/null
}
start_mysql
start_postgresql
start_mongodb
start_mail
echo -n "Waiting for addons to start"
for i in {1..10}; do
echo -n "."
for i in {1..20}; do
echo -n "."
sleep 1
done
echo ""
+192
View File
@@ -0,0 +1,192 @@
'use strict';
exports = module.exports = {
sync: sync
};
var appdb = require('./appdb.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
config = require('./config.js'),
CronJob = require('cron').CronJob,
debug = require('debug')('box:src/scheduler'),
docker = require('./docker.js'),
paths = require('./paths.js'),
safe = require('safetydance'),
_ = require('underscore');
var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
// appId -> { schedulerConfig (manifest), cronjobs, containerIds }
var gState = (function loadState() {
var state = safe.JSON.parse(safe.fs.readFileSync(paths.SCHEDULER_FILE, 'utf8'));
return state || { };
})();
function saveState(state) {
// do not save cronJobs
var safeState = { };
for (var appId in state) {
safeState[appId] = {
schedulerConfig: state[appId].schedulerConfig,
containerIds: state[appId].containerIds
};
}
safe.fs.writeFileSync(paths.SCHEDULER_FILE, JSON.stringify(safeState, null, 4), 'utf8');
}
function sync(callback) {
assert(!callback || typeof callback === 'function');
callback = callback || NOOP_CALLBACK;
debug('Syncing');
apps.getAll(function (error, allApps) {
if (error) return callback(error);
// stop tasks of apps that went away
var allAppIds = allApps.map(function (app) { return app.id; });
var removedAppIds = _.difference(Object.keys(gState), allAppIds);
async.eachSeries(removedAppIds, function (appId, iteratorDone) {
stopJobs(appId, gState[appId], true /* killContainers */, iteratorDone);
}, function (error) {
if (error) debug('Error stopping jobs : %j', error);
gState = _.omit(gState, removedAppIds);
// start tasks of new apps
async.eachSeries(allApps, function (app, iteratorDone) {
var appState = gState[app.id] || null;
var schedulerConfig = app.manifest.addons.scheduler || null;
if (!appState && !schedulerConfig) return iteratorDone(); // nothing changed
if (appState && _.isEqual(appState.schedulerConfig, schedulerConfig) && appState.cronJobs) {
return iteratorDone(); // nothing changed
}
var killContainers = appState && !appState.cronJobs ? true : false; // keep the old containers on 'startup'
stopJobs(app.id, appState, killContainers, function (error) {
if (error) debug('Error stopping jobs for %s : %s', app.id, error.message);
if (!schedulerConfig) {
delete gState[app.id];
return iteratorDone();
}
gState[app.id] = {
schedulerConfig: schedulerConfig,
cronJobs: createCronJobs(app.id, schedulerConfig),
containerIds: { }
};
saveState(gState);
iteratorDone();
});
});
debug('Done syncing');
});
});
}
function killContainer(containerId, callback) {
if (!containerId) return callback();
async.series([
docker.stopContainer.bind(null, containerId),
docker.deleteContainer.bind(null, containerId)
], function (error) {
if (error) debug('Failed to kill task with containerId %s : %s', containerId, error.message);
callback(error);
});
}
function stopJobs(appId, appState, killContainers, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof appState, 'object');
assert.strictEqual(typeof killContainers, 'boolean');
assert.strictEqual(typeof callback, 'function');
debug('stopJobs for %s', appId);
if (!appState) return callback();
async.eachSeries(Object.keys(appState.schedulerConfig), function (taskName, iteratorDone) {
if (appState.cronJobs && appState.cronJobs[taskName]) { // could be null across restarts
appState.cronJobs[taskName].stop();
}
if (!killContainers) return iteratorDone();
killContainer(appState.containerIds[taskName], iteratorDone);
}, callback);
}
function createCronJobs(appId, schedulerConfig) {
assert.strictEqual(typeof appId, 'string');
assert(schedulerConfig && typeof schedulerConfig === 'object');
debug('creating cron jobs for app %s', appId);
var jobs = { };
Object.keys(schedulerConfig).forEach(function (taskName) {
var task = schedulerConfig[taskName];
var cronTime = (config.TEST ? '*/5 ' : '00 ') + task.schedule; // time ticks faster in tests
debug('scheduling task for %s/%s @ %s : %s', appId, taskName, cronTime, task.command);
var cronJob = new CronJob({
cronTime: cronTime, // at this point, the pattern has been validated
onTick: doTask.bind(null, appId, taskName),
start: true
});
jobs[taskName] = cronJob;
});
return jobs;
}
function doTask(appId, taskName, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof taskName, 'string');
assert(!callback || typeof callback === 'function');
callback = callback || NOOP_CALLBACK;
var appState = gState[appId];
debug('Executing task %s/%s', appId, taskName);
apps.get(appId, function (error, app) {
if (error) return callback(error);
if (app.installationState !== appdb.ISTATE_INSTALLED || app.runState !== appdb.RSTATE_RUNNING) {
debug('task %s skipped. app %s is not installed/running', taskName, app.id);
return callback();
}
if (appState.containerIds[taskName]) debug('task %s/%s has existing container %s. killing it', appId, taskName, appState.containerIds[taskName]);
killContainer(appState.containerIds[taskName], function (error) {
if (error) return callback(error);
debug('Creating createSubcontainer for %s/%s : %s', app.id, taskName, gState[appId].schedulerConfig[taskName].command);
docker.createSubcontainer(app, [ '/bin/sh', '-c', gState[appId].schedulerConfig[taskName].command ], function (error, container) {
appState.containerIds[taskName] = container.id;
saveState(gState);
docker.startContainer(container.id, callback);
});
});
});
}
+4 -10
View File
@@ -43,11 +43,7 @@ function initializeExpressSync() {
app.set('view options', { layout: true, debug: true });
app.set('view engine', 'ejs');
if (process.env.BOX_ENV === 'test') {
app.use(express.static(path.join(__dirname, '/../webadmin')));
} else {
app.use(middleware.morgan('dev', { immediate: false }));
}
if (process.env.BOX_ENV !== 'test') app.use(middleware.morgan('Box :method :url :status :response-time ms - :res[content-length]', { immediate: false }));
var router = new express.Router();
router.del = router.delete; // amend router.del for readability further on
@@ -120,7 +116,6 @@ function initializeExpressSync() {
router.post('/api/v1/session/login', csrf, routes.oauth2.login);
router.get ('/api/v1/session/logout', routes.oauth2.logout);
router.get ('/api/v1/session/callback', routes.oauth2.callback);
router.get ('/api/v1/session/error', routes.oauth2.error);
router.get ('/api/v1/session/password/resetRequest.html', csrf, routes.oauth2.passwordResetRequestSite);
router.post('/api/v1/session/password/resetRequest', csrf, routes.oauth2.passwordResetRequest);
router.get ('/api/v1/session/password/sent.html', routes.oauth2.passwordSentSite);
@@ -130,13 +125,11 @@ function initializeExpressSync() {
// oauth2 routes
router.get ('/api/v1/oauth/dialog/authorize', routes.oauth2.authorization);
router.post('/api/v1/oauth/dialog/authorize/decision', csrf, routes.oauth2.decision);
router.post('/api/v1/oauth/token', routes.oauth2.token);
router.get ('/api/v1/oauth/clients', settingsScope, routes.clients.getAllByUserId);
router.post('/api/v1/oauth/clients', routes.developer.enabled, settingsScope, routes.clients.add);
router.get ('/api/v1/oauth/clients/:clientId', routes.developer.enabled, settingsScope, routes.clients.get);
router.post('/api/v1/oauth/clients/:clientId', routes.developer.enabled, settingsScope, routes.clients.add);
router.put ('/api/v1/oauth/clients/:clientId', routes.developer.enabled, settingsScope, routes.clients.update);
router.del ('/api/v1/oauth/clients/:clientId', routes.developer.enabled, settingsScope, routes.clients.del);
router.get ('/api/v1/oauth/clients/:clientId/tokens', settingsScope, routes.clients.getClientTokens);
router.del ('/api/v1/oauth/clients/:clientId/tokens', settingsScope, routes.clients.delClientTokens);
@@ -144,7 +137,7 @@ function initializeExpressSync() {
// app routes
router.get ('/api/v1/apps', appsScope, routes.apps.getApps);
router.get ('/api/v1/apps/:id', appsScope, routes.apps.getApp);
router.get ('/api/v1/apps/:id/icon', appsScope, routes.apps.getAppIcon);
router.get ('/api/v1/apps/:id/icon', routes.apps.getAppIcon);
router.post('/api/v1/apps/install', appsScope, routes.user.requireAdmin, routes.apps.installApp);
router.post('/api/v1/apps/:id/uninstall', appsScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.apps.uninstallApp);
@@ -199,6 +192,7 @@ function initializeExpressSync() {
return httpServer;
}
// provides hooks for the 'installer'
function initializeInternalExpressSync() {
var app = express();
var httpServer = http.createServer(app);
@@ -209,7 +203,7 @@ function initializeInternalExpressSync() {
var json = middleware.json({ strict: true, limit: QUERY_LIMIT }), // application/json
urlencoded = middleware.urlencoded({ extended: false, limit: QUERY_LIMIT }); // application/x-www-form-urlencoded
app.use(middleware.morgan('dev', { immediate: false }));
if (process.env.BOX_ENV !== 'test') app.use(middleware.morgan('Box Internal :method :url :status :response-time ms - :res[content-length]', { immediate: false }));
var router = new express.Router();
router.del = router.delete; // amend router.del for readability further on
+1 -4
View File
@@ -42,12 +42,9 @@ var assert = require('assert'),
_ = require('underscore');
var gDefaults = (function () {
var tz = safe.fs.readFileSync('/etc/timezone', 'utf8');
tz = tz ? tz.trim() : 'America/Los_Angeles';
var result = { };
result[exports.AUTOUPDATE_PATTERN_KEY] = '00 00 1,3,5,23 * * *';
result[exports.TIME_ZONE_KEY] = tz;
result[exports.TIME_ZONE_KEY] = 'America/Los_Angeles';
result[exports.CLOUDRON_NAME_KEY] = 'Cloudron';
result[exports.DEVELOPER_MODE_KEY] = false;
+155
View File
@@ -0,0 +1,155 @@
'use strict';
exports = module.exports = {
start: start,
stop: stop
};
var apps = require('./apps.js'),
AppsError = apps.AppsError,
assert = require('assert'),
clientdb = require('./clientdb.js'),
clients = require('./clients.js'),
ClientsError = clients.ClientsError,
config = require('./config.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:src/simpleauth'),
express = require('express'),
http = require('http'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
middleware = require('./middleware'),
tokendb = require('./tokendb.js'),
user = require('./user.js'),
UserError = require('./user.js').UserError;
var gHttpServer = null;
function loginLogic(clientId, username, password, callback) {
assert.strictEqual(typeof clientId, 'string');
assert.strictEqual(typeof username, 'string');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof callback, 'function');
debug('login: client %s and user %s', clientId, username);
clients.get(clientId, function (error, clientObject) {
if (error) return callback(error);
// only allow simple auth clients
if (clientObject.type !== clientdb.TYPE_SIMPLE_AUTH) return callback(new ClientsError(ClientsError.INVALID_CLIENT));
user.verify(username, password, function (error, userObject) {
if (error) return callback(error);
apps.get(clientObject.appId, function (error, appObject) {
if (error) return callback(error);
if (!apps.hasAccessTo(appObject, userObject)) return callback(new AppsError(AppsError.ACCESS_DENIED));
var accessToken = tokendb.generateToken();
var expires = Date.now() + 24 * 60 * 60 * 1000; // 1 day
tokendb.add(accessToken, tokendb.PREFIX_USER + userObject.id, clientId, expires, clientObject.scope, function (error) {
if (error) return callback(error);
debug('login: new access token for client %s and user %s: %s', clientId, username, accessToken);
callback(null, { accessToken: accessToken, user: userObject });
});
});
});
});
}
function logoutLogic(accessToken, callback) {
assert.strictEqual(typeof accessToken, 'string');
assert.strictEqual(typeof callback, 'function');
debug('logout: %s', accessToken);
tokendb.del(accessToken, function (error) {
if (error) return callback(error);
callback(null);
});
}
function login(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.clientId !== 'string') return next(new HttpError(400, 'clientId is required'));
if (typeof req.body.username !== 'string') return next(new HttpError(400, 'username is required'));
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'password is required'));
loginLogic(req.body.clientId, req.body.username, req.body.password, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new HttpError(401, 'Unknown client'));
if (error && error.reason === ClientsError.INVALID_CLIENT) return next(new HttpError(401, 'Unkown client'));
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(401, 'Forbidden'));
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(401, 'Unkown app'));
if (error && error.reason === UserError.WRONG_PASSWORD) return next(new HttpError(401, 'Forbidden'));
if (error && error.reason === AppsError.ACCESS_DENIED) return next(new HttpError(401, 'Forbidden'));
if (error) return next(new HttpError(500, error));
var tmp = {
accessToken: result.accessToken,
user: {
id: result.user.id,
username: result.user.username,
email: result.user.email,
admin: !!result.user.admin
}
};
next(new HttpSuccess(200, tmp));
});
}
function logout(req, res, next) {
assert.strictEqual(typeof req.query, 'object');
if (typeof req.query.access_token !== 'string') return next(new HttpError(400, 'access_token in query required'));
logoutLogic(req.query.access_token, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new HttpError(401, 'Forbidden'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, {}));
});
}
function initializeExpressSync() {
var app = express();
var httpServer = http.createServer(app);
httpServer.on('error', console.error);
var json = middleware.json({ strict: true, limit: '100kb' });
var router = new express.Router();
// basic auth
router.post('/api/v1/login', login);
router.get ('/api/v1/logout', logout);
if (process.env.BOX_ENV !== 'test') app.use(middleware.morgan('SimpleAuth :method :url :status :response-time ms - :res[content-length]', { immediate: false }));
app
.use(middleware.timeout(10000))
.use(json)
.use(router)
.use(middleware.lastMile());
return httpServer;
}
function start(callback) {
assert.strictEqual(typeof callback, 'function');
gHttpServer = initializeExpressSync();
gHttpServer.listen(config.get('simpleAuthPort'), '0.0.0.0', callback);
}
function stop(callback) {
assert.strictEqual(typeof callback, 'function');
gHttpServer.close(callback);
}
+1 -7
View File
@@ -28,12 +28,6 @@ function SubdomainError(reason, errorOrMessage) {
util.inherits(SubdomainError, Error);
SubdomainError.NOT_FOUND = 'No such domain';
SubdomainError.INTERNAL_ERROR = 'Internal error';
SubdomainError.EXTERNAL_ERROR = 'External error';
SubdomainError.STILL_BUSY = 'Still busy';
SubdomainError.FAILED_TOO_OFTEN = 'Failed too often';
SubdomainError.ALREADY_EXISTS = 'Domain already exists';
SubdomainError.BAD_FIELD = 'Bad Field';
SubdomainError.BAD_STATE = 'Bad State';
SubdomainError.INVALID_ZONE_NAME = 'Invalid domain name';
SubdomainError.INVALID_TASK = 'Invalid task';
SubdomainError.MISSING_CREDENTIALS = 'Missing credentials';
+10 -5
View File
@@ -5,6 +5,7 @@
var assert = require('assert'),
async = require('async'),
aws = require('./aws.js'),
caas = require('./caas.js'),
config = require('./config.js'),
debug = require('debug')('box:subdomains'),
util = require('util'),
@@ -17,6 +18,12 @@ module.exports = exports = {
status: status
};
// choose which subdomain backend we use
// for test purpose we use aws
function api() {
return config.token() && !config.TEST ? caas : aws;
}
function add(record, callback) {
assert.strictEqual(typeof record, 'object');
assert.strictEqual(typeof record.subdomain, 'string');
@@ -26,7 +33,7 @@ function add(record, callback) {
debug('add: ', record);
aws.addSubdomain(config.zoneName(), record.subdomain, record.type, record.value, function (error, changeId) {
api().addSubdomain(config.zoneName(), record.subdomain, record.type, record.value, function (error, changeId) {
if (error) return callback(error);
callback(null, changeId);
});
@@ -60,7 +67,7 @@ function remove(record, callback) {
debug('remove: ', record);
aws.delSubdomain(config.zoneName(), record.subdomain, record.type, record.value, function (error) {
api().delSubdomain(config.zoneName(), record.subdomain, record.type, record.value, function (error) {
if (error && error.reason !== SubdomainError.NOT_FOUND) return callback(error);
debug('deleteSubdomain: successfully deleted subdomain from aws.');
@@ -73,9 +80,7 @@ function status(changeId, callback) {
assert.strictEqual(typeof changeId, 'string');
assert.strictEqual(typeof callback, 'function');
debug('status: ', changeId);
aws.getChangeStatus(changeId, function (error, status) {
api().getChangeStatus(changeId, function (error, status) {
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error));
callback(null, status === 'INSYNC' ? 'done' : 'pending');
});
+12 -2
View File
@@ -28,6 +28,8 @@ function initialize(callback) {
if (error) return callback(error);
apps.forEach(function (app) {
if (app.installationState === appdb.ISTATE_INSTALLED && app.runState === appdb.RSTATE_RUNNING) return;
debug('Creating process for %s (%s) with state %s', app.location, app.id, app.installationState);
startAppTask(app.id);
});
@@ -46,6 +48,8 @@ function uninitialize(callback) {
stopAppTask(appId);
}
locker.removeListener('unlocked', startNextTask);
callback(null);
}
@@ -61,10 +65,16 @@ function startAppTask(appId) {
assert.strictEqual(typeof appId, 'string');
assert(!(appId in gActiveTasks));
if (Object.keys(gActiveTasks).length >= TASK_CONCURRENCY) {
debug('Reached concurrency limit, queueing task for %s', appId);
gPendingTasks.push(appId);
return;
}
var lockError = locker.recursiveLock(locker.OP_APPTASK);
if (lockError || Object.keys(gActiveTasks).length >= TASK_CONCURRENCY) {
debug('Reached concurrency limit, queueing task for %s', appId);
if (lockError) {
debug('Locked for another operation, queueing task for %s', appId);
gPendingTasks.push(appId);
return;
}
+47 -3
View File
@@ -36,14 +36,15 @@ describe('Apps', function () {
containerId: null,
portBindings: { PORT: 5678 },
healthy: null,
accessRestriction: ''
accessRestriction: null,
oauthProxy: false
};
before(function (done) {
async.series([
database.initialize,
database._clear,
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction)
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.oauthProxy)
], done);
});
@@ -157,5 +158,48 @@ describe('Apps', function () {
});
});
});
});
describe('validateAccessRestriction', function () {
it('allows null input', function () {
expect(apps._validateAccessRestriction(null)).to.eql(null);
});
it('does not allow wrong user type', function () {
expect(apps._validateAccessRestriction({ users: {} })).to.be.an(Error);
});
it('does not allow no user input', function () {
expect(apps._validateAccessRestriction({ users: [] })).to.be.an(Error);
});
it('allows single user input', function () {
expect(apps._validateAccessRestriction({ users: [ 'someuserid' ] })).to.eql(null);
});
it('allows multi user input', function () {
expect(apps._validateAccessRestriction({ users: [ 'someuserid', 'someuserid1', 'someuserid2', 'someuserid3' ] })).to.eql(null);
});
});
describe('hasAccessTo', function () {
it('returns true for unrestricted access', function () {
expect(apps.hasAccessTo({ accessRestriction: null }, { id: 'someuser' })).to.equal(true);
});
it('returns true for allowed user', function () {
expect(apps.hasAccessTo({ accessRestriction: { users: [ 'someuser' ] } }, { id: 'someuser' })).to.equal(true);
});
it('returns true for allowed user with multiple allowed', function () {
expect(apps.hasAccessTo({ accessRestriction: { users: [ 'foo', 'someuser', 'anotheruser' ] } }, { id: 'someuser' })).to.equal(true);
});
it('returns false for not allowed user', function () {
expect(apps.hasAccessTo({ accessRestriction: { users: [ 'foo' ] } }, { id: 'someuser' })).to.equal(false);
});
it('returns false for not allowed user with multiple allowed', function () {
expect(apps.hasAccessTo({ accessRestriction: { users: [ 'foo', 'anotheruser' ] } }, { id: 'someuser' })).to.equal(false);
});
});
});
+45 -13
View File
@@ -13,10 +13,10 @@ var addons = require('../addons.js'),
database = require('../database.js'),
expect = require('expect.js'),
fs = require('fs'),
js2xml = require('js2xmlparser'),
net = require('net'),
nock = require('nock'),
paths = require('../paths.js'),
sysinfo = require('../sysinfo.js'),
_ = require('underscore');
var MANIFEST = {
@@ -29,7 +29,7 @@ var MANIFEST = {
"contactEmail": "support@cloudron.io",
"version": "0.1.0",
"manifestVersion": 1,
"dockerImage": "girish/test:0.2.0",
"dockerImage": "cloudron/test:8.0.0",
"healthCheckPath": "/",
"httpPort": 7777,
"tcpPorts": {
@@ -57,16 +57,32 @@ var APP = {
containerId: null,
httpPort: 4567,
portBindings: null,
accessRestriction: '',
accessRestriction: null,
oauthProxy: false,
dnsRecordId: 'someDnsRecordId'
};
var awsHostedZones = {
HostedZones: [{
Id: '/hostedzone/ZONEID',
Name: 'localhost.',
CallerReference: '305AFD59-9D73-4502-B020-F4E6F889CB30',
ResourceRecordSetCount: 2,
ChangeInfo: {
Id: '/change/CKRTFJA0ANHXB',
Status: 'INSYNC'
}
}],
IsTruncated: false,
MaxItems: '100'
};
describe('apptask', function () {
before(function (done) {
config.set('version', '0.5.0');
database.initialize(function (error) {
expect(error).to.be(null);
appdb.add(APP.id, APP.appStoreId, APP.manifest, APP.location, APP.portBindings, APP.accessRestriction, done);
appdb.add(APP.id, APP.appStoreId, APP.manifest, APP.location, APP.portBindings, APP.accessRestriction, APP.oauthProxy, done);
});
});
@@ -121,21 +137,21 @@ describe('apptask', function () {
});
it('allocate OAuth credentials', function (done) {
addons._allocateOAuthCredentials(APP, function (error) {
addons._setupOauth(APP, {}, function (error) {
expect(error).to.be(null);
done();
});
});
it('remove OAuth credentials', function (done) {
addons._removeOAuthCredentials(APP, function (error) {
addons._teardownOauth(APP, {}, function (error) {
expect(error).to.be(null);
done();
});
});
it('remove OAuth credentials twice succeeds', function (done) {
addons._removeOAuthCredentials(APP, function (error) {
addons._teardownOauth(APP, {}, function (error) {
expect(!error).to.be.ok();
done();
});
@@ -154,7 +170,7 @@ describe('apptask', function () {
it('barfs on bad manifest', function (done) {
var badApp = _.extend({ }, APP);
badApp.manifest = _.extend({ }, APP.manifest);
delete badApp.manifest['id'];
delete badApp.manifest.id;
apptask._verifyManifest(badApp, function (error) {
expect(error).to.be.ok();
@@ -185,12 +201,20 @@ describe('apptask', function () {
it('registers subdomain', function (done) {
nock.cleanAll();
var scope = nock(config.apiServerOrigin())
.post('/api/v1/subdomains?token=' + config.token(), { records: [ { subdomain: APP.location, type: 'A', value: sysinfo.getIp() } ] })
.reply(201, { ids: [ APP.dnsRecordId ] });
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=APPSTORE_TOKEN')
.times(2)
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
var awsScope = nock(config.aws().endpoint)
.get('/2013-04-01/hostedzone')
.reply(200, js2xml('ListHostedZonesResponse', awsHostedZones, { arrayMap: { HostedZones: 'HostedZone'} }))
.post('/2013-04-01/hostedzone/ZONEID/rrset/')
.reply(200, js2xml('ChangeResourceRecordSetsResponse', { ChangeInfo: { Id: 'RRID', Status: 'INSYNC' } }));
apptask._registerSubdomain(APP, function (error) {
expect(error).to.be(null);
expect(scope.isDone()).to.be.ok();
expect(awsScope.isDone()).to.be.ok();
done();
});
});
@@ -198,12 +222,20 @@ describe('apptask', function () {
it('unregisters subdomain', function (done) {
nock.cleanAll();
var scope = nock(config.apiServerOrigin())
.delete('/api/v1/subdomains/' + APP.dnsRecordId + '?token=' + config.token())
.reply(204, {});
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=APPSTORE_TOKEN')
.times(2)
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
apptask._unregisterSubdomain(APP, function (error) {
var awsScope = nock(config.aws().endpoint)
.get('/2013-04-01/hostedzone')
.reply(200, js2xml('ListHostedZonesResponse', awsHostedZones, { arrayMap: { HostedZones: 'HostedZone'} }))
.post('/2013-04-01/hostedzone/ZONEID/rrset/')
.reply(200, js2xml('ChangeResourceRecordSetsResponse', { ChangeInfo: { Id: 'RRID', Status: 'INSYNC' } }));
apptask._unregisterSubdomain(APP, APP.location, function (error) {
expect(error).to.be(null);
expect(scope.isDone()).to.be.ok();
expect(awsScope.isDone()).to.be.ok();
done();
});
});
+2 -2
View File
@@ -34,8 +34,8 @@ for script in "${scripts[@]}"; do
fi
done
if ! docker inspect girish/test:0.2.0 >/dev/null 2>/dev/null; then
echo "docker pull girish/test:0.2.0 for tests to run"
if ! docker inspect cloudron/test:8.0.0 >/dev/null 2>/dev/null; then
echo "docker pull cloudron/test:8.0.0 for tests to run"
exit 1
fi
+38 -33
View File
@@ -478,7 +478,8 @@ describe('database', function () {
containerId: null,
portBindings: { port: 5678 },
health: null,
accessRestriction: '',
accessRestriction: null,
oauthProxy: false,
lastBackupId: null,
lastBackupConfig: null,
oldConfig: null
@@ -496,7 +497,8 @@ describe('database', function () {
containerId: null,
portBindings: { },
health: null,
accessRestriction: 'roleAdmin',
accessRestriction: { users: [ 'foobar' ] },
oauthProxy: true,
lastBackupId: null,
lastBackupConfig: null,
oldConfig: null
@@ -516,7 +518,7 @@ describe('database', function () {
});
it('add succeeds', function (done) {
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, function (error) {
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0.accessRestriction, APP_0.oauthProxy, function (error) {
expect(error).to.be(null);
done();
});
@@ -540,7 +542,7 @@ describe('database', function () {
});
it('add of same app fails', function (done) {
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, [ ], APP_0.accessRestriction, function (error) {
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, [ ], APP_0.accessRestriction, APP_0.oauthProxy, function (error) {
expect(error).to.be.a(DatabaseError);
expect(error.reason).to.be(DatabaseError.ALREADY_EXISTS);
done();
@@ -569,10 +571,20 @@ describe('database', function () {
APP_0.installationState = 'some-other-status';
APP_0.location = 'some-other-location';
APP_0.manifest.version = '0.2';
APP_0.accessRestriction = true;
APP_0.accessRestriction = '';
APP_0.oauthProxy = true;
APP_0.httpPort = 1337;
appdb.update(APP_0.id, { installationState: APP_0.installationState, location: APP_0.location, manifest: APP_0.manifest, accessRestriction: APP_0.accessRestriction, httpPort: APP_0.httpPort }, function (error) {
var data = {
installationState: APP_0.installationState,
location: APP_0.location,
manifest: APP_0.manifest,
accessRestriction: APP_0.accessRestriction,
oauthProxy: APP_0.oauthProxy,
httpPort: APP_0.httpPort
};
appdb.update(APP_0.id, data, function (error) {
expect(error).to.be(null);
appdb.get(APP_0.id, function (error, result) {
@@ -602,7 +614,7 @@ describe('database', function () {
});
it('add second app succeeds', function (done) {
appdb.add(APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, [ ], APP_1.accessRestriction, function (error) {
appdb.add(APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, [ ], APP_1.accessRestriction, APP_0.oauthProxy, function (error) {
expect(error).to.be(null);
done();
});
@@ -778,6 +790,7 @@ describe('database', function () {
var CLIENT_0 = {
id: 'cid-0',
appId: 'someappid_0',
type: clientdb.TYPE_OAUTH,
clientSecret: 'secret-0',
redirectURI: 'http://foo.bar',
scope: '*'
@@ -786,23 +799,17 @@ describe('database', function () {
var CLIENT_1 = {
id: 'cid-1',
appId: 'someappid_1',
type: clientdb.TYPE_OAUTH,
clientSecret: 'secret-',
redirectURI: 'http://foo.bar',
scope: '*'
};
var CLIENT_2 = {
id: 'cid-2',
appId: 'someappid_2',
clientSecret: 'secret-2',
redirectURI: 'http://foo.bar.baz',
scope: 'profile,roleUser'
};
it('add succeeds', function (done) {
clientdb.add(CLIENT_0.id, CLIENT_0.appId, CLIENT_0.clientSecret, CLIENT_0.redirectURI, CLIENT_0.scope, function (error) {
clientdb.add(CLIENT_0.id, CLIENT_0.appId, CLIENT_0.type, CLIENT_0.clientSecret, CLIENT_0.redirectURI, CLIENT_0.scope, function (error) {
expect(error).to.be(null);
clientdb.add(CLIENT_1.id, CLIENT_1.appId, CLIENT_1.clientSecret, CLIENT_1.redirectURI, CLIENT_1.scope, function (error) {
clientdb.add(CLIENT_1.id, CLIENT_1.appId, CLIENT_0.type, CLIENT_1.clientSecret, CLIENT_1.redirectURI, CLIENT_1.scope, function (error) {
expect(error).to.be(null);
done();
});
@@ -810,7 +817,7 @@ describe('database', function () {
});
it('add same client id fails', function (done) {
clientdb.add(CLIENT_0.id, CLIENT_0.appId, CLIENT_0.clientSecret, CLIENT_0.redirectURI, CLIENT_0.scope, function (error) {
clientdb.add(CLIENT_0.id, CLIENT_0.appId, CLIENT_0.type, CLIENT_0.clientSecret, CLIENT_0.redirectURI, CLIENT_0.scope, function (error) {
expect(error).to.be.a(DatabaseError);
expect(error.reason).to.equal(DatabaseError.ALREADY_EXISTS);
done();
@@ -833,6 +840,14 @@ describe('database', function () {
});
});
it('getByAppIdAndType succeeds', function (done) {
clientdb.getByAppIdAndType(CLIENT_0.appId, CLIENT_0.type, function (error, result) {
expect(error).to.be(null);
expect(result).to.eql(CLIENT_0);
done();
});
});
it('getByAppId fails for unknown client id', function (done) {
clientdb.getByAppId(CLIENT_0.appId + CLIENT_0.appId, function (error, result) {
expect(error).to.be.a(DatabaseError);
@@ -853,24 +868,14 @@ describe('database', function () {
});
});
it('update client fails due to unknown client id', function (done) {
clientdb.update(CLIENT_2.id, CLIENT_2.appId, CLIENT_2.clientSecret, CLIENT_2.redirectURI, CLIENT_2.scope, function (error) {
expect(error).to.be.a(DatabaseError);
expect(error.reason).to.equal(DatabaseError.NOT_FOUND);
done();
});
});
it('update client succeeds', function (done) {
clientdb.update(CLIENT_1.id, CLIENT_2.appId, CLIENT_2.clientSecret, CLIENT_2.redirectURI, CLIENT_2.scope, function (error) {
it('delByAppIdAndType succeeds', function (done) {
clientdb.delByAppIdAndType(CLIENT_1.appId, CLIENT_1.type, function (error) {
expect(error).to.be(null);
clientdb.get(CLIENT_1.id, function (error, result) {
expect(error).to.be(null);
expect(result.appId).to.eql(CLIENT_2.appId);
expect(result.clientSecret).to.eql(CLIENT_2.clientSecret);
expect(result.redirectURI).to.eql(CLIENT_2.redirectURI);
expect(result.scope).to.eql(CLIENT_2.scope);
clientdb.getByAppIdAndType(CLIENT_1.appId, CLIENT_1.type, function (error, result) {
expect(error).to.be.a(DatabaseError);
expect(error.reason).to.equal(DatabaseError.NOT_FOUND);
expect(result).to.not.be.ok();
done();
});
});
+99
View File
@@ -0,0 +1,99 @@
/* jslint node:true */
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
'use strict';
var async = require('async'),
authcodedb = require('../authcodedb.js'),
database = require('../database'),
DatabaseError = require('../databaseerror.js'),
expect = require('expect.js'),
janitor = require('../janitor.js'),
tokendb = require('../tokendb.js');
describe('janitor', function () {
var AUTHCODE_0 = {
authCode: 'authcode-0',
clientId: 'clientid-0',
userId: 'userid-0',
expiresAt: Date.now() + 5000
};
var AUTHCODE_1 = {
authCode: 'authcode-1',
clientId: 'clientid-1',
userId: 'userid-1',
expiresAt: Date.now() - 5000
};
var TOKEN_0 = {
accessToken: tokendb.generateToken(),
identifier: tokendb.PREFIX_USER + '0',
clientId: 'clientid-0',
expires: Date.now() + 60 * 60000,
scope: '*'
};
var TOKEN_1 = {
accessToken: tokendb.generateToken(),
identifier: tokendb.PREFIX_USER + '1',
clientId: 'clientid-1',
expires: Date.now() - 1000,
scope: '*',
};
before(function (done) {
async.series([
database.initialize,
database._clear,
authcodedb.add.bind(null, AUTHCODE_0.authCode, AUTHCODE_0.clientId, AUTHCODE_0.userId, AUTHCODE_0.expiresAt),
authcodedb.add.bind(null, AUTHCODE_1.authCode, AUTHCODE_1.clientId, AUTHCODE_1.userId, AUTHCODE_1.expiresAt),
tokendb.add.bind(null, TOKEN_0.accessToken, TOKEN_0.identifier, TOKEN_0.clientId, TOKEN_0.expires, TOKEN_0.scope),
tokendb.add.bind(null, TOKEN_1.accessToken, TOKEN_1.identifier, TOKEN_1.clientId, TOKEN_1.expires, TOKEN_1.scope)
], done);
});
after(function (done) {
database._clear(done);
});
it('can cleanupTokens', function (done) {
janitor.cleanupTokens(done);
});
it('did not remove the non-expired authcode', function (done) {
authcodedb.get(AUTHCODE_0.authCode, function (error, result) {
expect(error).to.be(null);
expect(result).to.be.eql(AUTHCODE_0);
done();
});
});
it('did remove expired authcode', function (done) {
authcodedb.get(AUTHCODE_1.authCode, function (error, result) {
expect(error).to.be.a(DatabaseError);
expect(error.reason).to.be(DatabaseError.NOT_FOUND);
expect(result).to.not.be.ok();
done();
});
});
it('did not remove the non-expired token', function (done) {
tokendb.get(TOKEN_0.accessToken, function (error, result) {
expect(error).to.be(null);
expect(result).to.be.eql(TOKEN_0);
done();
});
});
it('did remove the non-expired token', function (done) {
tokendb.get(TOKEN_1.accessToken, function (error, result) {
expect(error).to.be.a(DatabaseError);
expect(error.reason).to.be(DatabaseError.NOT_FOUND);
expect(result).to.not.be.ok();
done();
});
});
});
-2
View File
@@ -6,8 +6,6 @@
'use strict';
require('supererror', { splatchError: true});
var database = require('../database.js'),
expect = require('expect.js'),
EventEmitter = require('events').EventEmitter,
+2 -2
View File
@@ -13,8 +13,8 @@ mkdir -p $HOME/.cloudron_test
cd $HOME/.cloudron_test
mkdir -p data/appdata data/box/appicons data/mail data/nginx/cert data/nginx/applications data/collectd/collectd.conf.d data/addons configs
webadmin_scopes="root,profile,users,apps,settings,roleAdmin"
webadmin_scopes="root,profile,users,apps,settings"
webadmin_origin="https://${ADMIN_LOCATION}-localhost"
mysql --user=root --password="" \
-e "REPLACE INTO clients (id, appId, clientSecret, redirectURI, scope) VALUES (\"cid-webadmin\", \"webadmin\", \"secret-webadmin\", \"${webadmin_origin}\", \"${webadmin_scopes}\")" boxtest
-e "REPLACE INTO clients (id, appId, type, clientSecret, redirectURI, scope) VALUES (\"cid-webadmin\", \"webadmin\", \"admin\", \"secret-webadmin\", \"${webadmin_origin}\", \"${webadmin_scopes}\")" boxtest
+32 -12
View File
@@ -10,24 +10,27 @@ exports = module.exports = {
};
var apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
config = require('./config.js'),
debug = require('debug')('box:updatechecker'),
fs = require('fs'),
mailer = require('./mailer.js'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
semver = require('semver'),
superagent = require('superagent'),
util = require('util');
var NOOP_CALLBACK = function (error) { console.error(error); };
var gAppUpdateInfo = { }, // id -> update info { creationDate, manifest }
gBoxUpdateInfo = null,
gMailedUser = { };
gBoxUpdateInfo = null;
function loadState() {
var state = safe.JSON.parse(safe.fs.readFileSync(paths.UPDATE_CHECKER_FILE, 'utf8'));
return state || { };
}
function saveState(mailedUser) {
safe.fs.writeFileSync(paths.UPDATE_CHECKER_FILE, JSON.stringify(mailedUser, null, 4), 'utf8');
}
function getUpdateInfo() {
return {
@@ -78,6 +81,9 @@ function getAppUpdates(callback) {
function getBoxUpdates(callback) {
var currentVersion = config.version();
// do not crash if boxVersionsUrl is not set
if (!config.get('boxVersionsUrl')) return callback(null, null);
superagent
.get(config.get('boxVersionsUrl'))
.timeout(10 * 1000)
@@ -122,13 +128,21 @@ function getBoxUpdates(callback) {
function checkAppUpdates() {
debug('Checking App Updates');
var oldState = loadState();
var newState = { box: oldState.box }; // creaee new state so that old app ids are removed
getAppUpdates(function (error, result) {
if (error) debug('Error checking app updates: ', error);
gAppUpdateInfo = error ? {} : result;
async.eachSeries(Object.keys(gAppUpdateInfo), function iterator(id, iteratorDone) {
if (gMailedUser[id]) return iteratorDone();
newState[id] = gAppUpdateInfo[id].manifest.version;
if (oldState[id] === gAppUpdateInfo[id].manifest.version) {
debug('Skipping notification of app update %s since user was already notified', id);
return iteratorDone();
}
apps.get(id, function (error, app) {
if (error) {
@@ -137,8 +151,10 @@ function checkAppUpdates() {
}
mailer.appUpdateAvailable(app, gAppUpdateInfo[id]);
gMailedUser[id] = true;
iteratorDone();
});
}, function () {
saveState(newState);
});
});
}
@@ -146,15 +162,19 @@ function checkAppUpdates() {
function checkBoxUpdates() {
debug('Checking Box Updates');
var state = loadState();
getBoxUpdates(function (error, result) {
if (error) debug('Error checking box updates: ', error);
gBoxUpdateInfo = error ? null : result;
if (gBoxUpdateInfo && !gMailedUser['box']) {
if (gBoxUpdateInfo && state.box !== gBoxUpdateInfo.version) {
mailer.boxUpdateAvailable(gBoxUpdateInfo.version, gBoxUpdateInfo.changelog);
gMailedUser['box'] = true;
state.box = gBoxUpdateInfo.version;
saveState(state);
} else {
debug('Skipping notification of box update as user was already notified');
}
});
}
+7 -7
View File
@@ -134,7 +134,7 @@ function createUser(username, password, email, admin, invitor, callback) {
crypto.pbkdf2(password, salt, CRYPTO_ITERATIONS, CRYPTO_KEY_LENGTH, function (error, derivedKey) {
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
var now = (new Date()).toUTCString();
var now = (new Date()).toISOString();
var user = {
id: username,
username: username,
@@ -203,17 +203,17 @@ function verifyWithEmail(email, password, callback) {
});
}
function removeUser(username, callback) {
assert.strictEqual(typeof username, 'string');
function removeUser(userId, callback) {
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
userdb.del(username, function (error) {
userdb.del(userId, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND));
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
callback(null);
mailer.userRemoved(username);
mailer.userRemoved(userId);
});
}
@@ -331,7 +331,7 @@ function setPassword(userId, newPassword, callback) {
crypto.pbkdf2(newPassword, saltBuffer, CRYPTO_ITERATIONS, CRYPTO_KEY_LENGTH, function (error, derivedKey) {
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
user.modifiedAt = (new Date()).toUTCString();
user.modifiedAt = (new Date()).toISOString();
user.password = new Buffer(derivedKey, 'binary').toString('hex');
user.resetToken = '';
@@ -340,7 +340,7 @@ function setPassword(userId, newPassword, callback) {
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
// Also generate a token so the new user can get logged in immediately
clientdb.getByAppId('webadmin', function (error, result) {
clientdb.getByAppIdAndType('webadmin', clientdb.TYPE_ADMIN, function (error, result) {
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
var token = tokendb.generateToken();
-40
View File
@@ -1,40 +0,0 @@
'use strict';
// we can possibly remove this entire file and make our tests
// smarter to just use the host interface provided by boot2docker
// https://github.com/boot2docker/boot2docker#container-port-redirection
// https://github.com/boot2docker/boot2docker/pull/93
// https://github.com/docker/docker/issues/4007
exports = module.exports = {
forwardFromHostToVirtualBox: forwardFromHostToVirtualBox,
unforwardFromHostToVirtualBox: unforwardFromHostToVirtualBox
};
var assert = require('assert'),
child_process = require('child_process'),
debug = require('debug')('box:vbox'),
os = require('os');
function forwardFromHostToVirtualBox(rulename, port) {
assert.strictEqual(typeof rulename, 'string');
assert.strictEqual(typeof port, 'number');
if (os.platform() === 'darwin') {
debug('Setting up VirtualBox port forwarding for '+ rulename + ' at ' + port);
child_process.exec(
'VBoxManage controlvm boot2docker-vm natpf1 delete ' + rulename + ';' +
'VBoxManage controlvm boot2docker-vm natpf1 ' + rulename + ',tcp,127.0.0.1,' + port + ',,' + port);
}
}
function unforwardFromHostToVirtualBox(rulename) {
assert.strictEqual(typeof rulename, 'string');
if (os.platform() === 'darwin') {
debug('Removing VirtualBox port forwarding for '+ rulename);
child_process.exec('VBoxManage controlvm boot2docker-vm natpf1 delete ' + rulename);
}
}
+14 -38
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html ng-app="Application" ng-controller="Controller">
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
@@ -13,41 +13,6 @@
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" type="text/css">
<link href="//fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet" type="text/css">
<!-- jQuery-->
<script src="3rdparty/js/jquery.min.js"></script>
<!-- Bootstrap Core JavaScript -->
<script src="3rdparty/js/bootstrap.min.js"></script>
<!-- Angularjs scripts -->
<script src="3rdparty/js/angular.min.js"></script>
<script src="3rdparty/js/angular-loader.min.js"></script>
<script>
'use strict';
// create main application module
var app = angular.module('Application', []);
app.controller('Controller', ['$scope', '$http', function ($scope, $http) {
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
$scope.cloudronName = 'Cloudron';
$scope.referrer = search.referrer || null;
// try to fetch cloudron status
$http.get('/api/v1/cloudron/status').success(function(data, status) {
if (status !== 200 || typeof data !== 'object') return console.error(status, data);
$scope.cloudronName = data.cloudronName;
document.title = $scope.cloudronName + ' App Error';
}).error(function (data, status) {
console.error(status, data);
});
}]);
</script>
</head>
<body class="status-page">
@@ -55,10 +20,9 @@
<div class="wrapper">
<div class="content">
<img src="/img/logo_inverted_192.png"/>
<h1> {{cloudronName}} </h1>
<h3> <i class="fa fa-frown-o fa-fw text-danger"></i> Something has gone wrong </h3>
This app is currently not running. <a href="{{ referrer }}">Please retry later</a>.
This app is currently not running. <a id="appLink" href="">Please retry later</a>.
<footer>
<span class="text-muted"><a href="mailto: support@cloudron.io">Contact Support</a> - Copyright &copy; Cloudron 2014-15</span>
@@ -66,5 +30,17 @@
</div>
</div>
<script>
(function () {
'use strict';
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
document.getElementById('appLink').href = search.referrer;
})();
</script>
</body>
</html>
+1 -11
View File
@@ -31,7 +31,6 @@
var app = angular.module('Application', []);
app.controller('Controller', ['$scope', '$http', function ($scope, $http) {
$scope.cloudronName = 'Cloudron';
$scope.webServerOriginLink = '/';
$scope.errorMessage = '';
@@ -44,15 +43,6 @@
else console.error(status, data);
});
// try to fetch cloudron status
$http.get('/api/v1/cloudron/status').success(function(data, status) {
if (status !== 200 || typeof data !== 'object') return console.error(status, data);
$scope.cloudronName = data.cloudronName;
document.title = $scope.cloudronName + ' Error';
}).error(function (data, status) {
console.error(status, data);
});
var search = window.location.search.slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
$scope.errorCode = search.errorCode || 0;
@@ -68,7 +58,7 @@
<div class="wrapper">
<div class="content">
<img src="/api/v1/cloudron/avatar" onerror="this.src = '/img/logo_inverted_192.png'"/>
<h1> {{cloudronName}} </h1>
<h1> Cloudron </h1>
<div ng-show="errorCode == 0">
<h3> <i class="fa fa-frown-o fa-fw text-danger"></i> Something has gone wrong </h3>
+29 -27
View File
@@ -8,7 +8,9 @@
<link href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
<!-- Theme CSS -->
<!-- CSS -->
<link rel="stylesheet" type="text/css" href="/3rdparty/slick.css"/>
<link rel="stylesheet" type="text/css" href="/3rdparty/angular-ui-notification.min.css"/>
<link href="theme.css" rel="stylesheet">
<!-- Custom Fonts -->
@@ -25,12 +27,8 @@
<script src="3rdparty/js/bootstrap.min.js"></script>
<!-- Slick carousel -->
<link rel="stylesheet" type="text/css" href="/3rdparty/slick.css"/>
<script type="text/javascript" src="3rdparty/js/slick.js"></script>
<!-- Additional stylesheets -->
<link rel="stylesheet" type="text/css" href="/3rdparty/angular-ui-notification.min.css"/>
<!-- Angularjs scripts -->
<script src="3rdparty/js/angular.min.js"></script>
<script src="3rdparty/js/angular-loader.min.js"></script>
@@ -75,27 +73,32 @@
<br/>
<br/>
</div>
<p>This update will install version <b>{{config.update.box.version}}</b> on your Cloudron.</p>
<p>Recent Changes:</p>
<ul>
<li ng-repeat="change in config.update.box.changelog">{{change}}</li>
</ul>
<br/>
<fieldset ng-show="installedApps | readyToUpdate">
<form name="update_form" role="form" ng-submit="doUpdate()" autocomplete="off">
<div class="form-group" ng-class="{ 'has-error': update_form.password.$dirty && update_form.password.$invalid }">
<label class="control-label" for="inputUpdatePassword">Give your password to verify that you are performing that action</label>
<div class="control-label" ng-show="(!update_form.password.$dirty && update.error.password) || (update_form.password.$dirty && update_form.password.$invalid)">
<small ng-show="update_form.password.$error.required && !update.error.password">A password is required</small>
<small ng-show="update_form.password.$error.minlength">The password is too short</small>
<small ng-show="update_form.password.$error.maxlength">The password is too long</small>
<small ng-show="update.error.password">Incorrect password</small>
<div ng-show="installedApps | readyToUpdate">
<b ng-show="config.update.box.upgrade" class="text-danger">
This update upgrades the base system and will cause some application downtime.<br/>
</b>
<p>New version: <b>{{config.update.box.version}}</b></p>
<p>Recent Changes:</p>
<ul>
<li ng-repeat="change in config.update.box.changelog">{{change}}</li>
</ul>
<br/>
<fieldset>
<form name="update_form" role="form" ng-submit="doUpdate()" autocomplete="off">
<div class="form-group" ng-class="{ 'has-error': update_form.password.$dirty && update_form.password.$invalid }">
<label class="control-label" for="inputUpdatePassword">Give your password to verify that you are performing that action</label>
<div class="control-label" ng-show="(!update_form.password.$dirty && update.error.password) || (update_form.password.$dirty && update_form.password.$invalid)">
<small ng-show="update_form.password.$error.required && !update.error.password">A password is required</small>
<small ng-show="update_form.password.$error.minlength">The password is too short</small>
<small ng-show="update_form.password.$error.maxlength">The password is too long</small>
<small ng-show="update.error.password">Incorrect password</small>
</div>
<input type="password" class="form-control" ng-model="update.password" id="inputUpdatePassword" name="password" placeholder="Password" ng-maxlength="512" ng-minlength="5" required autofocus>
</div>
<input type="password" class="form-control" ng-model="update.password" id="inputUpdatePassword" name="password" placeholder="Password" ng-maxlength="512" ng-minlength="5" required autofocus>
</div>
<input class="ng-hide" type="submit" ng-disabled="update_form.$invalid || update.busy"/>
</form>
</fieldset>
<input class="ng-hide" type="submit" ng-disabled="update_form.$invalid || update.busy"/>
</form>
</fieldset>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
@@ -118,7 +121,7 @@
<span class="icon-bar"></span>
</button>
<a class="navbar-brand navbar-brand-icon" href="index.html"><img src="/api/v1/cloudron/avatar" width="40" height="40"/></a>
<a class="navbar-brand" href="index.html">{{config.cloudronName || 'Cloudron'}}</a>
<a class="navbar-brand" href="index.html">Cloudron</a>
</div>
<!-- /.navbar-header -->
@@ -144,7 +147,6 @@
<li><a href="#/account"><i class="fa fa-user fa-fw"></i> Account</a></li>
<li ng-show="user.admin && config.isDev"><a href="#/dns"><i class="fa fa-wrench fa-fw"></i> DNS Management</a></li>
<li ng-show="user.admin"><a href="#/graphs"><i class="fa fa-bar-chart fa-fw"></i> Graphs</a></li>
<li ng-show="user.admin"><a href="#/upgrade"><i class="fa fa-arrow-up fa-fw"></i> Upgrade</a></li>
<li><a href="#/support"><i class="fa fa-comment fa-fw"></i> Support</a></li>
<li ng-show="user.admin"><a href="#/settings"><i class="fa fa-wrench fa-fw"></i> Settings</a></li>
<li class="divider"></li>
+32 -23
View File
@@ -23,7 +23,7 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
function defaultErrorHandler(callback) {
return function (data, status) {
if (status === 401) return client.logout();
if (status === 401) return client.login();
if (status === 503) {
// this could indicate a update/upgrade/restore/migration
client.progress(function (error, result) {
@@ -75,8 +75,7 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
isCustomDomain: false,
developerMode: false,
region: null,
size: null,
cloudronName: null
size: null
};
this._installedApps = [];
this._clientId = '<%= oauth.clientId %>';
@@ -186,20 +185,6 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
}).error(defaultErrorHandler(callback));
};
Client.prototype.changeCloudronName = function (name, callback) {
var that = this;
var data = { name: name };
$http.post(client.apiOrigin + '/api/v1/settings/cloudron_name', data).success(function (data, status) {
if (status !== 200) return callback(new ClientError(status, data));
// will get overriden after polling for config, but ensures quick UI update
that._config.cloudronName = name;
callback(null);
}).error(defaultErrorHandler(callback));
};
Client.prototype.changeCloudronAvatar = function (avatarFile, callback) {
var fd = new FormData();
fd.append('avatar', avatarFile);
@@ -215,7 +200,7 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
Client.prototype.installApp = function (id, manifest, title, config, callback) {
var that = this;
var data = { appStoreId: id, manifest: manifest, location: config.location, portBindings: config.portBindings, accessRestriction: config.accessRestriction };
var data = { appStoreId: id, manifest: manifest, location: config.location, portBindings: config.portBindings, accessRestriction: config.accessRestriction, oauthProxy: config.oauthProxy };
$http.post(client.apiOrigin + '/api/v1/apps/install', data).success(function (data, status) {
if (status !== 202 || typeof data !== 'object') return defaultErrorHandler(callback);
@@ -249,7 +234,7 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
};
Client.prototype.configureApp = function (id, password, config, callback) {
var data = { appId: id, password: password, location: config.location, portBindings: config.portBindings, accessRestriction: config.accessRestriction };
var data = { appId: id, password: password, location: config.location, portBindings: config.portBindings, accessRestriction: config.accessRestriction, oauthProxy: config.oauthProxy };
$http.post(client.apiOrigin + '/api/v1/apps/' + id + '/configure', data).success(function (data, status) {
if (status !== 202) return callback(new ClientError(status, data));
callback(null);
@@ -324,6 +309,13 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
}).error(defaultErrorHandler(callback));
};
Client.prototype.getUsers = function (callback) {
$http.get(client.apiOrigin + '/api/v1/users').success(function (data, status) {
if (status !== 200 || typeof data !== 'object') return callback(new ClientError(status, data));
callback(null, data.users);
}).error(defaultErrorHandler(callback));
};
Client.prototype.getNonApprovedApps = function (callback) {
if (!this._config.developerMode) return callback(null, []);
@@ -376,12 +368,11 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
}).error(defaultErrorHandler(callback));
};
Client.prototype.createAdmin = function (username, password, email, name, setupToken, callback) {
Client.prototype.createAdmin = function (username, password, email, setupToken, callback) {
var payload = {
username: username,
password: password,
email: email,
name: name
email: email
};
var that = this;
@@ -605,12 +596,30 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
});
};
Client.prototype.login = function () {
this.setToken(null);
this._userInfo = {};
var callbackURL = window.location.protocol + '//' + window.location.host + '/login_callback.html';
var scope = 'root,profile,apps';
// generate a state id to protect agains csrf
var state = Math.floor((1 + Math.random()) * 0x1000000000000).toString(16).substring(1);
window.localStorage.oauth2State = state;
// stash for further use in login_callback
window.localStorage.returnTo = '/' + window.location.hash;
window.location.href = this.apiOrigin + '/api/v1/oauth/dialog/authorize?response_type=token&client_id=' + this._clientId + '&redirect_uri=' + callbackURL + '&scope=' + scope + '&state=' + state;
};
Client.prototype.logout = function () {
this.setToken(null);
this._userInfo = {};
// logout from OAuth session
window.location.href = client.apiOrigin + '/api/v1/session/logout';
var origin = window.location.protocol + "//" + window.location.host;
window.location.href = this.apiOrigin + '/api/v1/session/logout?redirect=' + origin;
};
Client.prototype.exchangeCodeForToken = function (code, callback) {
+7 -20
View File
@@ -40,9 +40,6 @@ app.config(['$routeProvider', function ($routeProvider) {
}).when('/support', {
controller: 'SupportController',
templateUrl: 'views/support.html'
}).when('/upgrade', {
controller: 'UpgradeController',
templateUrl: 'views/upgrade.html'
}).otherwise({ redirectTo: '/'});
}]);
@@ -96,13 +93,13 @@ app.filter('installationStateLabel', function() {
var waiting = app.progress === 0 ? ' (Waiting)' : '';
switch (app.installationState) {
case ISTATES.PENDING_INSTALL: return 'Installing...' + waiting;
case ISTATES.PENDING_CONFIGURE: return 'Configuring...' + waiting;
case ISTATES.PENDING_UNINSTALL: return 'Uninstalling...' + waiting;
case ISTATES.PENDING_RESTORE: return 'Restoring...' + waiting;
case ISTATES.PENDING_UPDATE: return 'Updating...' + waiting;
case ISTATES.PENDING_FORCE_UPDATE: return 'Updating...' + waiting;
case ISTATES.PENDING_BACKUP: return 'Backing up...' + waiting;
case ISTATES.PENDING_INSTALL: return 'Installing' + waiting;
case ISTATES.PENDING_CONFIGURE: return 'Configuring' + waiting;
case ISTATES.PENDING_UNINSTALL: return 'Uninstalling' + waiting;
case ISTATES.PENDING_RESTORE: return 'Restoring' + waiting;
case ISTATES.PENDING_UPDATE: return 'Updating' + waiting;
case ISTATES.PENDING_FORCE_UPDATE: return 'Updating' + waiting;
case ISTATES.PENDING_BACKUP: return 'Backing up' + waiting;
case ISTATES.ERROR: return 'Error';
case ISTATES.INSTALLED: {
if (app.runState === 'running') {
@@ -146,16 +143,6 @@ app.filter('applicationLink', function() {
};
});
app.filter('accessRestrictionLabel', function() {
return function (input) {
if (input === '') return 'public';
if (input === 'roleUser') return 'private';
if (input === 'roleAdmin') return 'private (Admins only)';
return input;
};
});
app.filter('prettyHref', function () {
return function (input) {
if (!input) return input;
+29 -44
View File
@@ -23,17 +23,6 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
Client.logout();
};
$scope.login = function () {
var callbackURL = window.location.protocol + '//' + window.location.host + '/login_callback.html';
var scope = 'root,profile,apps,roleAdmin';
// generate a state id to protect agains csrf
var state = Math.floor((1 + Math.random()) * 0x1000000000000).toString(16).substring(1);
window.localStorage.oauth2State = state;
window.location.href = Client.apiOrigin + '/api/v1/oauth/dialog/authorize?response_type=token&client_id=' + Client._clientId + '&redirect_uri=' + callbackURL + '&scope=' + scope + '&state=' + state;
};
$scope.setup = function () {
window.location.href = '/error.html?errorCode=1';
};
@@ -78,43 +67,43 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
if (error) return $scope.error(error);
if (isFirstTime) return $scope.setup();
// we use the config request as an indicator if the token is still valid
// TODO we should probably attach such a handler for each request, as the token can get invalid
// at any time!
if (localStorage.token) {
Client.refreshConfig(function (error) {
if (error && error.statusCode === 401) return $scope.login();
Client.refreshConfig(function (error) {
if (error) return $scope.error(error);
// check version and force reload if needed
if (!localStorage.version) {
localStorage.version = Client.getConfig().version;
} else if (localStorage.version !== Client.getConfig().version) {
localStorage.version = Client.getConfig().version;
window.location.reload(true);
}
Client.refreshUserInfo(function (error, result) {
if (error) return $scope.error(error);
Client.refreshUserInfo(function (error, result) {
Client.refreshInstalledApps(function (error) {
if (error) return $scope.error(error);
Client.refreshInstalledApps(function (error) {
if (error) return $scope.error(error);
// kick off installed apps and config polling
var refreshAppsTimer = $interval(Client.refreshInstalledApps.bind(Client), 2000);
var refreshConfigTimer = $interval(Client.refreshConfig.bind(Client), 5000);
var refreshUserInfoTimer = $interval(Client.refreshUserInfo.bind(Client), 5000);
// kick off installed apps and config polling
var refreshAppsTimer = $interval(Client.refreshInstalledApps.bind(Client), 2000);
var refreshConfigTimer = $interval(Client.refreshConfig.bind(Client), 5000);
var refreshUserInfoTimer = $interval(Client.refreshUserInfo.bind(Client), 5000);
$scope.$on('$destroy', function () {
$interval.cancel(refreshAppsTimer);
$interval.cancel(refreshConfigTimer);
$interval.cancel(refreshUserInfoTimer);
});
// now mark the Client to be ready
Client.setReady();
$scope.config = Client.getConfig();
$scope.initialized = true;
$scope.$on('$destroy', function () {
$interval.cancel(refreshAppsTimer);
$interval.cancel(refreshConfigTimer);
$interval.cancel(refreshUserInfoTimer);
});
// now mark the Client to be ready
Client.setReady();
$scope.config = Client.getConfig();
$scope.initialized = true;
});
});
} else {
$scope.login();
}
});
});
// wait till the view has loaded until showing a modal dialog
@@ -123,10 +112,6 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
if (config.progress.update && config.progress.update.percent !== -1) {
window.location.href = '/update.html';
}
if (config.cloudronName) {
document.title = config.cloudronName;
}
});
// setup all the dialog focus handling
+1 -2
View File
@@ -40,7 +40,6 @@ app.service('Wizard', [ function () {
this.username = '';
this.email = '';
this.password = '';
this.name = '';
this.availableAvatars = [{
file: null,
data: null,
@@ -198,7 +197,7 @@ app.controller('StepController', ['$scope', '$route', '$location', 'Wizard', fun
app.controller('FinishController', ['$scope', '$location', '$timeout', 'Wizard', 'Client', function ($scope, $location, $timeout, Wizard, Client) {
$scope.wizard = Wizard;
Client.createAdmin($scope.wizard.username, $scope.wizard.password, $scope.wizard.email, $scope.wizard.name, $scope.setupToken, function (error) {
Client.createAdmin($scope.wizard.username, $scope.wizard.password, $scope.wizard.email, $scope.setupToken, function (error) {
if (error) {
console.error('Internal error', error);
window.location.href = '/error.html';
+5 -1
View File
@@ -19,7 +19,11 @@
// clear oauth2 state
delete window.localStorage.oauth2State;
window.location.href = '/';
var returnTo = window.localStorage.returnTo;
delete window.localStorage.returnTo;
if (returnTo) window.location.href = returnTo;
else window.location.href = '/';
}
</script>
+2 -11
View File
@@ -38,18 +38,9 @@
else return 'https://my' + tmp.slice(tmp.indexOf('-')) + host.slice(tmp.length);
}
app.controller('Controller', ['$scope', '$http', function ($scope, $http) {
app.controller('Controller', ['$scope', function ($scope) {
$scope.apiOrigin = detectApiOrigin();
$scope.cloudronAvatar = $scope.apiOrigin + '/api/v1/cloudron/avatar';
$scope.cloudronName = 'Cloudron';
$http.get($scope.apiOrigin + '/api/v1/cloudron/status').success(function(data, status) {
if (status !== 200 || typeof data !== 'object') return console.error(status, data);
$scope.cloudronName = data.cloudronName;
document.title = $scope.cloudronName;
}).error(function (data, status) {
console.error(status, data);
});
}]);
</script>
@@ -60,7 +51,7 @@
<div class="wrapper">
<div class="content">
<img ng-src="{{ cloudronAvatar || '/img/logo_inverted_192.png' }}" onerror="this.src = '/img/logo_inverted_192.png'"/>
<h1> {{cloudronName}} </h1>
<h1> Cloudron </h1>
<p>
There is no app configured for this domain. If you want to put an app at this location,<br/>
please reconfigure the app in the <a ng-href="{{apiOrigin}}">settings panel</a> and leave the location empty.
+6 -18
View File
@@ -120,7 +120,6 @@ html {
.grid-item {
padding: 10px;
min-width: 200px;
overflow: hidden;
}
.grid-item:hover .grid-item-bottom {
@@ -175,6 +174,12 @@ html {
}
}
.app-update-badge {
position: absolute;
right: 0;
top: 0;
}
// ----------------------------
// Appstore view
// ----------------------------
@@ -354,23 +359,6 @@ html {
max-width: 800px;
}
.app-update-badge {
font-size: $font-size-h4;
position: absolute;
left: 2px;
top: 2px;
width: $font-size-h4 + 6px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
background-color: transparent;
}
.app-update-badge:hover {
width: inherit;
background-color: #5CB85C;
}
.text-success {
color: #5CB85C;
}
+1 -1
View File
@@ -152,7 +152,7 @@
<h4 class="text-muted">Credentials</h4>
<p>Permissions: <b>{{ client.scope }}</b></p>
<p>Client ID: <b>{{ client.id }}</b></p>
<p>Client Secret: <b>{{ client.clientSecret }}</b></p>
<p ng-show="client.clientSecret">Client Secret: <b>{{ client.clientSecret }}</b></p>
</div>
</div>
</div>
+22 -21
View File
@@ -36,12 +36,17 @@
</div>
</ng-form>
</div>
<div class="form-group" ng-show="appConfigure.app.manifest.singleUser">
<label class="control-label">User</label>
<p>This is a single user application. Access is granted to <b>{{appConfigure.app.accessRestriction.users[0]}}</b>.</p>
</div>
<div class="form-group">
<label class="control-label" for="accessRestriction">Website Visibility</label>
<select class="form-control" id="accessRestriction" ng-model="appConfigure.accessRestriction">
<option value="">Visible to all</option>
<option value="roleUser">Visible only to Cloudron users</option>
</select>
<label class="control-label" for="oauthProxy">Website Visibility</label>
<div class="checkbox">
<label>
<input type="checkbox" id="oauthProxy" ng-model="appConfigure.oauthProxy"> Cloudron users only
</label>
</div>
</div>
<a ng-show="!!appConfigure.app.manifest.configurePath" ng-href="https://{{ appConfigure.app.location }}{{ !appConfigure.app.location ? '' : (config.isCustomDomain ? '.' : '-') }}{{ config.fqdn }}/{{ appConfigure.app.manifest.configurePath }}" target="_blank">Application Specific Settings</a>
<br/>
@@ -55,10 +60,6 @@
</fieldset>
</div>
<div class="modal-footer ">
<button type="button" class="btn btn-default" style="float: left;" ng-click="startApp(appConfigure.app)" ng-show="appConfigure.app.runState === 'stopped' && !appConfigure.runStateBusy && !(appConfigure.app | installationActive)"><i class="fa fa-play"></i> Start</button>
<button type="button" class="btn btn-default" style="float: left;" ng-show="appConfigure.app.runState !== 'stopped' && appConfigure.app.runState !== 'running' || appConfigure.runStateBusy && !(appConfigure.app | installationActive)" disabled ><i class="fa fa-refresh fa-spin"></i></button>
<button type="button" class="btn btn-default" style="float: left;" ng-click="stopApp(appConfigure.app)" ng-show="appConfigure.app.runState === 'running' && !appConfigure.runStateBusy && !(appConfigure.app | installationActive)"><i class="fa fa-pause"></i> Stop</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-success" ng-click="doConfigure()" ng-disabled="appConfigureForm.$invalid || appConfigure.busy"><i class="fa fa-spinner fa-pulse" ng-show="appConfigure.busy"></i> Save</button>
</div>
@@ -209,8 +210,7 @@
<div class="text-muted" style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden">
{{ app | installationStateLabel }}
</div>
<br ng-hide="app | installationActive"/>
<div ng-show="app | installationActive">
<div ng-style="{ 'visibility': (app | installationActive) ? 'visible' : 'hidden' }">
<div class="progress progress-striped active">
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ app.progress }}%"></div>
</div>
@@ -221,7 +221,7 @@
<div class="grid-item-bottom-mobile" ng-show="user.admin">
<div class="row">
<div class="col-xs-4 text-left">
<a href="" ng-click="showRestore(app)" ng-show="(app | installError) === true">
<a href="" ng-click="showRestore(app)" ng-show="app.lastBackupId != null || (app | installError) === true">
<i class="fa fa-undo scale"></i>
</a>
@@ -244,26 +244,27 @@
</div>
<div class="grid-item-bottom" ng-show="user.admin">
<br/>
<br/>
<div>
<a href="" ng-click="showUninstall(app)"><i class="fa fa-remove scale"></i></a>
<a href="" ng-click="showUninstall(app)" title="Uninstall App"><i class="fa fa-remove scale"></i></a>
</div>
<div ng-show="(app | installError) === true">
<a href="" ng-click="showRestore(app)"><i class="fa fa-undo scale"></i></a>
<div ng-show="app.lastBackupId !== null || (app | installError) === true">
<a href="" ng-click="showRestore(app)" title="Restore App"><i class="fa fa-undo scale"></i></a>
</div>
<div ng-show="(app | installSuccess) == true">
<a href="" ng-click="showConfigure(app)"><i class="fa fa-wrench scale"></i></a>
</div>
<!-- we check the version here because the box updater does not know when an app gets updated -->
<div ng-show="config.update.apps[app.id].manifest.version && config.update.apps[app.id].manifest.version !== app.manifest.version && (app | installSuccess)">
<a href="" ng-click="showUpdate(app)"><i class="fa fa-arrow-up text-success scale"></i></a>
<a href="" ng-click="showConfigure(app)" title="Configure App"><i class="fa fa-wrench scale"></i></a>
</div>
<br/>
</div>
<!-- we check the version here because the box updater does not know when an app gets updated -->
<div class="app-update-badge" ng-show="config.update.apps[app.id].manifest.version && config.update.apps[app.id].manifest.version !== app.manifest.version && (app | installSuccess)">
<a href="" ng-click="showUpdate(app)" title="Update Available"><i class="fa fa-asterisk fa-2x text-success scale"></i></a>
</div>
</a>
</div>
</div>
+11 -20
View File
@@ -19,7 +19,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
portBindings: {},
portBindingsEnabled: {},
portBindingsInfo: {},
accessRestriction: ''
oauthProxy: false
};
$scope.appUninstall = {
@@ -52,7 +52,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
$scope.appConfigure.location = '';
$scope.appConfigure.password = '';
$scope.appConfigure.portBindings = {};
$scope.appConfigure.accessRestriction = '';
$scope.appConfigure.oauthProxy = false;
$scope.appConfigureForm.$setPristine();
$scope.appConfigureForm.$setUntouched();
@@ -89,7 +89,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
$scope.appConfigure.app = app;
$scope.appConfigure.location = app.location;
$scope.appConfigure.accessRestriction = app.accessRestriction;
$scope.appConfigure.oauthProxy = app.oauthProxy;
$scope.appConfigure.portBindingsInfo = app.manifest.tcpPorts || {}; // Portbinding map only for information
$scope.appConfigure.portBindings = {}; // This is the actual model holding the env:port pair
$scope.appConfigure.portBindingsEnabled = {}; // This is the actual model holding the enabled/disabled flag
@@ -122,7 +122,14 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
}
}
Client.configureApp($scope.appConfigure.app.id, $scope.appConfigure.password, { location: $scope.appConfigure.location || '', portBindings: finalPortBindings, accessRestriction: $scope.appConfigure.accessRestriction }, function (error) {
var data = {
location: $scope.appConfigure.location || '',
portBindings: finalPortBindings,
oauthProxy: $scope.appConfigure.oauthProxy,
accessRestriction: $scope.appConfigure.app.accessRestriction
};
Client.configureApp($scope.appConfigure.app.id, $scope.appConfigure.password, data, function (error) {
if (error) {
if (error.statusCode === 409 && (error.message.indexOf('is reserved') !== -1 || error.message.indexOf('is already in use') !== -1)) {
$scope.appConfigure.error.port = error.message;
@@ -313,22 +320,6 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
});
};
$scope.startApp = function (app) {
$scope.appConfigure.runStateBusy = true;
app.runState = 'pending_start'; // we assume we will end up there
Client.startApp(app.id, function () {
$scope.appConfigure.runStateBusy = false;
});
};
$scope.stopApp = function (app) {
$scope.appConfigure.runStateBusy = true;
app.runState = 'pending_stop'; // we assume we will end up there
Client.stopApp(app.id, function () {
$scope.appConfigure.runStateBusy = false;
});
};
$scope.cancel = function () {
window.history.back();
};
+5 -7
View File
@@ -33,15 +33,13 @@
</div>
</ng-form>
</div>
<!--
<div class="form-group">
<label class="control-label" for="accessRestriction">Access Restriction</label>
<select class="form-control" id="accessRestriction" ng-model="appInstall.accessRestriction">
<option value="">None</option>
<option value="roleUser">Only for Cloudron Users</option>
<div class="form-group" ng-show="appInstall.app.manifest.singleUser">
<label class="control-label" for="accessRestriction">User</label>
<p>This is a single user application.</p>
<select class="form-control" id="accessRestriction" ng-model="appInstall.accessRestriction" ng-required="appInstall.app.manifest.singleUser">
<option ng-repeat="user in users" value="{{user.id}}">{{user.username}} - {{user.email}}</option>
</select>
</div>
-->
<input class="ng-hide" type="submit" ng-disabled="appInstallForm.$invalid || busy"/>
</form>
</div>
+46 -26
View File
@@ -5,6 +5,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
$scope.apps = [];
$scope.config = Client.getConfig();
$scope.user = Client.getUserInfo();
$scope.users = [];
$scope.category = '';
$scope.cachedCategory = ''; // used to cache the selected category while searching
$scope.searchString = '';
@@ -16,7 +17,8 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
app: {},
location: '',
portBindings: {},
accessRestriction: '',
accessRestriction: null,
oauthProxy: false,
mediaLinks: []
};
@@ -135,7 +137,8 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
$scope.appInstall.error = {};
$scope.appInstall.location = '';
$scope.appInstall.portBindings = {};
$scope.appInstall.accessRestriction = '';
$scope.appInstall.accessRestriction = null;
$scope.appInstall.oauthProxy = false;
$scope.appInstall.installFormVisible = false;
$scope.appInstall.mediaLinks = [];
$('#collapseInstallForm').collapse('hide');
@@ -159,12 +162,15 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
angular.copy(app, $scope.appInstall.app);
$('#appInstallModal').modal('show');
console.log(app)
$scope.appInstall.mediaLinks = $scope.appInstall.app.manifest.mediaLinks || [];
$scope.appInstall.location = app.location;
$scope.appInstall.portBindingsInfo = $scope.appInstall.app.manifest.tcpPorts || {}; // Portbinding map only for information
$scope.appInstall.portBindings = {}; // This is the actual model holding the env:port pair
$scope.appInstall.portBindingsEnabled = {}; // This is the actual model holding the enabled/disabled flag
$scope.appInstall.accessRestriction = app.accessRestriction || '';
$scope.appInstall.accessRestriction = app.accessRestriction ? app.accessRestriction.users[0] : null;
$scope.appInstall.oauthProxy = false;
// set default ports
for (var env in $scope.appInstall.app.manifest.tcpPorts) {
@@ -194,7 +200,12 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
}
}
Client.installApp($scope.appInstall.app.id, $scope.appInstall.app.manifest, $scope.appInstall.app.title, { location: $scope.appInstall.location || '', portBindings: finalPortBindings, accessRestriction: $scope.appInstall.accessRestriction }, function (error) {
// translate to accessRestriction object
var accessRestriction = $scope.appInstall.app.manifest.singleUser ? {
users: [ $scope.appInstall.accessRestriction ]
} : null;
Client.installApp($scope.appInstall.app.id, $scope.appInstall.app.manifest, $scope.appInstall.app.title, { location: $scope.appInstall.location || '', portBindings: finalPortBindings, accessRestriction: accessRestriction, oauthProxy: $scope.appInstall.oauthProxy }, function (error) {
if (error) {
if (error.statusCode === 409 && (error.message.indexOf('is reserved') !== -1 || error.message.indexOf('is already in use') !== -1)) {
$scope.appInstall.error.port = error.message;
@@ -227,40 +238,49 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
function refresh() {
$scope.ready = false;
getAppList(function (error, apps) {
Client.getUsers(function (error, users) {
if (error) {
console.error(error);
return $timeout(refresh, 1000);
}
$scope.apps = apps;
$scope.users = users;
// show install app dialog immediately if an app id was passed in the query
if ($routeParams.appId) {
if ($routeParams.version) {
AppStore.getAppByIdAndVersion($routeParams.appId, $routeParams.version, function (error, result) {
if (error) {
$scope.showAppNotFound($routeParams.appId, $routeParams.version);
console.error(error);
return;
}
getAppList(function (error, apps) {
if (error) {
console.error(error);
return $timeout(refresh, 1000);
}
$scope.showInstall(result);
});
} else {
var found = apps.filter(function (app) {
return (app.id === $routeParams.appId) && ($routeParams.version ? $routeParams.version === app.manifest.version : true);
});
$scope.apps = apps;
if (found.length) {
$scope.showInstall(found[0]);
// show install app dialog immediately if an app id was passed in the query
if ($routeParams.appId) {
if ($routeParams.version) {
AppStore.getAppByIdAndVersion($routeParams.appId, $routeParams.version, function (error, result) {
if (error) {
$scope.showAppNotFound($routeParams.appId, $routeParams.version);
console.error(error);
return;
}
$scope.showInstall(result);
});
} else {
$scope.showAppNotFound($routeParams.appId, null);
var found = apps.filter(function (app) {
return (app.id === $routeParams.appId) && ($routeParams.version ? $routeParams.version === app.manifest.version : true);
});
if (found.length) {
$scope.showInstall(found[0]);
} else {
$scope.showAppNotFound($routeParams.appId, null);
}
}
}
}
$scope.ready = true;
$scope.ready = true;
});
});
}
+1 -1
View File
@@ -92,7 +92,7 @@ angular.module('Application').controller('GraphsController', ['$scope', '$locati
var options = {
scaleOverride: true,
scaleSteps: 10,
scaleStepWidth: $scope.activeApp === 'system' ? 200 : 60,
scaleStepWidth: $scope.activeApp === 'system' ? 200 : ($scope.activeApp.manifest.memoryLimit ? parseInt($scope.activeApp.manifest.memoryLimit/10000000,10) : 20),
scaleStartValue: 0
};
-34
View File
@@ -32,36 +32,6 @@
</div>
</div>
<!-- Modal change name -->
<div class="modal fade" id="nameChangeModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Change the Cloudron Name</h4>
</div>
<div class="modal-body">
<form name="nameChangeForm" class="form-signin" role="form" novalidate ng-submit="doChangeName()" autocomplete="off">
<fieldset>
<div class="form-group" ng-class="{ 'has-error': (nameChangeForm.name.$dirty && nameChangeForm.name.$invalid) }">
<label class="control-label" for="inputNameChangeName">New Cloudron Name</label>
<div class="control-label" ng-show="(!nameChangeForm.name.$dirty && nameChange.error.name) || (nameChangeForm.name.$dirty && nameChangeForm.name.$invalid)">
<small ng-show="nameChangeForm.name.$error.required">A valid name is required</small>
<small ng-show="(nameChangeForm.name.$dirty && nameChangeForm.name.$invalid) && !nameChangeForm.name.$error.required">The name is not valid</small>
</div>
<input type="text" class="form-control" ng-model="nameChange.name" id="inputNameChangeName" name="name" ng-maxlength="512" ng-minlength="1" required autofocus>
</div>
<input class="ng-hide" type="submit" ng-disabled="nameChangeForm.$invalid"/>
</fieldset>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-success" ng-click="doChangeName()" ng-disabled="nameChangeForm.$invalid || nameChange.busy"><i class="fa fa-spinner fa-pulse" ng-show="nameChange.busy"></i> Change</button>
</div>
</div>
</div>
</div>
<!-- Modal change avatar -->
<div class="modal fade" id="avatarChangeModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
@@ -160,10 +130,6 @@
</div>
<div class="col-xs-8">
<table width="100%">
<tr>
<td class="text-muted" style="vertical-align: top;">Name</td>
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ config.cloudronName }} <a href="" ng-click="showChangeName()"><i class="fa fa-pencil text-small"></i></a></td>
</tr>
<tr>
<td class="text-muted" style="vertical-align: top;">Model</td>
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ config.size }} - {{ config.region }}</td>
+1 -36
View File
@@ -24,12 +24,6 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
percent: 100
};
$scope.nameChange = {
busy: false,
error: {},
name: ''
};
$scope.avatarChange = {
busy: false,
error: {},
@@ -97,14 +91,6 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
$('#avatarFileInput').click();
};
function nameChangeReset() {
$scope.nameChange.error.name = null;
$scope.nameChange.name = '';
$scope.nameChangeForm.$setPristine();
$scope.nameChangeForm.$setUntouched();
}
function avatarChangeReset() {
$scope.avatarChange.error.avatar = null;
$scope.avatarChange.avatar = null;
@@ -156,22 +142,6 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
});
};
$scope.doChangeName = function () {
$scope.nameChange.error.name = null;
$scope.nameChange.busy = true;
Client.changeCloudronName($scope.nameChange.name, function (error) {
if (error) {
console.error('Unable to change name.', error);
} else {
nameChangeReset();
$('#nameChangeModal').modal('hide');
}
$scope.nameChange.busy = false;
});
};
function getBlobFromImg(img, callback) {
var size = 256;
@@ -263,11 +233,6 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
$('#developerModeChangeModal').modal('show');
};
$scope.showChangeName = function () {
nameChangeReset();
$('#nameChangeModal').modal('show');
};
$scope.showCreateBackup = function () {
$('#createBackupModal').modal('show');
};
@@ -301,7 +266,7 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
});
// setup all the dialog focus handling
['developerModeChangeModal', 'nameChangeModal'].forEach(function (id) {
['developerModeChangeModal'].forEach(function (id) {
$('#' + id).on('shown.bs.modal', function () {
$(this).find("[autofocus]:first").focus();
});

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