Compare commits

..

762 Commits

Author SHA1 Message Date
Johannes Zellner d0cf698dfa Hide the appstore account setup for now 2016-07-28 18:20:18 +02:00
Johannes Zellner df8910b1e1 Update to angularjs 1.5.8
This fixes the dependency issues with bootstrap
2016-07-28 17:29:25 +02:00
Johannes Zellner 7862fbd7ee Set correct focus for selfhosters in setup wizard 2016-07-28 16:22:20 +02:00
Johannes Zellner cd896a4422 Use persistent appstore login tokens 2016-07-28 15:47:09 +02:00
Johannes Zellner b8a635c638 Add UI components to allow cloudron name changes 2016-07-28 12:56:17 +02:00
Johannes Zellner 690564983f Add cloudron name change api to client.js 2016-07-28 12:41:15 +02:00
Johannes Zellner 4fb3a42319 Return 202 when setting the cloudron name 2016-07-28 12:40:36 +02:00
Johannes Zellner 6d2e52b3b5 Add provider fallback in webadmin
This is a bug and requires an upgrade to set the provider again
2016-07-28 12:10:13 +02:00
Johannes Zellner ced31afe55 Fix label for xframeoptions 2016-07-28 10:55:07 +02:00
Girish Ramakrishnan 5c4be56edb 0.17.4 changes 2016-07-27 20:42:22 -07:00
Girish Ramakrishnan 3595f624de Fix progress text 2016-07-27 20:38:49 -07:00
Girish Ramakrishnan 0e9007e9ef fix debug 2016-07-27 20:11:45 -07:00
Girish Ramakrishnan 971647c986 use the new appstore update route to detect app updates 2016-07-27 19:15:10 -07:00
Girish Ramakrishnan 138829f69b remove appupdate pre-release logic
this all seems very premature since prereleases are not supported in
appstore side
2016-07-27 17:46:58 -07:00
Girish Ramakrishnan e0d4c1adc1 use support instead of admin 2016-07-27 11:48:03 -07:00
Girish Ramakrishnan 03c97d2027 send appVersions when checking for updates 2016-07-27 10:14:10 -07:00
Johannes Zellner 867e875707 Revert "Add basic 404 page"
This reverts commit 3793220dd48356d5fe421312915a8392fcccca0e.
2016-07-27 19:09:43 +02:00
Johannes Zellner 2ac7c15b90 Do not show appstore account in settings for caas 2016-07-27 17:58:33 +02:00
Johannes Zellner dcdca52dbd Add basic 404 page 2016-07-27 17:52:54 +02:00
Johannes Zellner 711814cc2f only perform the appstore account setup on non caas cloudrons 2016-07-27 17:42:44 +02:00
Johannes Zellner c2b57a704d We can't use the AppStore API wrapper due to Client.config()
Fetching the config requires of course an access token...
2016-07-27 17:32:22 +02:00
Johannes Zellner 1106aa6bba Create cloudron.io account on the fly with the same credentials 2016-07-27 17:22:03 +02:00
Johannes Zellner 482a87e994 Add cloudron.io account registration api 2016-07-27 17:21:44 +02:00
Johannes Zellner d65990f780 Improve cloudron store checkbox layout 2016-07-27 17:02:22 +02:00
Johannes Zellner 60c1fb4a93 Add checkbox for appstore account creation 2016-07-27 16:54:54 +02:00
Johannes Zellner 02fcb749aa Use uib-tooltip instead of the non angular aware bootstrap one 2016-07-27 16:52:36 +02:00
Johannes Zellner dfc0598ec9 Wrap all controller setup code with Client.onReady()
This ensures we don't rely on timing for execution against a non
ready Client instance
2016-07-27 16:34:38 +02:00
Johannes Zellner b13dd55fc6 Ensure we only callback once for onReady() 2016-07-27 16:34:15 +02:00
Johannes Zellner 3020071fe4 Provider argument for setup is never used 2016-07-27 14:15:22 +02:00
Johannes Zellner 57d2a3ff6e Show model only for caas so far 2016-07-27 11:20:15 +02:00
Johannes Zellner 6fa414206c Remove admin checks in settings view
We anyways only allow settings to be shown for admins
see settings.js
2016-07-27 11:20:15 +02:00
Johannes Zellner 4619435a2d prepend the apiOrigin in one place 2016-07-27 11:20:15 +02:00
Johannes Zellner ce433932dd Remove obsolete property 2016-07-27 11:20:15 +02:00
Johannes Zellner 4b79af7975 Do not set the global auth header for all but use wrappers instead
Setting it global means we send this to all requests being made through angular
2016-07-27 11:20:15 +02:00
Johannes Zellner 35cb804f00 Show appstore account email instead of id 2016-07-27 11:20:15 +02:00
Johannes Zellner b132b2dc15 Also fetch the appstore account profile 2016-07-27 11:20:15 +02:00
Johannes Zellner 5da766131b Set accessToken for appstore via params 2016-07-27 11:20:15 +02:00
Johannes Zellner 642e5aceed Add AppStore.profile() 2016-07-27 11:20:15 +02:00
Johannes Zellner e8088be586 Remove debug console.log() 2016-07-27 11:20:15 +02:00
Johannes Zellner 2a64764deb Save appstore config after login 2016-07-27 11:20:15 +02:00
Johannes Zellner a8d04028f3 Fix typo 2016-07-27 11:20:15 +02:00
Johannes Zellner 57c7ae3c2b Fetch appstore config in settings view 2016-07-27 11:20:15 +02:00
Johannes Zellner 8165227b0a Keep same style in settings rest api 2016-07-27 11:20:15 +02:00
Johannes Zellner f5af539102 Add webadmin client calls for appstore config 2016-07-27 11:20:15 +02:00
Johannes Zellner 41e1afaf68 Add settings/appstore routes 2016-07-27 11:20:15 +02:00
Johannes Zellner 7361acbec5 Add appstore config in settingsdb 2016-07-27 11:20:15 +02:00
Johannes Zellner adbe862fd3 Add register button for appstore account 2016-07-27 11:20:15 +02:00
Johannes Zellner 7e3628f4c5 Add basic error handling 2016-07-27 11:20:15 +02:00
Johannes Zellner 99af676344 Add AppStore.login()/logout() 2016-07-27 11:20:15 +02:00
Johannes Zellner 9f377cb8fe Add cloudron.io icon 2016-07-27 11:20:15 +02:00
Johannes Zellner da1418c48b Add angular base64 module 2016-07-27 11:20:15 +02:00
Johannes Zellner 84b7d77aa0 Add appstore login form dialog 2016-07-27 11:20:15 +02:00
Johannes Zellner 748e30a6e5 Minor rewording 2016-07-27 11:20:15 +02:00
Johannes Zellner 34453c9dde Add initial section for appstore account view in settings 2016-07-27 11:20:15 +02:00
Girish Ramakrishnan ebe64852be checkout is a noun/adjective. check out is a verb 2016-07-27 01:00:25 -07:00
Girish Ramakrishnan f5c7e993ea 0.17.4 changes 2016-07-27 00:22:08 -07:00
Girish Ramakrishnan b628e2a6c8 add hack for mysql server on ec2 2016-07-27 00:15:08 -07:00
Girish Ramakrishnan 01af6ef23a fix wording in out_of_disk_space mail template 2016-07-26 17:22:58 -07:00
Girish Ramakrishnan 947edfec72 typo: Check "available" and not "used" 2016-07-26 17:10:22 -07:00
Girish Ramakrishnan 159fecc9ce send certificate renewal errors to owner for non-caas 2016-07-26 16:47:58 -07:00
Girish Ramakrishnan 0bf8b94bb4 send outOfDiskSpace mails to owners for non-caas provider 2016-07-26 16:43:14 -07:00
Girish Ramakrishnan d4d07e27c0 send email for certificate renewal error 2016-07-26 16:37:10 -07:00
Girish Ramakrishnan e9e09e66c3 remove unused variables 2016-07-26 16:37:10 -07:00
Girish Ramakrishnan a67b2c7559 warn user that custom domain might overwrite MX record 2016-07-25 21:58:28 -07:00
Girish Ramakrishnan d539f1fec8 Keep menu items alphabetical 2016-07-25 21:49:20 -07:00
Girish Ramakrishnan a3c270c4a1 do not reconfigure apps when infra version has not changed 2016-07-25 18:57:54 -07:00
Girish Ramakrishnan 33c70dad8b fix merging blooper 2016-07-25 16:18:09 -07:00
Girish Ramakrishnan 28ec9d82da assume success if dns server to be down 2016-07-25 15:31:26 -07:00
Girish Ramakrishnan e13075b835 more changelog 2016-07-25 14:38:19 -07:00
Girish Ramakrishnan 03022f0207 Do not send more than 1 oom mail every hour 2016-07-25 14:19:20 -07:00
Girish Ramakrishnan 98facf2a3c delete eventlog older than 7 days 2016-07-25 12:54:27 -07:00
Girish Ramakrishnan 338f4bcdea docker event stream can be null if it errored 2016-07-25 11:39:32 -07:00
Girish Ramakrishnan e46b1a9245 test for app instead of error 2016-07-25 11:37:30 -07:00
Girish Ramakrishnan 129843c0ba debug name of addons that changed 2016-07-25 10:18:35 -07:00
Girish Ramakrishnan b079d688c1 bump mail container to 0.18.0 2016-07-25 10:13:05 -07:00
Girish Ramakrishnan 684aec41cc create all addons on infra upgrade 2016-07-25 10:13:01 -07:00
Girish Ramakrishnan cc26c2b1f1 0.17.3 changes 2016-07-25 00:51:30 -07:00
Girish Ramakrishnan 12915ee169 incremental infra creation 2016-07-25 00:39:57 -07:00
Girish Ramakrishnan e5a34581b1 compare objects instead of just the version
this is in preparation of incremental infra updates. in that case,
an equal infra version is not enough (the images could have changed).
infra version will only signify if containers need to be recreated
wholesum.
2016-07-24 23:19:13 -07:00
Girish Ramakrishnan 5c53aec837 setup mail aliases whenever mail container is created 2016-07-24 23:04:27 -07:00
Girish Ramakrishnan b3a4973348 default xframeoption is sameorigin 2016-07-24 23:00:43 -07:00
Girish Ramakrishnan b45fc46ff3 emit platform ready timer only if platform succeeded 2016-07-24 22:59:47 -07:00
Girish Ramakrishnan 0c014d3e74 minor rewording 2016-07-24 22:48:16 -07:00
Johannes Zellner 520845157f Only poll every 5 sec on update 2016-07-18 14:44:08 +02:00
Johannes Zellner 3193cec6aa Skip caas cloudron details fetching for selfhosted cloudrons 2016-07-18 11:59:32 +02:00
Girish Ramakrishnan 17240c77bf pass error message and not the object 2016-07-17 10:15:14 -07:00
Johannes Zellner 82e8c8cef2 Only adjust swapiness for ec2
On DO the disk I/O seems to be much better so this is not required
2016-07-17 18:54:27 +02:00
Girish Ramakrishnan 263c68f9c2 Add placeholder text for domain name 2016-07-17 09:43:27 -07:00
Girish Ramakrishnan 2ccbd7b8d1 Remove application specific configure link
Most people won't visit it this way and it is just cluttering the UI
2016-07-15 11:23:30 -07:00
Johannes Zellner 3c6c575db9 fixup changes typo 2016-07-15 19:00:30 +02:00
Johannes Zellner 3300c6b47a Make the system use swap only when needed
The default swappiness is 60 on ubuntu. This sets the tendency
to swap out memory pages to be more frequent, which in our case
means swapping out to networked disks and increase the cpu load
a lot, which is especially bad on EC2
2016-07-15 14:07:02 +02:00
Johannes Zellner 679d948857 Add 0.17.2 changes 2016-07-15 13:18:54 +02:00
Johannes Zellner c00267a650 Increase default button color contrast 2016-07-15 12:37:52 +02:00
Johannes Zellner 6d1a382381 Prepare for more advanced config options 2016-07-15 12:35:08 +02:00
Johannes Zellner 8e2f259712 Fix radio button margin 2016-07-15 12:34:50 +02:00
Johannes Zellner 0a85f91175 Add error reporting for xFrameOptions field 2016-07-15 11:45:41 +02:00
Johannes Zellner fe81cad9a2 Add help description 2016-07-15 11:36:12 +02:00
Johannes Zellner 3331d1aa13 Ensure the X-Frame-Options header has a single string argument 2016-07-15 11:26:05 +02:00
Johannes Zellner ae35c20227 Pass down the xFramOptions to the app configure route 2016-07-15 11:18:04 +02:00
Johannes Zellner a49e1b5117 Set xFrameOptions fallback 2016-07-15 11:08:11 +02:00
Johannes Zellner 286f360908 Basic ui to specify xFrameOptions 2016-07-14 17:15:25 +02:00
Johannes Zellner 7f6360361f Fixup the appsdb tests 2016-07-14 16:28:59 +02:00
Johannes Zellner 0d5d54d2d8 Add xFrameOptions to apps and routes 2016-07-14 16:28:59 +02:00
Johannes Zellner 37563ee8cb Add xFrameOptions to appsdb.js 2016-07-14 16:28:59 +02:00
Johannes Zellner e902e11024 Add apps.xFrameOptions column 2016-07-14 16:28:59 +02:00
Johannes Zellner dcb14b452b Validate xFrameOptions in app install 2016-07-14 16:28:59 +02:00
Johannes Zellner 66049a9e2d Support x-frame-options in appconfig.ejs template 2016-07-14 16:28:59 +02:00
Johannes Zellner 4b40084c7f Sorry this test needs even more time for me 2016-07-14 16:28:11 +02:00
Johannes Zellner 33c701ece7 Adjust migrate route tests to new api 2016-07-14 16:17:32 +02:00
Johannes Zellner cfeab2db42 Remove hack to show bootstrap tooltip 2016-07-14 12:48:21 +02:00
Johannes Zellner ebb564f623 Add tooltip to show the exact time in event log 2016-07-14 12:46:52 +02:00
Johannes Zellner d501310dc3 Add angular directives for bootstrap
https://angular-ui.github.io/bootstrap/
2016-07-14 12:46:24 +02:00
Girish Ramakrishnan 0c4772db23 merge website link and author name 2016-07-13 10:55:01 -07:00
Girish Ramakrishnan 21c5033e34 remove unused variable 2016-07-13 10:16:45 -07:00
Johannes Zellner 46d725157f show website and author on one line to give room for last updated 2016-07-13 14:55:05 +02:00
Girish Ramakrishnan b84ce23c12 Add 0.17.1 changes 2016-07-12 16:04:50 -07:00
Girish Ramakrishnan 75889af198 Use latest mail container (outbound dkim crash fix) 2016-07-12 16:03:13 -07:00
Girish Ramakrishnan c0f944c1bf use safe.require instead 2016-07-12 11:37:44 -07:00
Girish Ramakrishnan 743a8650f0 Add ability to setup a ghost account for caas 2016-07-12 11:01:02 -07:00
Johannes Zellner 94ee636254 No need to again check the groups for admin
This is already in user.get() which is attached to req.user
2016-07-12 10:11:04 -07:00
Girish Ramakrishnan 57d2fda14c do not validate dns config in test mode 2016-07-12 09:44:58 -07:00
Girish Ramakrishnan a26168e3cd test: put network name in the end 2016-07-12 09:28:25 -07:00
Girish Ramakrishnan 5deadbfdc7 make prettyDate work for more than 30 days 2016-07-09 13:06:59 -07:00
Girish Ramakrishnan 7b7e3b5950 show "Last updated" 2016-07-09 13:03:40 -07:00
Girish Ramakrishnan bcc1b6343e initialize appId before icon is saved 2016-07-09 12:25:00 -07:00
Girish Ramakrishnan d6e275aaf0 0.17.0 changes 2016-07-08 12:52:00 -07:00
Girish Ramakrishnan 78f0992935 set button action 2016-07-08 12:26:32 -07:00
Girish Ramakrishnan f6dfb70afc give autofocus to domain field 2016-07-08 12:12:55 -07:00
Girish Ramakrishnan 11c530b605 do global replace
Otherwise, this breaks title like "Tiny Tiny RSS"
2016-07-08 11:16:39 -07:00
Girish Ramakrishnan 61af079358 fix debug message 2016-07-08 10:46:40 -07:00
Girish Ramakrishnan 3335936e35 fix cors crash with malformed origin 2016-07-07 16:42:45 -07:00
Girish Ramakrishnan 5a6b5f945d fix fqdn for naked domain 2016-07-07 13:14:15 -07:00
Girish Ramakrishnan 87d54b3883 fix comment 2016-07-07 13:12:53 -07:00
Girish Ramakrishnan 32bce6a9a8 add some debugging info 2016-07-07 12:30:08 -07:00
Girish Ramakrishnan fc932487e5 fix typo 2016-07-07 12:28:13 -07:00
Girish Ramakrishnan 9ad4e61b87 query box status via apiServerOrigin otherwise we hit nxdomain 2016-07-06 16:26:40 -05:00
Girish Ramakrishnan 44e7d87aac setup apiServerOrigin for splash page 2016-07-06 16:26:26 -05:00
Girish Ramakrishnan 2637b740ab fix cloudron.retire 2016-07-06 13:12:09 -05:00
Girish Ramakrishnan 1caf4e9e76 remove the isConfigured check entirely
good thing is that we will not check if the my. cert is valid each
time on start up which will work out well when restoring from
old backups with an outdated cert.
2016-07-06 10:11:54 -05:00
Johannes Zellner f0f01453ec Make the backup cronjob run every 4 hours again
This only calls ensureBackup() which in turn would only
trigger one once per day
2016-07-06 14:54:31 +02:00
Girish Ramakrishnan dc78aab821 remove quoting of the json 2016-07-05 22:50:54 -05:00
Girish Ramakrishnan 9b4a400694 typo 2016-07-05 22:34:51 -05:00
Girish Ramakrishnan d9b61500b3 redirect to new domain if it responds to progress 2016-07-05 22:10:12 -05:00
Girish Ramakrishnan 2d01f2a0e9 pass migrate reason all the way to splash code 2016-07-05 22:04:24 -05:00
Girish Ramakrishnan c3b9ed934d use ng-if since the password has a required tag 2016-07-05 17:40:11 -05:00
Girish Ramakrishnan b49d3bd639 fix grammar 2016-07-05 17:33:53 -05:00
Girish Ramakrishnan 9096e16e37 redirect to update.html when migrating domain 2016-07-05 16:29:27 -05:00
Girish Ramakrishnan 944b3a9da1 route53: do not use appFqdn 2016-07-05 16:28:11 -05:00
Girish Ramakrishnan 8d1ff3140a fix typo 2016-07-05 16:16:43 -05:00
Girish Ramakrishnan 88fd25eff4 init customDomain only if we have an access key 2016-07-05 16:10:34 -05:00
Girish Ramakrishnan 57332eb0ce add some space 2016-07-05 16:01:09 -05:00
Girish Ramakrishnan 6b16ce04ab show dns & certs 2016-07-05 15:52:55 -05:00
Girish Ramakrishnan efba474aa5 add js for migrating domain 2016-07-05 15:24:22 -05:00
Girish Ramakrishnan 0a45f087f2 custom domain migration ui (no javascript yet) 2016-07-05 14:38:38 -05:00
Girish Ramakrishnan 95d7d9192d show the domain name 2016-07-05 14:35:12 -05:00
Girish Ramakrishnan bbe21d36c6 dns config: make form a dialog 2016-07-05 14:15:24 -05:00
Girish Ramakrishnan 82a3ac5382 do not reload when dns config changes 2016-07-05 13:38:24 -05:00
Girish Ramakrishnan 26ce8cf7ac do not redirect for non-custom domain 2016-07-05 12:29:30 -05:00
Girish Ramakrishnan 812a6c7ea2 make certificates section visible for non-custom domains 2016-07-05 12:29:30 -05:00
Girish Ramakrishnan 202af95502 Show credentials only for custom domain 2016-07-05 12:29:26 -05:00
Girish Ramakrishnan ff9fb1912b reword text a bit 2016-07-05 12:16:45 -05:00
Girish Ramakrishnan f2c5d8d016 DNS -> Domain 2016-07-05 12:15:25 -05:00
Girish Ramakrishnan eaeaf92c1a handle BAD_FIELD 2016-07-04 23:32:44 -05:00
Girish Ramakrishnan 70034602c7 handle error 2016-07-04 23:32:44 -05:00
Girish Ramakrishnan dcc6108da1 pass req.body instead of options 2016-07-04 23:32:44 -05:00
Girish Ramakrishnan 6acd01eaae pass domain as part of the config 2016-07-04 23:22:50 -05:00
Girish Ramakrishnan e5baee82e8 one of size, region, domain is allowed in migration route 2016-07-04 23:22:50 -05:00
Girish Ramakrishnan 1126626b51 implement domain migration 2016-07-04 22:30:25 -05:00
Girish Ramakrishnan ab1b5f89a1 validate route53 credentials 2016-07-04 19:42:17 -05:00
Girish Ramakrishnan 21c5491717 bump infra version (for mail container) 2016-07-04 16:17:53 -05:00
Johannes Zellner fb5467d1cd Only show plan selection UI for caas 2016-07-04 16:48:42 +02:00
Johannes Zellner 3f5d974c0c Minor echo to ec2 image building 2016-07-04 14:04:41 +02:00
Johannes Zellner e422357670 Set the correct hostname in start.sh 2016-07-04 10:41:54 +02:00
Johannes Zellner 53d03698ad Setup admin certs if we are configured 2016-07-04 10:18:39 +02:00
Girish Ramakrishnan c8a3af83ff use latest mail container (subaddress alias fix) 2016-07-03 08:37:45 -05:00
Girish Ramakrishnan 2e2b75bab2 dns ui: add collapse button 2016-07-03 08:29:55 -05:00
Girish Ramakrishnan d259a3f326 move developer mode settings into API view 2016-07-02 22:33:02 -05:00
Girish Ramakrishnan a92adf07f4 move support to separate section 2016-07-02 22:22:25 -05:00
Girish Ramakrishnan ff428ba5bf Fix migrate API 2016-07-02 17:48:49 -05:00
Girish Ramakrishnan a5def529bb refactor migrate to take options 2016-07-02 16:03:21 -05:00
Johannes Zellner 78fec9ec9b Adjust changes 2016-07-02 18:53:23 +02:00
Girish Ramakrishnan bd5c1269f6 remove jslint 2016-07-02 11:42:52 -05:00
Girish Ramakrishnan 55e2043eca pass domain argument to cloudron.migrate 2016-07-02 11:23:52 -05:00
Girish Ramakrishnan bfd92bf7ed do not rely on appstore for billing, plan, currency 2016-07-02 10:41:10 -05:00
Girish Ramakrishnan 4983120ae8 rename getCloudronDetails 2016-07-02 10:30:12 -05:00
Girish Ramakrishnan 200ae149a9 handle appstore failure when setting plan 2016-07-02 10:23:00 -05:00
Girish Ramakrishnan a863b8fa22 fix cors test 2016-07-02 06:03:33 -05:00
Girish Ramakrishnan 9315e7eb65 read all params individually 2016-07-01 15:27:36 -05:00
Johannes Zellner 982bfc313c Do not allow so send cookies in cors use case 2016-07-01 20:31:43 +02:00
Girish Ramakrishnan 4aa2ce4501 initialize currency in onReady 2016-06-30 17:40:18 -05:00
Girish Ramakrishnan 15d5ff1c51 list any custom plan 2016-06-30 17:28:40 -05:00
Girish Ramakrishnan 505ede7f42 make entire label clickable 2016-06-30 15:49:13 -05:00
Johannes Zellner 88b2ef65cc Pass the provider setting through to the update call 2016-06-30 19:24:36 +02:00
Johannes Zellner 7fc1126e1f Remove unused installer.sh.ejs 2016-06-30 19:03:40 +02:00
Girish Ramakrishnan 8f7e5f154b send complete plan information (since we do not have id) 2016-06-30 12:00:05 -05:00
Girish Ramakrishnan 412243e656 Fix plan selection 2016-06-30 11:48:06 -05:00
Johannes Zellner f06c218bd1 Give the base image creation instance a name 2016-06-30 16:00:53 +02:00
Johannes Zellner 4149a5908b Only trigger cloudron retire and print any errors, but always succeed 2016-06-30 15:05:18 +02:00
Johannes Zellner e82c33b896 Revert "Increase sysadmin route timeout as stopping might take longer"
This reverts commit 900db217ddb84ab324187ab29bf61e593f824e4a.
2016-06-30 15:01:42 +02:00
Johannes Zellner 9182038d12 Revert "Do not wait for cloudron.target to stop"
This reverts commit dcfe2e4fdbcbd2acb98cefe9b50ef0bb1828eb48.
2016-06-30 15:01:32 +02:00
Johannes Zellner da836d6bbe ami region, image separator is a = 2016-06-30 14:39:30 +02:00
Johannes Zellner 894d63554b Ensure all amis are public and available 2016-06-30 14:25:05 +02:00
Johannes Zellner 568593db93 Use t2.small for EC2 image creation 2016-06-30 11:15:51 +02:00
Johannes Zellner 14983861c0 Do not wait for cloudron.target to stop
This will allow the box code to respond properly to the retire request
2016-06-30 10:52:50 +02:00
Johannes Zellner f319919a4f Increase sysadmin route timeout as stopping might take longer 2016-06-30 10:52:50 +02:00
Girish Ramakrishnan f2c897a87d load webadmin after migration 2016-06-29 23:41:42 -05:00
Girish Ramakrishnan 9c8166a2b8 Add some time information 2016-06-29 23:37:39 -05:00
Girish Ramakrishnan 0642e64ccb fix title of update page for migrations 2016-06-29 23:26:47 -05:00
Girish Ramakrishnan 77bd5bfcbe pass retire reason 2016-06-29 23:24:00 -05:00
Girish Ramakrishnan 9a1392b784 set currency based on config 2016-06-29 19:01:41 -05:00
Girish Ramakrishnan 4dabf7bb26 send currency information 2016-06-29 18:59:50 -05:00
Girish Ramakrishnan 4250a26967 send plan information 2016-06-29 18:57:06 -05:00
Girish Ramakrishnan 14ca94be78 fix typo 2016-06-29 18:24:44 -05:00
Girish Ramakrishnan bcc3b4aee7 Make only current plan bold 2016-06-29 15:27:35 -05:00
Johannes Zellner 4d47c21a74 Also prevent autofill for developer mode modal 2016-06-29 21:33:12 +02:00
Johannes Zellner c75b38ec56 Prevent autofill for planchange password input field 2016-06-29 21:29:28 +02:00
Girish Ramakrishnan 64b59a3047 make current plan bold 2016-06-29 14:11:00 -05:00
Girish Ramakrishnan 3a7eb74e28 match ams instead of ams3 region 2016-06-28 17:23:00 -05:00
Girish Ramakrishnan e64a85150a use ams2 since ams3 is over capacity 2016-06-28 17:20:42 -05:00
Girish Ramakrishnan 4939363296 set font-weight to normal for plans 2016-06-28 16:56:25 -05:00
Girish Ramakrishnan 4be3f484d0 check tag type 2016-06-28 16:56:21 -05:00
Girish Ramakrishnan 9bfbdbba3b handle migrate in update.html 2016-06-28 16:21:22 -05:00
Girish Ramakrishnan 0c3de27c3d better progress message 2016-06-28 16:16:15 -05:00
Girish Ramakrishnan 24e36dc24c clean up code 2016-06-28 16:09:44 -05:00
Girish Ramakrishnan 1fb4c80951 remove bad comment 2016-06-28 16:02:35 -05:00
Girish Ramakrishnan 43193a6394 display currency based on region 2016-06-28 15:58:55 -05:00
Girish Ramakrishnan 66fd20e1ff self-retire after migration call 2016-06-28 15:43:39 -05:00
Girish Ramakrishnan 84c5e7bdeb redirect to update.html after migrate call succeeded 2016-06-28 15:41:34 -05:00
Girish Ramakrishnan c7c6944e5f set migrate progress 2016-06-28 15:34:04 -05:00
Girish Ramakrishnan 823290aa29 remove jslint comment 2016-06-28 15:31:08 -05:00
Girish Ramakrishnan 118f36e115 use ng-checked to preselect current plan 2016-06-28 15:29:28 -05:00
Girish Ramakrishnan 2802d5f49b just use margin-left 2016-06-28 14:53:11 -05:00
Girish Ramakrishnan ad9bb6555b fix button alignment 2016-06-28 14:51:47 -05:00
Girish Ramakrishnan 3ec3f172bb migrate UI fixes 2016-06-28 14:36:19 -05:00
Girish Ramakrishnan b9f0efa778 send password for migrate 2016-06-28 14:02:45 -05:00
Girish Ramakrishnan 41e33e71c8 initial migrate UI 2016-06-28 13:45:50 -05:00
Johannes Zellner ed5ebcbd5c Copy our AMIs to all EC2 regions 2016-06-28 12:54:59 +02:00
Johannes Zellner 914ebcb37d Changes for 0.16.6 2016-06-28 12:14:01 +02:00
Johannes Zellner 8769a1d15b Only backup once per day 2016-06-28 12:13:51 +02:00
Girish Ramakrishnan dac9f29900 Add migrate route 2016-06-27 22:40:41 -05:00
Girish Ramakrishnan eaa2058b10 remove jslint comment 2016-06-27 12:37:15 -05:00
Girish Ramakrishnan 2131c6502c 0.16.5 changes 2016-06-25 00:17:09 -05:00
Girish Ramakrishnan 0ef1cd100a bump mail container 2016-06-25 00:16:18 -05:00
Girish Ramakrishnan 5a48b90adc let the mail addon figure out how to setup aliases file 2016-06-25 00:02:19 -05:00
Girish Ramakrishnan 701a9e964f aliases have username as the "to" 2016-06-25 00:02:19 -05:00
Johannes Zellner 621fb6ddce Ensure root can login via ssh 2016-06-24 15:51:45 +02:00
Johannes Zellner d91fe9223c Dedupe the user.verify*() code 2016-06-23 11:58:10 +02:00
Johannes Zellner 7826bc2b20 Fix client secret overflow 2016-06-23 11:28:13 +02:00
Johannes Zellner 9a5e66739c Show error if client actions don't succeed due to API disabled 2016-06-23 11:25:51 +02:00
Johannes Zellner fd22f0d52b Give error feedback if client name is invalid 2016-06-23 10:34:26 +02:00
Johannes Zellner 1bf869963b Only allow alphanumerics and dash in auth client names 2016-06-23 10:16:02 +02:00
Girish Ramakrishnan d1dab8746e rewrite aliases as well and not just the destination 2016-06-22 23:26:33 -05:00
Girish Ramakrishnan 4adcd947e4 0.16.4 changes 2016-06-22 21:54:39 -05:00
Girish Ramakrishnan b08618288a setup aliases by domain name 2016-06-22 21:47:21 -05:00
Girish Ramakrishnan f9ed725002 wait (practically) forever for admin DNS propagation 2016-06-22 16:00:03 -05:00
Girish Ramakrishnan 8cfbf92adc fix acme prod setting detection 2016-06-22 15:55:53 -05:00
Girish Ramakrishnan eb93903bb8 platform might already by ready 2016-06-22 12:03:08 -05:00
Girish Ramakrishnan 501e1342b6 emit ready event if nothing to do 2016-06-22 11:53:48 -05:00
Girish Ramakrishnan 2a761a52d3 more debugs 2016-06-22 11:48:53 -05:00
Johannes Zellner ce116e56bf Remove webdav specific headers
This is not actually doing anything in that directive
2016-06-22 16:06:11 +02:00
Johannes Zellner ab9745e859 Enable root ssh access on ec2 2016-06-22 14:27:42 +02:00
Johannes Zellner ff4b1fa346 Rename createImage -> createDOImage 2016-06-22 14:07:32 +02:00
Johannes Zellner 02fcee5d98 Remove unused vars in image creation scripts 2016-06-22 14:06:58 +02:00
Johannes Zellner 152589e7dd Do not allow ec2 cloudrons to be upgraded from the UI 2016-06-22 10:21:56 +02:00
Johannes Zellner cc3f21e213 Handle new upgrade error code in routes 2016-06-22 10:21:56 +02:00
Johannes Zellner 61d8767c25 Block self upgrades on non caas cloudrons 2016-06-22 10:21:56 +02:00
Johannes Zellner 3416723129 Fix typo 2016-06-22 10:21:56 +02:00
Johannes Zellner 6477c7b47d Add comment in createEC2Image 2016-06-22 10:21:56 +02:00
Johannes Zellner 99ea4c8c30 Make amis public and available in the regions 2016-06-22 10:21:56 +02:00
Johannes Zellner ef200fcc85 Support s3 backup upload without session tokens 2016-06-22 10:21:56 +02:00
Johannes Zellner c691b75344 Make ami public (still commented) 2016-06-22 10:21:56 +02:00
Johannes Zellner c24ef743f7 Try to autodectect if running on DO or EC2 2016-06-22 10:21:56 +02:00
Johannes Zellner 77ecf1ce22 Also handle 403 status code for non-approved apps 2016-06-22 10:21:56 +02:00
Johannes Zellner c6c36a4f3c Also make box-setup.service depend on cloud-init for ec2 2016-06-22 10:21:56 +02:00
Johannes Zellner 2a3640032f Remove obsolete SELFHOSTED env 2016-06-22 10:21:56 +02:00
Johannes Zellner f0e8915825 Do not copy amis to other regions for now 2016-06-22 10:21:56 +02:00
Johannes Zellner 96dabc5694 Support ec2 user-data 2016-06-22 10:21:56 +02:00
Johannes Zellner abb3d5f0ef Copy the new image to oregon and singapore for now 2016-06-22 10:21:56 +02:00
Johannes Zellner 255d4ea088 Also terminate the instance after image creation 2016-06-22 10:21:56 +02:00
Johannes Zellner 3ac7992686 Wait for instance to come up 2016-06-22 10:21:56 +02:00
Johannes Zellner 822e886347 Take ec2 image snapshot 2016-06-22 10:21:56 +02:00
Johannes Zellner 7b5184f181 Initial script for creating ec2 base images 2016-06-22 10:21:56 +02:00
Girish Ramakrishnan f901728cc9 Fix typo 2016-06-21 16:34:31 -05:00
Girish Ramakrishnan 4f0132b371 0.16.3 changes 2016-06-21 15:19:00 -05:00
Girish Ramakrishnan 3ffc2c0440 wait for 10 minutes before giving up on external domain 2016-06-21 15:15:51 -05:00
Girish Ramakrishnan f84de690ce pass retry options to waitForDns 2016-06-21 15:12:36 -05:00
Girish Ramakrishnan 9f74fead4b make it gender netural 2016-06-21 11:59:26 -05:00
Girish Ramakrishnan c1c1fed605 0.16.2 changes 2016-06-21 11:13:56 -05:00
Girish Ramakrishnan 87584be484 restartAppTask when resuming tasks
We end up with duplicate tasks because the auto installer might
have queued up some pending_install tasks on start up.
2016-06-21 10:56:25 -05:00
Girish Ramakrishnan 5a61c5ba51 platform: ready event 2016-06-21 10:46:02 -05:00
Girish Ramakrishnan fbf7e6804f add note for resuming tasks 2016-06-21 10:33:38 -05:00
Girish Ramakrishnan dfa4d093f7 test: ensure redis does not export the port 2016-06-20 22:59:47 -05:00
Girish Ramakrishnan acfeb85d4a redis: do not publish the port on the host 2016-06-20 22:46:04 -05:00
Girish Ramakrishnan b3ad463470 use debug instead 2016-06-20 22:42:37 -05:00
Girish Ramakrishnan 1e54581c40 clear timer 2016-06-20 22:39:11 -05:00
Girish Ramakrishnan a9b91591b4 Make installationProgress TEXT
Some error messages from apptask can be very long! This cases the db
update to fail. In turn causing the installationState to not be set.
2016-06-20 22:30:17 -05:00
Girish Ramakrishnan b59739ec54 fix typo
this is sad. why didn't jshint catch this?
2016-06-20 21:38:39 -05:00
Girish Ramakrishnan bb9bfd542d fix bug in querying mail server IP 2016-06-20 18:26:22 -05:00
Girish Ramakrishnan 93b7e8d0a7 0.16.1 changes 2016-06-20 15:11:25 -05:00
Girish Ramakrishnan b723f9e768 errored apps can be reconfigured
this is especially true when coming from a restore because the app
always has a good backup anyway.
2016-06-20 15:07:57 -05:00
Girish Ramakrishnan 6bc14ea7e4 resumeTasks only when configured and platform ready 2016-06-20 14:46:52 -05:00
Girish Ramakrishnan cabed28f1e use redisName instead 2016-06-20 13:15:10 -05:00
Girish Ramakrishnan ea74e389d3 make generateToken less stronger to fix UI layout issues 2016-06-20 13:13:13 -05:00
Girish Ramakrishnan c37d914518 platform: rename from legacy to corrupt 2016-06-20 11:59:43 -05:00
Girish Ramakrishnan 86b8fe324e add missing comma 2016-06-20 11:15:25 -05:00
Girish Ramakrishnan 8412db0796 add missing brackets 2016-06-20 09:50:03 -05:00
Girish Ramakrishnan da9c07e369 more 0.16.0 changes 2016-06-18 17:59:09 -05:00
Girish Ramakrishnan 3c305a51ce make sure hostname is unset 2016-06-18 17:58:01 -05:00
Girish Ramakrishnan 3ec29dc9e1 Do not set hostname of app containers
do no set hostname of containers to location as it might conflict with addons names. for example, an app installed in mail
location may not reach mail container anymore by DNS. We cannot set hostname to fqdn either as that sets up the dns
name to look up the internal docker ip. this makes curl from within container fail
Note that Hostname has no effect on DNS. We have to use the --net-alias for dns.
Hostname cannot be set with container NetworkMode
2016-06-18 17:53:54 -05:00
Girish Ramakrishnan a3d185e653 fix typo 2016-06-18 14:51:49 -05:00
Girish Ramakrishnan c2da3da035 set force to false explicitly 2016-06-18 13:24:27 -05:00
Girish Ramakrishnan 43c69d4dc6 we have exceeded 100 images 2016-06-18 12:12:11 -05:00
Girish Ramakrishnan 5ba1dd39e7 add missing return 2016-06-18 12:10:30 -05:00
Girish Ramakrishnan 56bc391b38 add ui text for clone 2016-06-17 19:11:29 -05:00
Girish Ramakrishnan 7e93c23110 set the lastBackupId to backup from 2016-06-17 18:41:29 -05:00
Girish Ramakrishnan 7009b9f3ac implement clone 2016-06-17 17:45:14 -05:00
Girish Ramakrishnan 0609a90d2a add CLONE installation state 2016-06-17 16:56:15 -05:00
Girish Ramakrishnan fe62aba4d7 make appdb.add take a data object 2016-06-17 16:43:35 -05:00
Girish Ramakrishnan 224a5f370f 0.16.0 changes 2016-06-17 13:08:35 -05:00
Girish Ramakrishnan 584df9a6da fix redis query 2016-06-17 12:39:29 -05:00
Girish Ramakrishnan f31c43bbc3 fix EPIPE issue by using http instead of superagent 2016-06-17 11:12:20 -05:00
Girish Ramakrishnan 10ebff2edf fix response to say cloudron network 2016-06-17 10:10:18 -05:00
Girish Ramakrishnan cc1755105c do not allow access if app is not found 2016-06-17 10:08:41 -05:00
Girish Ramakrishnan 6c5a3997cb fix clientSecret.length test 2016-06-17 10:07:55 -05:00
Girish Ramakrishnan 2017d668a9 use 128 byte passwords 2016-06-17 09:49:25 -05:00
Girish Ramakrishnan 7e57f31d14 create cloudron network in test 2016-06-17 09:18:15 -05:00
Girish Ramakrishnan de0d42e52f search cloudron network for the apps 2016-06-17 09:18:15 -05:00
Girish Ramakrishnan dbadbd2c4e bump all the container versions 2016-06-17 09:18:15 -05:00
Girish Ramakrishnan d51d2e5131 start addons and apps in the cloudron network
also remove getLinkSync, since we don't use linking anymore
2016-06-17 09:18:10 -05:00
Girish Ramakrishnan be2c7a97b3 Use docker command instead of api to create redis container 2016-06-16 18:02:51 -05:00
Girish Ramakrishnan 2ab13d587a remove dead function 2016-06-16 17:24:38 -05:00
Girish Ramakrishnan f13f24c88d Just use docker commands 2016-06-16 15:41:50 -05:00
Girish Ramakrishnan 91bc45bd4e not sure why this is required but it makes the test more stable 2016-06-16 15:09:46 -05:00
Girish Ramakrishnan a75b9c1428 remove debugs 2016-06-16 14:59:20 -05:00
Girish Ramakrishnan d837e9f679 make test more reliable 2016-06-16 14:50:49 -05:00
Girish Ramakrishnan d5ffa53e70 create an isolated docker network named cloudron
we will move from linked containers to an isolated network. This
has the main advantage that linked containers can be upgraded.
For example, I can update the mysql container independently without
breaking the apps that require it. Apps will only see some minor
downtime and will need to reconnect.
2016-06-16 06:59:47 -07:00
Girish Ramakrishnan fee6f3de0f configure/restoreInstalledApps must always succeed 2016-06-16 06:50:34 -07:00
Girish Ramakrishnan f15f3c9052 node-ify setup_infra.sh 2016-06-15 05:26:40 -07:00
Johannes Zellner a37f87511b Prevent clickjacking by sending X-Frame-Options 2016-06-15 13:10:26 +02:00
Girish Ramakrishnan 069778caca 0.15.3 changes 2016-06-14 14:49:25 -07:00
Girish Ramakrishnan 741fe75def fix progress message 2016-06-14 14:42:29 -07:00
Girish Ramakrishnan f8b402a48e add progress message tooltip 2016-06-14 14:32:14 -07:00
Girish Ramakrishnan f53c0b7700 set radix for parseInt 2016-06-14 14:21:24 -07:00
Girish Ramakrishnan f74310f364 fix title of configure dialog 2016-06-14 14:17:05 -07:00
Girish Ramakrishnan a5a1526023 Allow configure command when in configuration state
Currently, we only allow restore (from backup) and uninstall. If configure
is taking very long (like external domain) and someone wants to reconfigure
we should let them.

We are sort of trying to think of 'reconfigure' as 'retry' in case of
external errors.
2016-06-14 14:13:47 -07:00
Girish Ramakrishnan 26f318477b Do not send crash logs for apptask cancellations 2016-06-14 14:13:47 -07:00
Girish Ramakrishnan 060d9e88ef Pass manifest to backupApp 2016-06-14 11:32:29 -07:00
Girish Ramakrishnan 9cf497a87d 0.15.2 changes 2016-06-13 23:31:35 -07:00
Girish Ramakrishnan b174765992 delete unused addonsa fter backup 2016-06-13 23:07:41 -07:00
Girish Ramakrishnan 9c2d217176 fix typo 2016-06-13 23:04:43 -07:00
Girish Ramakrishnan 3197349058 Fix app backup before updates
we were passing the current manifest to the backup code which meant that
the app version and manifest was incorrect.
2016-06-13 21:19:29 -07:00
Girish Ramakrishnan 5f3378878e remove lastBackupConfig 2016-06-13 19:19:28 -07:00
Girish Ramakrishnan 53cd45496b parse the response 2016-06-13 18:28:51 -07:00
Girish Ramakrishnan 942339435a return config correctly 2016-06-13 18:04:22 -07:00
Girish Ramakrishnan 2bd6519795 add assert 2016-06-13 18:02:57 -07:00
Girish Ramakrishnan 1763c36a0b restore from the backup's config.json
To summarize what we are doing is that restore is simply getting old data and
old code. Config is not changed. If config is required, then it has to come
in the restore REST parameter. Otherwise, there is too much magic.

https://blog.smartserver.io/2016/06/13/app-restore/
2016-06-13 16:54:59 -07:00
Girish Ramakrishnan 2c0eb33625 use apps.getAppConfig when generating config.json 2016-06-13 15:11:49 -07:00
Girish Ramakrishnan 040b9993c7 refactor code into getAppConfig 2016-06-13 15:07:15 -07:00
Girish Ramakrishnan 8f21126697 add a way to get the restore config (config.json) 2016-06-13 15:04:27 -07:00
Girish Ramakrishnan 716d29165c store altDomain in backupConfig 2016-06-13 13:14:04 -07:00
Girish Ramakrishnan a2ec308155 pass the lastBackupId explicity as the backup to restore to 2016-06-13 10:13:54 -07:00
Girish Ramakrishnan b82610ba00 pass data argument to restore 2016-06-13 10:08:58 -07:00
Johannes Zellner ed4674cd14 Disable the trial popup 2016-06-13 16:51:28 +02:00
Johannes Zellner 4e9dc75a37 Replace DatabaseError with ClientsError where applicable 2016-06-13 14:43:56 +02:00
Johannes Zellner f284b4cd83 Use clients.get() instead of clientdb.get() 2016-06-13 13:51:14 +02:00
Johannes Zellner 15cf83b37c Fixup the built-in client setup for tests 2016-06-13 13:48:43 +02:00
Johannes Zellner 0eff8911ee Do not use DatabaseError in routes clients.js 2016-06-13 13:29:39 +02:00
Girish Ramakrishnan 814a0ce3a6 wait for 10mins before sending out emails about app being down 2016-06-11 18:36:38 -07:00
Girish Ramakrishnan b3e1c221b7 bump infra to force a reconfigure for existing pr cloudrons 2016-06-11 13:49:55 -07:00
Girish Ramakrishnan dc31946e50 move webdav block outside location
when inside location, nginx is redirecting to 127.0.0.1 (no clue why)
2016-06-11 12:05:16 -07:00
Girish Ramakrishnan 36bbb98970 0.15.1 changes 2016-06-10 12:40:48 -07:00
Girish Ramakrishnan ea4cea9733 add tag on blur 2016-06-10 12:35:57 -07:00
Girish Ramakrishnan 7c06937a57 Add @fqdn hint to email aliases 2016-06-10 12:04:40 -07:00
Girish Ramakrishnan 597704d3ed remove the plain input email aliases 2016-06-10 11:58:25 -07:00
Girish Ramakrishnan 63290b9936 Final fixes to taginput 2016-06-10 11:58:00 -07:00
Girish Ramakrishnan 324222b040 Fix template code style 2016-06-09 16:11:57 -07:00
Girish Ramakrishnan f37b92da04 taginput: remove autocomplete. we don't use it 2016-06-09 10:34:42 -07:00
Girish Ramakrishnan 0de3b8fbdb taginput: add tag input control for email aliases 2016-06-09 10:34:29 -07:00
Johannes Zellner f0cb3f94cb Fixup the token ui to allow removal of user added clients 2016-06-09 16:17:14 +02:00
Johannes Zellner 1508a5c6b9 Add tokendb.delByClientId() 2016-06-09 15:42:52 +02:00
Johannes Zellner 9b9db6acf1 Only the rest api shall not allow to remove those 2016-06-09 15:35:46 +02:00
Johannes Zellner 001bf94773 Remove unused require 2016-06-09 15:35:20 +02:00
Johannes Zellner 0160c12965 Allow to distinguish between built-in auth clients and external ones 2016-06-09 15:35:00 +02:00
Johannes Zellner d08397336d Add test for addon auth clients deletion 2016-06-09 15:12:30 +02:00
Johannes Zellner 880754877d Prevent the rest api to delete addon auth clients 2016-06-09 14:44:38 +02:00
Johannes Zellner 984a191e4c Use the variable correctly 2016-06-09 14:24:53 +02:00
Girish Ramakrishnan cdca43311b wording and other minor fixes 2016-06-08 17:06:27 -07:00
Girish Ramakrishnan 020b47841a 0.15.1 changes 2016-06-08 16:42:00 -07:00
Girish Ramakrishnan 3f602c8a04 verifyWithUsername and not as id 2016-06-08 15:54:22 -07:00
Girish Ramakrishnan dea0c5642d bump mail container 2016-06-08 13:16:44 -07:00
Girish Ramakrishnan 3d2b75860b minor fixes to session ui 2016-06-08 13:04:22 -07:00
Girish Ramakrishnan 0da754a14b fix singular/plural 2016-06-08 12:52:19 -07:00
Girish Ramakrishnan 3d408c8c90 fix wording 2016-06-08 12:43:15 -07:00
Girish Ramakrishnan 40348a5132 Merge branch '0.15' 2016-06-08 11:56:24 -07:00
Girish Ramakrishnan 9a177d9e46 fix typo 2016-06-08 10:14:59 -07:00
Girish Ramakrishnan 6f1df9980d minor wording changes 2016-06-08 09:58:30 -07:00
Girish Ramakrishnan 0c9d331f47 more changes 2016-06-08 08:46:18 -07:00
Girish Ramakrishnan f9db24e162 Fix autoupdate detection logic
We should be comparing existing manifest ports with new manifest ports.
The user chosen bindings itself doesn't matter.
2016-06-08 08:45:40 -07:00
Johannes Zellner 385bf3561b Remove unused function in platform.js 2016-06-08 15:06:03 +02:00
Johannes Zellner 4304f20fe0 Fix some log output 2016-06-08 15:05:43 +02:00
Johannes Zellner 509083265f fix setup_infra.sh to accomodate ifconfig differences 2016-06-08 15:01:28 +02:00
Johannes Zellner 1b9dbd06c8 Fix typo 2016-06-08 14:55:14 +02:00
Johannes Zellner d6482414bb Fixup the clientdb tests 2016-06-08 14:49:54 +02:00
Johannes Zellner 194b9b35bd We now have 3 built-in api clients 2016-06-08 14:48:03 +02:00
Johannes Zellner 6b9acb4722 Preserve the built-in clients on db clean 2016-06-08 14:47:21 +02:00
Johannes Zellner 08c3cb9376 Insert the default auth clients for tests 2016-06-08 14:37:41 +02:00
Johannes Zellner 79631ba996 Provide a fallback for the redirect uri 2016-06-08 14:30:42 +02:00
Johannes Zellner 4776a005a5 Remove redundant client TYPE_*s 2016-06-08 14:09:06 +02:00
Johannes Zellner e954df2120 Issue developer tokens with cid-cli 2016-06-08 13:39:31 +02:00
Johannes Zellner 526a62a20e Fix the async iterator 2016-06-08 13:35:32 +02:00
Johannes Zellner e2432d002f Count activeTokens correctly 2016-06-08 13:31:17 +02:00
Johannes Zellner 6e4d6d1099 Do not show application token listing if there is nothing to show 2016-06-08 13:03:59 +02:00
Johannes Zellner fc2d1d61d7 Also logout the webadmin session 2016-06-08 13:03:21 +02:00
Johannes Zellner 4c4ae08b44 Allow users to see issued tokens and revoke them all in one 2016-06-08 12:56:48 +02:00
Johannes Zellner 401c0e1b44 Special handling for better ui for sdk tokens 2016-06-08 11:55:40 +02:00
Johannes Zellner e431bd6040 Fix typo 2016-06-08 11:36:01 +02:00
Johannes Zellner a69cd204d6 Handle sdk and cli clients just like the webadmin 2016-06-08 11:33:08 +02:00
Johannes Zellner 3c3de6205e Add test case for blocking cid-webadmin deletion 2016-06-08 11:27:10 +02:00
Johannes Zellner 16444f775d Prevent deletion of the built-in clients 2016-06-08 11:24:02 +02:00
Johannes Zellner 2676658b5d Add auth client cid-sdk and cid-cli 2016-06-08 11:20:06 +02:00
Girish Ramakrishnan fbb8a842c1 do not require password 2016-06-08 00:07:41 -07:00
Girish Ramakrishnan 62b586e8dd fix require path 2016-06-07 20:57:39 -07:00
Girish Ramakrishnan 313d98ef70 add a route to check for updates quickly 2016-06-07 20:24:41 -07:00
Girish Ramakrishnan 06448f146d fix typo 2016-06-07 20:09:53 -07:00
Girish Ramakrishnan 064d950f87 add new tests for field validation 2016-06-07 16:00:02 -07:00
Girish Ramakrishnan 3236ce9cd6 check if both are null 2016-06-07 15:36:45 -07:00
Johannes Zellner f74b22645f Token view is now admin only 2016-06-07 22:56:27 +02:00
Johannes Zellner 3540f2c197 Move token management to separate view for admins only 2016-06-07 22:54:53 +02:00
Girish Ramakrishnan 3231fe7874 use user.get which already set admin flag 2016-06-07 10:03:08 -07:00
Girish Ramakrishnan dc8fd2eab3 do not use userdb directly 2016-06-07 10:01:14 -07:00
Girish Ramakrishnan 3ae388602c fix wording 2016-06-07 09:27:42 -07:00
Johannes Zellner 733187f3c4 Also show redirectURI for developers to use 2016-06-07 16:21:33 +02:00
Johannes Zellner 02d2a7058e Remove whitespace in scope input 2016-06-07 16:15:53 +02:00
Johannes Zellner 25003bcf40 Add placeholder text in scope input 2016-06-07 16:12:56 +02:00
Johannes Zellner 234caa60eb Add form validation for scopes 2016-06-07 16:10:33 +02:00
Johannes Zellner a0227b6043 Remove now unused localhost test client
We can now simply use the regular APIs to do local development against a Cloudron
2016-06-07 16:03:50 +02:00
Johannes Zellner 46ac6c4918 Offset the footer in the console views 2016-06-07 15:58:53 +02:00
Johannes Zellner 4afde79297 Fix error message 2016-06-07 15:56:22 +02:00
Johannes Zellner 17d48f3fce Specify the expiration on the client. Currently this is 100 years.
I am not sure if this is the best approach, or if we should introduce a magic value instead.
2016-06-07 15:54:32 +02:00
Johannes Zellner facdabcc8d Mention the token expiration in the ui 2016-06-07 15:54:09 +02:00
Johannes Zellner 691803f10b Allow optional expiresAt to be set on token creation 2016-06-07 15:47:13 +02:00
Johannes Zellner 8144c6d086 Patch up the token delete button with the route 2016-06-07 15:38:42 +02:00
Johannes Zellner 290ab6cc7d Fix typo 2016-06-07 15:38:30 +02:00
Johannes Zellner 8e5af17e5d Add route to delete a single token 2016-06-07 15:34:27 +02:00
Johannes Zellner d9d94faf75 Refresh the client on token actions 2016-06-07 15:15:56 +02:00
Johannes Zellner 0201ab19e4 Pass in the client object 2016-06-07 14:47:56 +02:00
Johannes Zellner 721fe74f3c The route creates the subobject 2016-06-07 14:47:47 +02:00
Johannes Zellner 96eeb247a1 Add rest api to create a new token for a client 2016-06-07 14:29:37 +02:00
Johannes Zellner 6261231593 add ui for token generation 2016-06-07 14:19:20 +02:00
Johannes Zellner d62d2b17fe Select tokens and secrets with single click 2016-06-07 14:10:20 +02:00
Johannes Zellner 89cef4f050 Show tokens for admins 2016-06-07 14:07:41 +02:00
Johannes Zellner 8602e033c5 Only show active clients for non-admins 2016-06-07 13:46:45 +02:00
Johannes Zellner 3598d89b12 ?all is gone in clients route 2016-06-07 13:17:02 +02:00
Johannes Zellner ffd552583c Patch up the client remove ui 2016-06-07 13:12:53 +02:00
Johannes Zellner 9eabc9d266 Fixup the wording 2016-06-07 12:50:52 +02:00
Johannes Zellner edf8cd736e Add modal for client removal 2016-06-07 12:48:12 +02:00
Johannes Zellner c5ebe2c2bf Fixup the panel padding 2016-06-07 12:48:00 +02:00
Johannes Zellner 5d0ccc0dd7 Show correct token count 2016-06-07 12:37:18 +02:00
Johannes Zellner 4147455654 Fetch tokens for each client separately 2016-06-07 12:37:04 +02:00
Johannes Zellner f3436a99a2 Fixup the client list details 2016-06-07 12:28:33 +02:00
Johannes Zellner 70d569e2e8 List all oauth clients in webadmin 2016-06-07 12:26:14 +02:00
Johannes Zellner 684625fbaf Remove unused clients.getAllWithDetailsByUserId() 2016-06-07 12:25:48 +02:00
Johannes Zellner c8b9ae542c Simply return oauth clients instead of join with tokendb 2016-06-07 12:15:25 +02:00
Johannes Zellner af29c1ba86 Handle external api client requests separately 2016-06-07 12:00:18 +02:00
Johannes Zellner 207e81345f Log event for external login 2016-06-07 11:59:54 +02:00
Johannes Zellner d880731351 Support ?all query param for oauth clients get route 2016-06-07 11:18:30 +02:00
Johannes Zellner e603cfe96e Add clients.getAllWithDetails() 2016-06-07 11:17:29 +02:00
Johannes Zellner 5b93a2870f Add clientdb.getAllWithTokenCount() 2016-06-07 11:17:14 +02:00
Johannes Zellner 1214300800 Consistent code styling 2016-06-07 11:08:35 +02:00
Johannes Zellner 8159334cbf Avoid more inlining 2016-06-07 10:55:36 +02:00
Johannes Zellner 78135c807a Be consistent in client.js add -> create 2016-06-07 10:49:11 +02:00
Johannes Zellner bfa33e4d8e Fixup a linter issue 2016-06-07 10:48:36 +02:00
Johannes Zellner 8b23174769 add client.addOAuthClient() 2016-06-07 10:45:50 +02:00
Johannes Zellner a078c94b97 Ensure autofocus is handled 2016-06-07 10:43:33 +02:00
Johannes Zellner c86392cd60 Add modal dialog to create API clients 2016-06-07 10:42:54 +02:00
Johannes Zellner f0e9256d46 Avoid style inlining 2016-06-07 10:10:58 +02:00
Girish Ramakrishnan 0cd4e4f03a update now takes appStoreId 2016-06-04 20:51:17 -07:00
Girish Ramakrishnan 1766da9174 update code path now takes appStoreId 2016-06-04 20:05:29 -07:00
Girish Ramakrishnan dbdcf1ec27 pass data object to update 2016-06-04 19:12:36 -07:00
Girish Ramakrishnan c916ea2589 fix style 2016-06-04 18:56:53 -07:00
Girish Ramakrishnan 5540b5f545 remove unused require 2016-06-04 18:55:31 -07:00
Girish Ramakrishnan 1e38190e68 setting falsy values for cert/key removes it 2016-06-04 18:30:05 -07:00
Girish Ramakrishnan 8f3553090f make args optional in configure 2016-06-04 18:07:06 -07:00
Girish Ramakrishnan cc0f5a1f03 fix configure arg insanity 2016-06-04 16:32:27 -07:00
Girish Ramakrishnan a1c531d2a8 better type checking in configure and make accessRestriction optional 2016-06-04 16:27:50 -07:00
Girish Ramakrishnan 57cb3b04d7 generate 2048-bit keys 2016-06-04 15:59:53 -07:00
Girish Ramakrishnan a49cf98a8d do not allow appId to be set
this is some legacy code
2016-06-04 13:40:43 -07:00
Girish Ramakrishnan da6cab8dd6 we return 400 now 2016-06-04 13:32:41 -07:00
Girish Ramakrishnan 3b7cfdd7db better type checking 2016-06-04 13:31:18 -07:00
Girish Ramakrishnan f9251c8b37 sending manifest is now redundant 2016-06-04 13:23:13 -07:00
Girish Ramakrishnan 4068ff5f21 add TODO note to validate accessRestriction 2016-06-04 13:20:10 -07:00
Girish Ramakrishnan ee073c91a3 return BAD_FIELD if app was not found 2016-06-04 13:15:38 -07:00
Girish Ramakrishnan 9e8742ca87 download manifest from appstore when appStoreId is provided 2016-06-04 01:07:43 -07:00
Girish Ramakrishnan 7f99fe2399 appStoreId has empty string default 2016-06-03 23:58:09 -07:00
Girish Ramakrishnan bfe8df35df toLowerCase in one place 2016-06-03 23:54:46 -07:00
Girish Ramakrishnan e2848d3e08 fix apps.install insane arg list 2016-06-03 23:35:55 -07:00
Girish Ramakrishnan bc823b4a75 make checkManifestConstraints return AppsError 2016-06-03 22:19:09 -07:00
Girish Ramakrishnan c24f780722 make validateAccessRestriction and validateMemory return AppsError 2016-06-03 22:16:55 -07:00
Girish Ramakrishnan 0d51ec9920 make validatePortBindings return AppsError 2016-06-03 22:15:02 -07:00
Girish Ramakrishnan e07e544029 make validateHostname return AppsError 2016-06-03 22:14:08 -07:00
Girish Ramakrishnan 5aff55c5ca typo when stashing altDomain 2016-06-03 19:50:01 -07:00
Girish Ramakrishnan 5ebc29746d fix failing tests 2016-06-03 19:14:16 -07:00
Girish Ramakrishnan 8fc44e6bc9 remove redundant checks 2016-06-03 19:08:47 -07:00
Girish Ramakrishnan 44f4872134 remove dead comments 2016-06-03 17:55:05 -07:00
Girish Ramakrishnan 49dd584a41 return expiresAt as ISO-string for API consistency 2016-06-03 10:11:09 -07:00
Girish Ramakrishnan 6d8f1f90d4 add note to change expires to TIMESTAMP type 2016-06-03 10:07:30 -07:00
Girish Ramakrishnan c1ded66c1a make download_url a post route 2016-06-03 09:23:15 -07:00
Johannes Zellner 4df49a82e5 Some clientdb.TYPE_ oversight in clients.js 2016-06-03 15:28:04 +02:00
Johannes Zellner 92e6ee9539 The clientSecret is now only ever created in the clients.js 2016-06-03 15:11:08 +02:00
Johannes Zellner 3ad2a2a5ca Fixup the unit tests 2016-06-03 15:07:44 +02:00
Johannes Zellner 226537de04 Move client TYPE_* to clients.js 2016-06-03 15:05:00 +02:00
Johannes Zellner 41b324eb2d Remove clientdb usage in addons.js 2016-06-03 14:56:45 +02:00
Johannes Zellner 1360729e97 Don't use clientdb directly from auth.js and apptask.js 2016-06-03 14:52:59 +02:00
Johannes Zellner 725e1debcc Provide getByAppIdAndType() by clients.js 2016-06-03 14:47:06 +02:00
Johannes Zellner 201efa70b7 use clients instead of clientdb in oauth2.js 2016-06-03 14:38:58 +02:00
Johannes Zellner c52d0369fa Provide better feedback on invalid scopes 2016-06-03 13:53:33 +02:00
Johannes Zellner b4dfad3aa3 Fixup the unit tests after removing PREFIX_USER 2016-06-03 13:09:26 +02:00
Johannes Zellner 7667cdc66d PREFIX_USER finally gone 2016-06-03 13:01:23 +02:00
Johannes Zellner 3a9a667890 Make all token grants without PREFIX_USER 2016-06-03 13:01:05 +02:00
Johannes Zellner 304cfed5a9 Result of password setting is now a plain token identifier 2016-06-03 13:00:07 +02:00
Johannes Zellner 778c583a52 Activation hands out a token without PREFIX_USER now 2016-06-03 12:59:13 +02:00
Johannes Zellner f988bb4d14 Do not use PREFIX_USER for token managment 2016-06-03 12:58:39 +02:00
Johannes Zellner 7057f1aaa2 All token identifiers are now plain user ids 2016-06-03 12:54:59 +02:00
Johannes Zellner e06f5f88b8 Remove the token types 2016-06-03 12:54:34 +02:00
Johannes Zellner 03cd3f0b6f Remove attached tokenType on req.user 2016-06-03 12:53:11 +02:00
Johannes Zellner 615f875169 Remove PREFIX_DEV for developer tokens 2016-06-03 12:52:10 +02:00
Johannes Zellner f27ba04a00 Add test case for developer tokens 2016-06-03 11:11:11 +02:00
Johannes Zellner 3e0006a327 Allow tokens with SCOPE_ROLE_SDK through without a password 2016-06-03 11:10:59 +02:00
Johannes Zellner 558ca42ae8 Issue developer tokens with SCOPE_ROLE_SDK 2016-06-03 11:10:22 +02:00
Johannes Zellner 9d8a803185 Handle scope roles in scope checks 2016-06-03 11:09:48 +02:00
Johannes Zellner 105047b0c4 Add SCOPE_ROLE_SDK 2016-06-03 11:08:35 +02:00
Johannes Zellner e335aa5dee Check for sdk token instead of token type DEV 2016-06-03 10:17:52 +02:00
Johannes Zellner 10163733db Separate the scope checking 2016-06-03 10:10:58 +02:00
Girish Ramakrishnan 251fad8514 add test for groupIds in listing api 2016-06-03 00:14:52 -07:00
Girish Ramakrishnan 036740f97b filter out correct fields in the route code 2016-06-03 00:04:17 -07:00
Girish Ramakrishnan f4958d936c return groupIds in get user route 2016-06-03 00:00:11 -07:00
Girish Ramakrishnan 80ca69a128 user.update does not need the user object 2016-06-02 23:53:06 -07:00
Girish Ramakrishnan 097d23c412 move logic to model code 2016-06-02 23:29:43 -07:00
Girish Ramakrishnan 13a1213b0d make group listing API return member userIds 2016-06-02 21:07:33 -07:00
Girish Ramakrishnan 76fe2bf531 add note to fix precision at some point 2016-06-02 19:43:23 -07:00
Girish Ramakrishnan 50c4e4c91e log event only after lock is acquired 2016-06-02 19:26:58 -07:00
Girish Ramakrishnan 46441d1814 cloudron.update is not exposed 2016-06-02 19:23:21 -07:00
Girish Ramakrishnan a4e73be834 pass auditSource for certificate renewal 2016-06-02 18:54:45 -07:00
Girish Ramakrishnan 6be0d0814d pass auditSource from cron.js 2016-06-02 18:51:50 -07:00
Girish Ramakrishnan e30d71921e pass auditSource for app autoupdater 2016-06-02 18:49:56 -07:00
Girish Ramakrishnan a49c78f32c make box autoupdate generate eventlog 2016-06-02 18:47:09 -07:00
Girish Ramakrishnan b077223e58 fix scope name 2016-06-02 17:49:54 -07:00
Girish Ramakrishnan d2864dfe56 rename root scope to cloudron scope (for lack of better scope name) 2016-06-02 16:51:14 -07:00
Girish Ramakrishnan 6d08af35a8 give developer token root scope 2016-06-02 15:58:40 -07:00
Girish Ramakrishnan 54f9d653f7 fix error messages 2016-06-02 14:41:21 -07:00
Girish Ramakrishnan 8d65f93fa4 return error.message 2016-06-02 14:40:29 -07:00
Girish Ramakrishnan 462440bb30 do not check for password in profile route
This is already checked by the verifyPassword middleware based on
the token type.

When using dev tokens, this check barfs for lack of password field
even when none is required.
2016-06-02 14:26:01 -07:00
Girish Ramakrishnan 65261dc4d5 add time_zone setter route 2016-06-02 13:54:07 -07:00
Girish Ramakrishnan 54ead09aac make the name API work
currently this only works for the main webadmin (and not for
nakeddomain, error etc) but that's fine.
2016-06-02 13:25:02 -07:00
Girish Ramakrishnan 28b3550214 use error.message 2016-06-02 13:00:23 -07:00
Girish Ramakrishnan e2e70da4c5 restrict length to 32 2016-06-02 12:51:49 -07:00
Johannes Zellner 7326ea27ca Only set username and displayName after successful update 2016-06-02 21:12:02 +02:00
Girish Ramakrishnan 1fe00f7f80 do not use verbs in resource url 2016-06-02 12:01:48 -07:00
Girish Ramakrishnan e9e9d6000d remove token check for user.update to work with dev tokens 2016-06-02 11:29:59 -07:00
Girish Ramakrishnan 6dccb3655f add no groups available message in edit user dialog 2016-06-02 10:55:34 -07:00
Girish Ramakrishnan c3113bd74d go back to step2 if activation fails 2016-06-02 10:40:06 -07:00
Girish Ramakrishnan e79119b72a 0.15.0 changes 2016-06-02 10:32:10 -07:00
Johannes Zellner 086cfdc1e6 Disabled form fields are not POSTed
I did not know about that fact, one has to use readonly
2016-06-02 16:12:32 +02:00
Johannes Zellner 1f091d3b4b We have to let angular know 2016-06-02 16:06:15 +02:00
Johannes Zellner 892fa4b2ec We still require the username to be sent always 2016-06-02 16:01:25 +02:00
Johannes Zellner a87b4b207c Adhere to already set username in user setup view 2016-06-02 15:47:58 +02:00
Johannes Zellner bdd14022d6 Report user conflict message all the way through the rest routes 2016-06-02 15:41:07 +02:00
Johannes Zellner 3d40cf03b1 Pass down the reason why the user conflicts 2016-06-02 15:39:21 +02:00
Johannes Zellner 594be7dbbd Allow the userdb code to distinguish between username or email duplicates 2016-06-02 15:34:27 +02:00
Johannes Zellner a52e2ffc23 Distinguish between username and email conflict 2016-06-02 15:19:35 +02:00
Johannes Zellner 8eeee712aa Remove unused require 2016-06-02 14:14:16 +02:00
Johannes Zellner 0f62faa198 All our tokens are now representing an user with a profile 2016-06-02 14:13:57 +02:00
Johannes Zellner bfd66cf309 Remove unused token PREFIX_APP 2016-06-02 14:07:41 +02:00
Johannes Zellner c2f7d61e34 Remove unused token TYPE_APP 2016-06-02 14:07:19 +02:00
Johannes Zellner d5d5e356ae Add error handling for invalid usernames 2016-06-02 13:52:44 +02:00
Johannes Zellner 531752cd43 Use placeholder for description in username and displayName fields 2016-06-02 13:52:33 +02:00
Johannes Zellner 9eac56578c Allow admins to set the username and displayName optionally on user creation 2016-06-02 13:32:45 +02:00
Johannes Zellner d06398dbfd Move webdav nginx fixes into app endpoint
Not sure if this will now still work with oauth proxy though.
2016-06-02 09:49:01 +02:00
Girish Ramakrishnan 60ce6b69ee profile updates must be POST 2016-06-02 00:31:41 -07:00
Girish Ramakrishnan 4fcc7fe99f updateUser is POST 2016-06-02 00:27:06 -07:00
Girish Ramakrishnan 82cd215ffa merge bad fields and pass error.message correctly in REST responses 2016-06-02 00:12:21 -07:00
Girish Ramakrishnan 1dcea84068 fix typo 2016-06-01 23:43:21 -07:00
Girish Ramakrishnan 4107252bfe developer mode is true by default 2016-06-01 23:13:12 -07:00
Girish Ramakrishnan 9cc6cb56f7 fix error message 2016-06-01 19:38:42 -07:00
Girish Ramakrishnan 48b99a4203 enable developer mode by default
also emphasize on the api aspect
2016-06-01 18:57:15 -07:00
Girish Ramakrishnan 824767adbb enabled -> isEnabled 2016-06-01 18:21:02 -07:00
Girish Ramakrishnan 3d84880d92 keep limits in sync with the nginx config 2016-06-01 18:09:23 -07:00
Girish Ramakrishnan dfa08469d6 set timeouts explicitly 2016-06-01 17:33:28 -07:00
Girish Ramakrishnan d798073d95 fix comment of default_server 2016-06-01 17:28:15 -07:00
Girish Ramakrishnan 41632b8c11 fix favicon of naked domain 2016-06-01 17:27:39 -07:00
Girish Ramakrishnan 6ccc46717e remove unused middleware 2016-06-01 16:59:51 -07:00
Girish Ramakrishnan 2495caf2eb remove unused redis module 2016-06-01 16:55:58 -07:00
Girish Ramakrishnan ae9c104a8b remove unused bytes module 2016-06-01 16:54:25 -07:00
Girish Ramakrishnan 683f371778 remove serve-favicon
favicon is served up as /api/v1/cloudron/avatar
2016-06-01 16:52:28 -07:00
Girish Ramakrishnan eb29bdd575 document keepalive_timeout 2016-06-01 16:51:52 -07:00
Girish Ramakrishnan b13de298bf Add some REST api tests 2016-06-01 16:33:18 -07:00
Johannes Zellner 47978436c2 Set Destination header for webdav in nginx proxy 2016-06-01 18:49:50 +02:00
Johannes Zellner 71b5cc4702 Fix invite mail wording 2016-06-01 18:45:31 +02:00
Girish Ramakrishnan 5a9e32d41a hide aliases field if no username is set 2016-06-01 06:33:29 -07:00
Girish Ramakrishnan b03e4db8d5 check for null username 2016-05-31 21:38:51 -07:00
Girish Ramakrishnan 663ff2410a user.update must become a post route 2016-05-31 11:51:56 -07:00
Girish Ramakrishnan f763759008 return empty groupIds 2016-05-31 11:49:59 -07:00
Girish Ramakrishnan 69aa11d6c6 send pretty json 2016-05-31 11:14:59 -07:00
Girish Ramakrishnan 65041743c5 update to connect-lastmile@0.1.0 2016-05-31 10:48:37 -07:00
Girish Ramakrishnan 76214d3d7a no variable named infra_version 2016-05-30 19:45:59 -07:00
Girish Ramakrishnan be83a967fc node require will not work without json extension 2016-05-30 17:03:56 -07:00
Girish Ramakrishnan 119e095710 actually change ownership 2016-05-30 15:51:52 -07:00
Girish Ramakrishnan 5df3a41988 INFRA_VERSION may not exist 2016-05-30 14:48:41 -07:00
Girish Ramakrishnan a34b611e20 make INFRA_VERSION writable by yellowtent user 2016-05-30 12:52:39 -07:00
Girish Ramakrishnan 75c1731443 do not add app mailboxes to database
a) we don't allow .app pattern in database for aliases and mailboxes
b) the addons already know about app names separately
2016-05-30 01:38:43 -07:00
Girish Ramakrishnan 9e36b7abf4 load addon vars for existing infra case 2016-05-30 01:06:41 -07:00
Girish Ramakrishnan b37226d4d1 fix ui issues 2016-05-30 00:07:58 -07:00
Girish Ramakrishnan 311efe5d10 test: add test for getting aliases 2016-05-29 23:23:03 -07:00
Girish Ramakrishnan ebdd6d8a31 add missing require 2016-05-29 23:15:55 -07:00
Girish Ramakrishnan 3ee9f70113 return alias info in mailbox response 2016-05-29 23:14:24 -07:00
Girish Ramakrishnan adfc069e16 allow user alias to be set for custom domains 2016-05-29 23:02:01 -07:00
Girish Ramakrishnan 31fd0d711a add setAliases 2016-05-29 22:45:48 -07:00
Girish Ramakrishnan a6a852cfae remove bogus debug 2016-05-29 22:01:21 -07:00
Girish Ramakrishnan e9b3e22e86 check version of existing infra 2016-05-29 21:58:04 -07:00
Girish Ramakrishnan 564d61bcf5 fix typo 2016-05-29 21:31:49 -07:00
Girish Ramakrishnan 5582ac7402 add some debugs 2016-05-29 21:28:55 -07:00
Girish Ramakrishnan a05b6ad78d delete mailbox on user delete 2016-05-29 21:02:51 -07:00
Girish Ramakrishnan ec71390d0b autocreate mailbox when username is available 2016-05-29 19:14:01 -07:00
Girish Ramakrishnan 68a3862ee5 add create and remove mailbox 2016-05-29 18:56:40 -07:00
Girish Ramakrishnan a9f70d8363 add mailbox search endpoint 2016-05-29 18:24:54 -07:00
Girish Ramakrishnan e91539d79a add a todo 2016-05-29 18:08:16 -07:00
Girish Ramakrishnan 5546bfbf0e add mailbox ldap auth point 2016-05-29 17:25:23 -07:00
Girish Ramakrishnan 803d47b426 refactor authenticate path into a middleware 2016-05-29 17:16:52 -07:00
Girish Ramakrishnan e4c0192243 rename to appUserBind since it is tailored for apps 2016-05-29 17:07:48 -07:00
Girish Ramakrishnan d5b5289e0c Add mailbox importer for existing users and apps
this should prevent conflicts of mailboxes from the get-go.
2016-05-28 02:07:43 -07:00
Girish Ramakrishnan 2909aad72a use async.series 2016-05-28 01:59:48 -07:00
Girish Ramakrishnan cafbb31e78 push aliases to mail container on startup 2016-05-28 01:53:25 -07:00
Girish Ramakrishnan 080128539c set/unset aliases on the mail container 2016-05-28 01:33:20 -07:00
Girish Ramakrishnan cf93a99a4e add a note about mailboxes 2016-05-27 22:28:56 -07:00
Girish Ramakrishnan ce927bfa22 alias
also remove id since it's not useful for mailbox case (not like
mailbox can be renamed and we need a fixed it)
2016-05-27 22:20:08 -07:00
Girish Ramakrishnan 6993a9c7e7 add more mailbox route test 2016-05-27 18:23:14 -07:00
Girish Ramakrishnan 84d04cce16 initial mailboxes route 2016-05-27 18:17:57 -07:00
Girish Ramakrishnan f735fd8172 add mailboxes tests 2016-05-27 17:43:25 -07:00
Girish Ramakrishnan 53e28db1d6 add note on accessRestriction 2016-05-27 11:10:36 -07:00
Girish Ramakrishnan 77457d1ea9 initial mailbox db and model code 2016-05-27 10:36:47 -07:00
Girish Ramakrishnan 161b7cf76b add mailboxes table 2016-05-26 21:08:20 -07:00
Girish Ramakrishnan 01b6defd24 mount mail container /run into data 2016-05-26 15:18:10 -07:00
Girish Ramakrishnan badc524ff2 '-' has special meaning haraka
so do '.app' instead
2016-05-26 10:58:30 -07:00
Girish Ramakrishnan b3f53099f0 allow only alpha numerals in username 2016-05-25 21:36:20 -07:00
Girish Ramakrishnan a28560cdc0 0.14.2 changes 2016-05-24 20:17:20 -07:00
Girish Ramakrishnan 4afdf50736 finally finally finally the tests are working 2016-05-24 20:04:37 -07:00
Girish Ramakrishnan 078e36f07f turns out we cannot use sudo since it asks for password 2016-05-24 19:59:47 -07:00
Girish Ramakrishnan 3b8e15a61c check for node in path 2016-05-24 18:32:15 -07:00
Girish Ramakrishnan 67682c5d27 check for test image 2016-05-24 17:44:19 -07:00
Girish Ramakrishnan 48e3b8ebf9 provide a dummy config for tests 2016-05-24 17:36:54 -07:00
Girish Ramakrishnan 2072dedf66 bump mail addon version 2016-05-24 16:46:54 -07:00
Girish Ramakrishnan 51f43ecc27 use infra_version.js in checkInstall 2016-05-24 16:46:27 -07:00
Girish Ramakrishnan 2347a7ced2 admin email is a platform property 2016-05-24 16:36:56 -07:00
Girish Ramakrishnan b2cadaf95c load vars files after the platform is created 2016-05-24 16:28:59 -07:00
Girish Ramakrishnan 957f787701 setup mail addon root credentials 2016-05-24 16:18:21 -07:00
Girish Ramakrishnan ad48067bb2 setup_infra now uses infra_version.js
INFRA_VERSION is now removed. Note that DATA_DIR/INFRA_VERSION
still exists.
2016-05-24 16:16:03 -07:00
Girish Ramakrishnan 12b6c46558 use infra_version.js in splashpage 2016-05-24 13:31:45 -07:00
Girish Ramakrishnan b4ba17c599 use the constant 2016-05-24 13:23:41 -07:00
Girish Ramakrishnan 7fb28662c1 remove old images in platform.js 2016-05-24 13:23:41 -07:00
Girish Ramakrishnan 4845db538a use infra_version.js in platform.js 2016-05-24 13:23:41 -07:00
Girish Ramakrishnan 8429985253 use infra_version.js in addons.js 2016-05-24 13:23:41 -07:00
Girish Ramakrishnan aff9ff47bc use infra_version.js in baseimage script 2016-05-24 13:23:38 -07:00
Girish Ramakrishnan 9b3077eca3 add infra_version.js 2016-05-24 13:02:38 -07:00
Girish Ramakrishnan 39396cb3ab call callback if provided 2016-05-24 11:39:05 -07:00
Girish Ramakrishnan 364f0ead51 debug out the cmd 2016-05-24 11:26:11 -07:00
Girish Ramakrishnan 5ac1d5575c platform.js: minor refactor 2016-05-24 10:58:18 -07:00
Girish Ramakrishnan e5a030baff move platform cleanup bits to javascript 2016-05-24 10:52:55 -07:00
Girish Ramakrishnan a100837e69 Add helpers to restore/reconfigure all apps 2016-05-24 10:44:45 -07:00
Girish Ramakrishnan ffacf17a42 add paths.INFRA_VERSION_FILE 2016-05-24 10:26:08 -07:00
Girish Ramakrishnan f5d37b6443 add ini module 2016-05-24 10:25:33 -07:00
Girish Ramakrishnan d71d09c1ba Add shell.execSync 2016-05-24 10:22:39 -07:00
Girish Ramakrishnan c1a2444dfa move container creation to platform.js 2016-05-24 09:40:26 -07:00
Girish Ramakrishnan ef40aae3ba set adminEmail to no-reply@localhost for tests 2016-05-24 00:54:38 -07:00
Girish Ramakrishnan 9570086c87 add config.smtpPort 2016-05-24 00:53:42 -07:00
Girish Ramakrishnan 57a823a698 make tests work 2016-05-24 00:44:01 -07:00
Girish Ramakrishnan ec0ee07b17 test: email works now 2016-05-23 23:17:38 -07:00
Girish Ramakrishnan 3d7545133e wait 30 secs in the beginning 2016-05-23 23:16:02 -07:00
Girish Ramakrishnan bcc752469a remove containers after the test 2016-05-23 22:47:40 -07:00
Girish Ramakrishnan da85f4c096 stop ldap server in test 2016-05-23 21:59:06 -07:00
Girish Ramakrishnan 3b740a5651 make tests work 2016-05-23 20:41:00 -07:00
Girish Ramakrishnan 7eb202f19a test: use the test-app instead of duplicating the checks in the tests 2016-05-23 20:17:11 -07:00
Girish Ramakrishnan 8dbd4c8527 use 1024 bit keys
Stacked error: error:04075070:rsa routines:RSA_sign:digest too big for rsa key
2016-05-23 19:31:57 -07:00
Girish Ramakrishnan 88f2ce554d bump to mail 0.13.1 with the auth fix 2016-05-23 18:57:59 -07:00
Girish Ramakrishnan 57888659a6 Update test app version 2016-05-23 18:31:12 -07:00
Girish Ramakrishnan ebdefa7f18 test: oauth addon 2016-05-23 17:34:25 -07:00
Girish Ramakrishnan 569150f602 tests: create a self-signed cert 2016-05-23 16:40:18 -07:00
Girish Ramakrishnan 6ccb806628 make apps-test pass 2016-05-23 16:31:02 -07:00
Girish Ramakrishnan ae807b28b6 test: let server start the infra
otherwise, deps like dkim keys need to be setup in tests as well
2016-05-23 15:53:51 -07:00
Girish Ramakrishnan 00726b01e2 pass -no-run-if-empty instead 2016-05-23 15:50:36 -07:00
Girish Ramakrishnan f5b777ab33 add route tests for username 2016-05-23 15:00:21 -07:00
Girish Ramakrishnan d84e584222 add some username tests 2016-05-23 14:56:09 -07:00
Girish Ramakrishnan 31e452e1cc test: mixed case reserved name 2016-05-23 14:52:29 -07:00
Girish Ramakrishnan e015b9bd7a 0.14.1 changes 2016-05-23 12:29:49 -07:00
Girish Ramakrishnan 10e0cbcebc do not set allowHalfOpen (otherwise we have to end socket ourself) 2016-05-23 10:50:04 -07:00
Girish Ramakrishnan 2768c3a336 acme: configure prod based on caas or acme 2016-05-23 09:48:17 -07:00
Girish Ramakrishnan 37512c4cac Wrap the stdin stream to indicate EOF
The docker exec protocol supports half-closing to signal that the stdin
is finished. The CLI tool tried to do this by closing it's half of the
socket when the stdin finished. Unfortunately, this does not work because
nginx immediately terminates a half-close :/ Node itself has no problem.

http://mailman.nginx.org/pipermail/nginx/2008-September/007388.html
seems to support the hypothesis. Basically, for HTTP and websockets
there is no notion of half-close.

Websocket protocol itself has no half-close as well:
http://www.lenholgate.com/blog/2011/07/websockets---i-miss-the-tcp-half-close.html
http://doc.akka.io/docs/akka/2.4.5/scala/http/client-side/websocket-support.html

The fix is to implement our own protocol that wrap stdin. We put a length
header for every payload. When we hit EOF, the length is set to 0. The server
sees this 0 length header and closes the exec container socket.
2016-05-22 22:27:49 -07:00
Girish Ramakrishnan 0aaaa866e4 Add a whole bunch of magic for docker.exec to work 2016-05-22 00:27:32 -07:00
Girish Ramakrishnan 53cb7fe687 debug out cmd 2016-05-19 15:54:35 -07:00
Girish Ramakrishnan da42f2f00c fix boolean logic 2016-05-19 15:54:35 -07:00
Girish Ramakrishnan 27d2daae93 leave a note in nginx config 2016-05-19 12:27:54 -07:00
Girish Ramakrishnan 42cc8249f8 reserve usernames with -app in them 2016-05-18 21:45:02 -07:00
Girish Ramakrishnan de055492ef set username restriction to 2 chars 2016-05-18 11:05:45 -07:00
Girish Ramakrishnan efa3ccaffe fix crash because of missing error handling 2016-05-18 10:00:32 -07:00
Girish Ramakrishnan 32e238818a make script more robust 2016-05-17 18:16:18 -07:00
Girish Ramakrishnan 2c1083d58b log error if the curl parsing fails 2016-05-17 17:15:10 -07:00
Girish Ramakrishnan 517b967fe9 enable sieve 2016-05-17 14:04:57 -07:00
Girish Ramakrishnan 3c4ca8e9c8 reserve more usernames 2016-05-17 12:47:10 -07:00
Girish Ramakrishnan 4ec043836b 0.14.0 changes 2016-05-17 09:36:28 -07:00
Girish Ramakrishnan 7ec93b733b setup restore paths for recvmail and email addon 2016-05-17 09:27:59 -07:00
Girish Ramakrishnan a81262afb5 remove unused var 2016-05-17 08:47:57 -07:00
Girish Ramakrishnan 266603bb19 Do not barf on rmi 2016-05-16 16:56:17 -07:00
Girish Ramakrishnan 6dcecaaf55 log the ldap source 2016-05-16 14:31:57 -07:00
Girish Ramakrishnan 099eb2bca4 use port 2525 2016-05-16 12:52:36 -07:00
Girish Ramakrishnan b92ed8d079 cn can also be the cloudron email
cn can be:
1. username
2. username@fqdn
3. user@personalemail.com
2016-05-16 12:21:15 -07:00
Girish Ramakrishnan 0838ce4ef8 fix casing 2016-05-16 12:13:23 -07:00
Girish Ramakrishnan cc8767274a Bind 587 with 2525 as well
MSA (587) and MTA (25) are the same protocol. Only difference is
that 587 does relaying with AUTH. Haraka has been configured to
make this distinction.
2016-05-16 08:32:30 -07:00
Girish Ramakrishnan dfaed79e31 fix mail container name 2016-05-16 08:18:33 -07:00
Girish Ramakrishnan 9dc1a95992 SENDIMAGE -> IMAGE 2016-05-15 21:51:04 -07:00
Girish Ramakrishnan 5be05529c2 remove unused ldap ou 2016-05-15 21:25:56 -07:00
Girish Ramakrishnan 6ff7786f04 use the send addon service api 2016-05-15 21:23:44 -07:00
Girish Ramakrishnan a833b65ef3 add mail container link once 2016-05-15 21:18:43 -07:00
Girish Ramakrishnan 83a252bd20 there is only mail container now 2016-05-15 21:15:53 -07:00
Girish Ramakrishnan 7ef3805dbc docker data in test dir requires sudo 2016-05-13 22:38:36 -07:00
Girish Ramakrishnan e5d906a065 create fake cert/key for tests to pass 2016-05-13 22:35:13 -07:00
Girish Ramakrishnan b5e4e9fed6 bump postgresql 2016-05-13 22:13:58 -07:00
Girish Ramakrishnan 3ccb72f891 tests finally work 2016-05-13 22:05:05 -07:00
Girish Ramakrishnan 45cd4ba349 fix name 2016-05-13 22:03:40 -07:00
Girish Ramakrishnan 5b9b21c469 create recvmail link 2016-05-13 22:03:34 -07:00
Girish Ramakrishnan ed55ad1c6f change image name 2016-05-13 21:34:04 -07:00
Girish Ramakrishnan 560f460a32 rename to sendmail 2016-05-13 20:48:31 -07:00
Girish Ramakrishnan aa116ce58c fix tests 2016-05-13 20:21:09 -07:00
Girish Ramakrishnan 3f0e2024e4 pass db name and password for tests 2016-05-13 19:35:20 -07:00
Girish Ramakrishnan d9c5b2b642 setup and teardown recvmail addon 2016-05-13 18:58:48 -07:00
Girish Ramakrishnan 5322ed054d reserve 4190 for sieve 2016-05-13 18:48:05 -07:00
Girish Ramakrishnan 39c4954371 remove isIncomingMailEnabled. always enable for now
also, custom domain === we will take over domain completely (setup
mx and all)
2016-05-13 18:44:08 -07:00
Girish Ramakrishnan 78ad49bd74 hack for settings test 2016-05-13 18:27:00 -07:00
Girish Ramakrishnan f56c960b92 use latest manifestformat (for recvmail, email) 2016-05-13 17:59:11 -07:00
Girish Ramakrishnan 8e077660c4 start_addons is redundant 2016-05-13 17:57:56 -07:00
Girish Ramakrishnan 1b8b4900a2 parametrize data_dir for tests 2016-05-13 17:57:56 -07:00
Girish Ramakrishnan 27ddcb9758 use the internal hostnames for email addon
The public ones are for the user to configure their MUA. This
things can have ssl disabled safely as well.
2016-05-13 08:28:13 -07:00
Girish Ramakrishnan fdb951c9e5 set MAIL_DOMAIN in email addon as well 2016-05-12 23:34:35 -07:00
Girish Ramakrishnan 0f2037513b remove recvmail bind 2016-05-12 21:48:42 -07:00
Girish Ramakrishnan 9da4e038bd all lower case 2016-05-12 18:54:13 -07:00
Girish Ramakrishnan ae3e0177bb make script more robust 2016-05-12 18:16:26 -07:00
Girish Ramakrishnan 0751974624 Add a hack to test addon patching 2016-05-12 16:41:58 -07:00
Girish Ramakrishnan b8242c82d6 create bind point for recvmail 2016-05-12 14:33:02 -07:00
Girish Ramakrishnan 442c02fa1b set mailAlternateAddress to username@fqdn
This is mostly to keep haraka's rcpt_to.ldap happy. That plugin
could do with some love.
2016-05-12 14:32:15 -07:00
Girish Ramakrishnan d5306052bb refactor code for readability 2016-05-12 13:36:53 -07:00
Girish Ramakrishnan 8543dbe3be create a new ou for addons 2016-05-12 13:20:57 -07:00
Girish Ramakrishnan bf42b735d1 fix the smtp port 2016-05-12 09:11:29 -07:00
Girish Ramakrishnan a2ba3989d0 use manifestformat 2.4.0 2016-05-12 08:57:12 -07:00
Girish Ramakrishnan 6f36d79358 implement email addon 2016-05-12 08:54:59 -07:00
Girish Ramakrishnan 1da24564b3 reserve postman subdomain 2016-05-11 15:04:22 -07:00
Girish Ramakrishnan da61d5c0f1 add ou=recvmail for dovecot 2016-05-11 14:26:34 -07:00
Girish Ramakrishnan 79da7b31c7 use latest base image 2016-05-11 13:14:11 -07:00
Girish Ramakrishnan 631b238b63 use MAIL_LOCATION for mx record 2016-05-11 09:59:12 -07:00
Girish Ramakrishnan ff5ca617b1 use ADMIN_LOCATION 2016-05-11 09:58:31 -07:00
Girish Ramakrishnan e16125c67e set MAIL_DOMAIN and MAIL_SERVER_NAME 2016-05-11 09:49:20 -07:00
Girish Ramakrishnan 646ba096c3 start recvmail addon in setup_infra 2016-05-11 08:55:51 -07:00
Girish Ramakrishnan 8be3b4c281 add mx records 2016-05-11 08:50:33 -07:00
Girish Ramakrishnan 5afff5eecc open 25 (inbound smtp) and 587 (inbound submission) 2016-05-11 08:48:50 -07:00
Girish Ramakrishnan 84206738e1 open port 993 (imap) 2016-05-11 08:47:59 -07:00
166 changed files with 12308 additions and 5641 deletions
+6 -5
View File
@@ -1,7 +1,8 @@
{
"node": true,
"browser": true,
"unused": true,
"globalstrict": true,
"predef": [ "angular", "$" ]
"node": true,
"browser": true,
"unused": true,
"globalstrict": true,
"predef": [ "angular", "$" ],
"esnext": true
}
+78
View File
@@ -510,3 +510,81 @@
[0.13.4]
- Fix mail addon restart issue
[0.14.0]
- You have mail :-)
[0.14.1]
- 2-character usernames are now allowed
- Make cloudron CLI push/pull more robust
[0.14.2]
- Update mail addon
[0.15.0]
- [REST API](https://cloudron.io/references/api.html) is now in public beta
- Enable Developer mode by default for new Cloudrons
- Reverse proxy fixes for apps exposing a WebDav server
- Allow admins to optionally set the username and displayName on user creation
- Fix app autoupdate logic to detect if one or more in-use port bindings was removed
[0.15.1]
- Fix mail connectivity from IPv6 clients
- Add API token management UI
- Improved UI to enter email aliases
[0.15.2]
- Allow restoring apps from any previous backup
[0.15.3]
- Show installation progress in a tooltip
[0.16.0]
- Allow apps to be configured in configuring state
- Improved platform architecture that allows incremental infrastructure updates
- Implement app clone
[0.16.1]
- Fix UI layout issue in tokens page
- Resume app tasks only when configured and platform ready
- Allow errored apps to be reconfigured
[0.16.2]
- Fix assert when backing up apps in errored state
- Fix bug where multiple redis installations caused an error
[0.16.3]
- Timeout in 10mins if app restore fails because of external domain CNAME setup
[0.16.4]
- Setup email aliases to only alias names for the Cloudron domain
[0.16.5]
- Allow sending email with alias as the From
[0.16.6]
- Add plan migration interface
- Initial EC2 support
[0.17.0]
- Public beta release of Cloudron Mail Server
- Add new DNS & Certs UI that enables easy migration to a custom domain
- Allow sending and receiving email from alias subaddresses
- Fix installation issue with some apps on the naked domain
[0.17.1]
- Preliminary user impersonation support
- Fix crash in mail container when generating bounces
[0.17.2]
- Add config option to embed apps in other sites
[0.17.3]
- Incremental infrastructure update logic
- Keep eventlogs only for a week
- Throttle OOM mails
[0.17.4]
- Add warning for users moving to custom domains
- Out of disk space and certificate renewal mails are now sent to cloudron owner for selfhosters
- Fix a bug where selfhosted Cloudrons do not start because of a MySQL error
- Implement new app versioning & update scheme
@@ -10,7 +10,6 @@ readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd)"
export JSON="${SOURCE_DIR}/node_modules/.bin/json"
provider="digitalocean"
installer_revision=$(git rev-parse HEAD)
box_name=""
server_id=""
@@ -23,14 +22,13 @@ deploy_env="dev"
[[ $(uname -s) == "Darwin" ]] && GNU_GETOPT="/usr/local/opt/gnu-getopt/bin/getopt" || GNU_GETOPT="getopt"
readonly GNU_GETOPT
args=$(${GNU_GETOPT} -o "" -l "provider:,revision:,regions:,size:,name:,no-destroy,env:" -n "$0" -- "$@")
args=$(${GNU_GETOPT} -o "" -l "revision:,regions:,size:,name:,no-destroy,env:" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--env) deploy_env="$2"; shift 2;;
--revision) installer_revision="$2"; shift 2;;
--provider) provider="$2"; shift 2;;
--name) box_name="$2"; destroy_server="no"; shift 2;;
--no-destroy) destroy_server="no"; shift 2;;
--) break;;
@@ -38,28 +36,23 @@ while true; do
esac
done
echo "Creating image using ${provider}"
if [[ "${provider}" == "digitalocean" ]]; then
if [[ "${deploy_env}" == "staging" ]]; then
assertNotEmpty DIGITAL_OCEAN_TOKEN_STAGING
export DIGITAL_OCEAN_TOKEN="${DIGITAL_OCEAN_TOKEN_STAGING}"
elif [[ "${deploy_env}" == "dev" ]]; then
assertNotEmpty DIGITAL_OCEAN_TOKEN_DEV
export DIGITAL_OCEAN_TOKEN="${DIGITAL_OCEAN_TOKEN_DEV}"
elif [[ "${deploy_env}" == "prod" ]]; then
assertNotEmpty DIGITAL_OCEAN_TOKEN_PROD
export DIGITAL_OCEAN_TOKEN="${DIGITAL_OCEAN_TOKEN_PROD}"
else
echo "No such env ${deploy_env}."
exit 1
fi
vps="/bin/bash ${SCRIPT_DIR}/digitalocean.sh"
echo "Creating digitalocean image"
if [[ "${deploy_env}" == "staging" ]]; then
assertNotEmpty DIGITAL_OCEAN_TOKEN_STAGING
export DIGITAL_OCEAN_TOKEN="${DIGITAL_OCEAN_TOKEN_STAGING}"
elif [[ "${deploy_env}" == "dev" ]]; then
assertNotEmpty DIGITAL_OCEAN_TOKEN_DEV
export DIGITAL_OCEAN_TOKEN="${DIGITAL_OCEAN_TOKEN_DEV}"
elif [[ "${deploy_env}" == "prod" ]]; then
assertNotEmpty DIGITAL_OCEAN_TOKEN_PROD
export DIGITAL_OCEAN_TOKEN="${DIGITAL_OCEAN_TOKEN_PROD}"
else
echo "Unknown provider : ${provider}"
echo "No such env ${deploy_env}."
exit 1
fi
vps="/bin/bash ${SCRIPT_DIR}/digitalocean.sh"
readonly ssh_keys="${HOME}/.ssh/id_rsa_caas_${deploy_env}"
readonly scp202="scp -P 202 -o ConnectTimeout=10 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${ssh_keys}"
readonly scp22="scp -o ConnectTimeout=10 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${ssh_keys}"
@@ -137,8 +130,8 @@ while true; do
sleep 30
done
echo "Copying INFRA_VERSION"
$scp22 "${SCRIPT_DIR}/../src/INFRA_VERSION" root@${server_ip}:.
echo "Copying infra_version.js"
$scp22 "${SCRIPT_DIR}/../src/infra_version.js" root@${server_ip}:.
echo "Copying box source"
cd "${SOURCE_DIR}"
+185
View File
@@ -0,0 +1,185 @@
#!/bin/bash
set -eu -o pipefail
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd)"
export JSON="${SOURCE_DIR}/node_modules/.bin/json"
installer_revision=$(git rev-parse HEAD)
instance_id=""
server_ip=""
destroy_server="yes"
ami_id="ami-f9e30f96"
region="eu-central-1"
aws_credentials="baseimage"
security_group="sg-b9a473d1"
instance_type="t2.small"
subnet_id="subnet-801402e9"
key_pair_name="id_rsa_yellowtent"
# Only GNU getopt supports long options. OS X comes bundled with the BSD getopt
# brew install gnu-getopt to get the GNU getopt on OS X
[[ $(uname -s) == "Darwin" ]] && GNU_GETOPT="/usr/local/opt/gnu-getopt/bin/getopt" || GNU_GETOPT="getopt"
readonly GNU_GETOPT
args=$(${GNU_GETOPT} -o "" -l "revisio0n:,no-destroy" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--revision) installer_revision="$2"; shift 2;;
--no-destroy) destroy_server="no"; shift 2;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
done
readonly ssh_keys="${HOME}/.ssh/id_rsa_yellowtent"
readonly scp202="scp -P 202 -o ConnectTimeout=10 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${ssh_keys}"
readonly scp22="scp -o ConnectTimeout=10 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${ssh_keys}"
readonly ssh202="ssh -p 202 -o IdentitiesOnly=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${ssh_keys}"
readonly ssh22="ssh -o IdentitiesOnly=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${ssh_keys}"
if [[ ! -f "${ssh_keys}" ]]; then
echo "caas ssh key is missing at ${ssh_keys} (pick it up from secrets repo)"
exit 1
fi
function debug() {
echo "$@" >&2
}
function get_pretty_revision() {
local git_rev="$1"
local sha1=$(git rev-parse --short "${git_rev}" 2>/dev/null)
echo "${sha1}"
}
now=$(date "+%Y-%m-%d-%H%M%S")
pretty_revision=$(get_pretty_revision "${installer_revision}")
echo "Creating EC2 instance"
instance_id=$(aws ec2 run-instances --image-id ${ami_id} --region ${region} --profile ${aws_credentials} --security-group-ids ${security_group} --instance-type ${instance_type} --key-name ${key_pair_name} --subnet-id ${subnet_id} --associate-public-ip-address | $JSON Instances[0].InstanceId)
echo "Got InstanceId: ${instance_id}"
# name the instance
aws ec2 create-tags --profile ${aws_credentials} --resources ${instance_id} --tags "Key=Name,Value=baseimage-${pretty_revision}"
echo "Waiting for instance to be running..."
while true; do
event_status=`aws ec2 describe-instances --instance-id ${instance_id} --region ${region} --profile ${aws_credentials} | $JSON Reservations[0].Instances[0].State.Name`
if [[ "${event_status}" == "running" ]]; then
break
fi
debug -n "."
sleep 10
done
server_ip=$(aws ec2 describe-instances --instance-id ${instance_id} --region ${region} --profile ${aws_credentials} | $JSON Reservations[0].Instances[0].PublicIpAddress)
echo "Server IP is: ${server_ip}"
while true; do
echo "Trying to copy init script to server"
if $scp22 "${SCRIPT_DIR}/initializeBaseUbuntuImage.sh" ubuntu@${server_ip}:.; then
break
fi
echo "Timedout, trying again in 30 seconds"
sleep 30
done
echo "Copying infra_version.js"
$scp22 "${SCRIPT_DIR}/../src/infra_version.js" ubuntu@${server_ip}:.
echo "Copying box source"
cd "${SOURCE_DIR}"
git archive --format=tar HEAD | $ssh22 "ubuntu@${server_ip}" "cat - > /tmp/box.tar.gz"
echo "Enabling root ssh access"
if ! $ssh22 "ubuntu@${server_ip}" "sudo sed -e 's/.* \(ssh-rsa.*\)/\1/' -i /root/.ssh/authorized_keys"; then
echo "Unable to enable root access"
echo "Make sure to cleanup the ec2 instance ${instance_id}"
exit 1
fi
echo "Executing init script"
if ! $ssh22 "root@${server_ip}" "/bin/bash /home/ubuntu/initializeBaseUbuntuImage.sh ${installer_revision}"; then
echo "Init script failed"
echo "Make sure to cleanup the ec2 instance ${instance_id}"
exit 1
fi
snapshot_name="cloudron-${pretty_revision}-${now}"
echo "Creating ami image ${snapshot_name}"
image_id=$(aws ec2 create-image --region ${region} --profile ${aws_credentials} --instance-id ${instance_id} --name ${snapshot_name} | $JSON ImageId)
echo "Image creation started for image id: ${image_id}"
echo "Waiting for image creation to finish..."
while true; do
event_status=`aws ec2 describe-images --region ${region} --profile ${aws_credentials} --image-id ${image_id} | $JSON Images[0].State`
if [[ "${event_status}" == "available" ]]; then
break
fi
debug -n "."
sleep 10
done
echo "Terminating instance"
aws ec2 terminate-instances --region ${region} --profile ${aws_credentials} --instance-ids ${instance_id}
echo "Make image public"
aws ec2 modify-image-attribute --region ${region} --profile ${aws_credentials} --image-id ${image_id} --launch-permission "{\"Add\":[{\"Group\":\"all\"}]}"
# http://docs.aws.amazon.com/general/latest/gr/rande.html#ec2_region
# Images are currently created in eu-central-1
echo "Coping image to other regions"
ec2_regions=( "us-east-1" "us-west-1" "us-west-2" "ap-south-1" "ap-northeast-2" "ap-southeast-1" "ap-southeast-2" "ap-northeast-1" "eu-west-1" "sa-east-1" )
ec2_amis=( )
for r in ${ec2_regions[@]}; do
echo "=> ${r}"
ami_id=$(aws ec2 copy-image --region ${r} --profile ${aws_credentials} --source-image-id ${image_id} --source-region ${region} --name ${snapshot_name} | $JSON ImageId)
# append in the same order as the regions
ec2_amis+=( ${ami_id} )
done
# wait for all images to be available
echo "Waiting for images to be ready (first will take the longest)..."
region_string="${region}=${image_id}"
i=0
while [ $i -lt ${#ec2_regions[*]} ]; do
echo "=> ${ec2_regions[$i]} ${ec2_amis[$i]}"
while true; do
event_status=`aws ec2 describe-images --region ${ec2_regions[$i]} --profile ${aws_credentials} --image-id ${ec2_amis[$i]} | $JSON Images[0].State`
if [[ "${event_status}" == "available" ]]; then
echo "done"
break
fi
debug -n "."
sleep 10
done
# now make it public
aws ec2 modify-image-attribute --region ${ec2_regions[$i]} --profile ${aws_credentials} --image-id ${ec2_amis[$i]} --launch-permission "{\"Add\":[{\"Group\":\"all\"}]}"
# append to output string for release tool
region_string+=",${ec2_regions[$i]}=${ec2_amis[$i]}"
# inc the iteration counter
i=$(( $i + 1));
done
echo ""
echo "--------------------------------------------------"
echo "New image id is: ${image_id}"
echo "Image region string for release:"
echo "${region_string}"
echo "--------------------------------------------------"
echo ""
+34 -13
View File
@@ -10,7 +10,7 @@ if [[ -z "${JSON}" ]]; then
exit 1
fi
readonly CURL="curl -s -u ${DIGITAL_OCEAN_TOKEN}:"
readonly CURL="curl --retry 5 -s -u ${DIGITAL_OCEAN_TOKEN}:"
function debug() {
echo "$@" >&2
@@ -49,9 +49,9 @@ function get_droplet_ip() {
function get_droplet_id() {
local droplet_name="$1"
id=$($CURL "https://api.digitalocean.com/v2/droplets?per_page=100" | $JSON "droplets" | $JSON -c "this.name === '${droplet_name}'" | $JSON "[0].id")
id=$($CURL "https://api.digitalocean.com/v2/droplets?per_page=200" | $JSON "droplets" | $JSON -c "this.name === '${droplet_name}'" | $JSON "[0].id")
[[ -z "$id" ]] && exit 1
echo "$id"
echo "$id"
}
function power_off_droplet() {
@@ -109,13 +109,24 @@ function get_image_id() {
local snapshot_name="$1"
local image_id=""
image_id=$($CURL "https://api.digitalocean.com/v2/images?per_page=100" \
| $JSON images \
| $JSON -c "this.name === \"${snapshot_name}\"" 0.id)
if [[ -n "${image_id}" ]]; then
echo "${image_id}"
if ! response=$($CURL "https://api.digitalocean.com/v2/images?per_page=200"); then
echo "Failed to get image listing. ${response}"
return 1
fi
if ! image_id=$(echo "$response" \
| $JSON images \
| $JSON -c "this.name === \"${snapshot_name}\"" 0.id); then
echo "Failed to parse curl response: ${response}"
return 1
fi
if [[ -z "${image_id}" ]]; then
echo "Failed to get image id of ${snapshot_name}. reponse: ${response}"
return 1
fi
echo "${image_id}"
}
function snapshot_droplet() {
@@ -128,16 +139,26 @@ function snapshot_droplet() {
debug -n "Waiting for snapshot to complete"
while true; do
local event_status=`$CURL "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions/${event_id}" | $JSON action.status`
if ! response=$($CURL "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions/${event_id}"); then
echo "Could not get action status. ${response}"
continue
fi
if ! event_status=$(echo "${response}" | $JSON action.status); then
echo "Could not parse action.status from response. ${response}"
continue
fi
if [[ "${event_status}" == "completed" ]]; then
break
fi
debug -n "."
sleep 10
done
debug ""
debug "! done"
get_image_id "${snapshot_name}"
if ! image_id=$(get_image_id "${snapshot_name}"); then
return 1
fi
echo "${image_id}"
}
function destroy_droplet() {
@@ -177,7 +198,7 @@ function transfer_image_to_all_regions() {
local image_id="$1"
xfer_events=()
image_regions=(ams3) ## sfo1 is where the image is created
image_regions=(ams2) ## sfo1 is where the image is created
for image_region in ${image_regions[@]}; do
xfer_event=$(transfer_image ${image_id} ${image_region})
echo "Image transfer to ${image_region} initiated. Event id: ${xfer_event}"
+29 -69
View File
@@ -6,7 +6,6 @@ readonly USER=yellowtent
readonly USER_HOME="/home/${USER}"
readonly INSTALLER_SOURCE_DIR="${USER_HOME}/installer"
readonly INSTALLER_REVISION="$1"
readonly SELFHOSTED=$(( $# > 1 ? 1 : 0 ))
readonly USER_DATA_FILE="/root/user_data.img"
readonly USER_DATA_DIR="/home/yellowtent/data"
@@ -19,18 +18,6 @@ function die {
[[ "$(systemd --version 2>&1)" == *"systemd 229"* ]] || die "Expecting systemd to be 229"
if [ -f "${SOURCE_DIR}/INFRA_VERSION" ]; then
source "${SOURCE_DIR}/INFRA_VERSION"
else
echo "No INFRA_VERSION found, skip pulling docker images"
fi
if [ ${SELFHOSTED} == 0 ]; then
echo "!! Initializing Ubuntu image for CaaS"
else
echo "!! Initializing Ubuntu image for Selfhosting"
fi
echo "==== Create User ${USER} ===="
if ! id "${USER}"; then
useradd "${USER}" -m
@@ -66,15 +53,11 @@ iptables -P OUTPUT ACCEPT
# NOTE: keep these in sync with src/apps.js validatePortBindings
# allow ssh, http, https, ping, dns
iptables -I INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
if [ ${SELFHOSTED} == 0 ]; then
iptables -A INPUT -p tcp -m tcp -m multiport --dports 80,202,443,886 -j ACCEPT
else
iptables -A INPUT -p tcp -m tcp -m multiport --dports 80,22,443,886 -j ACCEPT
fi
iptables -A INPUT -p tcp -m tcp -m multiport --dports 25,80,202,443,587,993,4190 -j ACCEPT
iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT
iptables -A INPUT -p icmp --icmp-type echo-reply -j ACCEPT
iptables -A INPUT -p udp --sport 53 -j ACCEPT
iptables -A INPUT -s 172.17.0.0/16 -j ACCEPT # required to accept any connections from apps to our IP:<public port>
iptables -A INPUT -s 172.18.0.0/16 -j ACCEPT # required to accept any connections from apps to our IP:<public port>
# loopback
iptables -A INPUT -i lo -j ACCEPT
@@ -156,30 +139,22 @@ update-grub
# now add the user to the docker group
usermod "${USER}" -a -G docker
if [ -z $(echo "${INFRA_VERSION}") ]; then
echo "Skip pulling base docker images"
else
echo "=== Pulling base docker images ==="
docker pull "${BASE_IMAGE}"
echo "==== Install nodejs ===="
# Cannot use anything above 4.1.1 - https://github.com/nodejs/node/issues/3803
mkdir -p /usr/local/node-4.1.1
curl -sL https://nodejs.org/dist/v4.1.1/node-v4.1.1-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-4.1.1
ln -s /usr/local/node-4.1.1/bin/node /usr/bin/node
ln -s /usr/local/node-4.1.1/bin/npm /usr/bin/npm
apt-get install -y python # Install python which is required for npm rebuild
[[ "$(python --version 2>&1)" == "Python 2.7."* ]] || die "Expecting python version to be 2.7.x"
echo "=== Pulling mysql addon image ==="
docker pull "${MYSQL_IMAGE}"
echo "==== Downloading docker images ===="
images=$(node -e "var i = require('${SOURCE_DIR}/infra_version.js'); console.log(i.baseImage, Object.keys(i.images).map(function (x) { return i.images[x].tag; }).join(' '));")
echo "=== Pulling postgresql addon image ==="
docker pull "${POSTGRESQL_IMAGE}"
echo "=== Pulling redis addon image ==="
docker pull "${REDIS_IMAGE}"
echo "=== Pulling mongodb addon image ==="
docker pull "${MONGODB_IMAGE}"
echo "=== Pulling graphite docker images ==="
docker pull "${GRAPHITE_IMAGE}"
echo "=== Pulling mail relay ==="
docker pull "${MAIL_IMAGE}"
fi
echo "Pulling images: ${images}"
for image in ${images}; do
docker pull "${image}"
done
echo "==== Install nginx ===="
apt-get -y install nginx-full
@@ -210,25 +185,11 @@ echo "==== Install logrotate ==="
apt-get install -y cron logrotate
systemctl enable cron
echo "==== Install nodejs ===="
# Cannot use anything above 4.1.1 - https://github.com/nodejs/node/issues/3803
mkdir -p /usr/local/node-4.1.1
curl -sL https://nodejs.org/dist/v4.1.1/node-v4.1.1-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-4.1.1
ln -s /usr/local/node-4.1.1/bin/node /usr/bin/node
ln -s /usr/local/node-4.1.1/bin/npm /usr/bin/npm
apt-get install -y python # Install python which is required for npm rebuild
[[ "$(python --version 2>&1)" == "Python 2.7."* ]] || die "Expecting python version to be 2.7.x"
echo "=== Rebuilding npm packages ==="
cd "${INSTALLER_SOURCE_DIR}" && npm install --production
chown "${USER}:${USER}" -R "${INSTALLER_SOURCE_DIR}"
echo "==== Install installer systemd script ===="
provisionEnv="PROVISION=digitalocean"
if [ ${SELFHOSTED} == 1 ]; then
provisionEnv="PROVISION=local"
fi
cat > /etc/systemd/system/cloudron-installer.service <<EOF
[Unit]
Description=Cloudron Installer
@@ -238,7 +199,7 @@ BindsTo=systemd-journald.service
[Service]
Type=idle
ExecStart="${INSTALLER_SOURCE_DIR}/src/server.js"
Environment="DEBUG=installer*,connect-lastmile" ${provisionEnv}
Environment="DEBUG=installer*,connect-lastmile"
; kill any child (installer.sh) as well
KillMode=control-group
Restart=on-failure
@@ -265,12 +226,13 @@ EOF
# Allocate swap files
# https://bbs.archlinux.org/viewtopic.php?id=194792 ensures this runs after do-resize.service
# On ubuntu ec2 we use cloud-init https://wiki.archlinux.org/index.php/Cloud-init
echo "==== Install box-setup systemd script ===="
cat > /etc/systemd/system/box-setup.service <<EOF
[Unit]
Description=Box Setup
Before=docker.service collectd.service mysql.service
After=do-resize.service
After=do-resize.service cloud-init.service
[Service]
Type=oneshot
@@ -311,16 +273,14 @@ chown root:systemd-journal /var/log/journal
systemctl restart systemd-journald
setfacl -n -m u:${USER}:r /var/log/journal/*/system.journal
if [ ${SELFHOSTED} == 0 ]; then
echo "==== Install ssh ==="
apt-get -y install openssh-server
# https://stackoverflow.com/questions/4348166/using-with-sed on why ? must be escaped
sed -e 's/^#\?Port .*/Port 202/g' \
-e 's/^#\?PermitRootLogin .*/PermitRootLogin without-password/g' \
-e 's/^#\?PermitEmptyPasswords .*/PermitEmptyPasswords no/g' \
-e 's/^#\?PasswordAuthentication .*/PasswordAuthentication no/g' \
-i /etc/ssh/sshd_config
echo "==== Install ssh ==="
apt-get -y install openssh-server
# https://stackoverflow.com/questions/4348166/using-with-sed on why ? must be escaped
sed -e 's/^#\?Port .*/Port 202/g' \
-e 's/^#\?PermitRootLogin .*/PermitRootLogin without-password/g' \
-e 's/^#\?PermitEmptyPasswords .*/PermitEmptyPasswords no/g' \
-e 's/^#\?PasswordAuthentication .*/PasswordAuthentication no/g' \
-i /etc/ssh/sshd_config
# required so we can connect to this machine since port 22 is blocked by iptables by now
systemctl reload sshd
fi
# required so we can connect to this machine since port 22 is blocked by iptables by now
systemctl reload sshd
-164
View File
@@ -1,164 +0,0 @@
#!/bin/bash
set -eu -o pipefail
echo ""
echo "======== Cloudron Installer ========"
echo ""
if [ $# -lt 4 ]; then
echo "Usage: ./installer.sh <fqdn> <aws key id> <aws key secret> <bucket> <provider> <revision>"
exit 1
fi
# commandline arguments
readonly fqdn="${1}"
readonly aws_access_key_id="${2}"
readonly aws_access_key_secret="${3}"
readonly aws_backup_bucket="${4}"
readonly provider="${5}"
readonly revision="${6}"
# environment specific urls
<% if (env === 'prod') { %>
readonly api_server_origin="https://api.cloudron.io"
readonly web_server_origin="https://cloudron.io"
<% } else { %>
readonly api_server_origin="https://api.<%= env %>.cloudron.io"
readonly web_server_origin="https://<%= env %>.cloudron.io"
<% } %>
readonly release_bucket_url="https://s3.amazonaws.com/<%= env %>-cloudron-releases"
readonly versions_url="https://s3.amazonaws.com/<%= env %>-cloudron-releases/versions.json"
readonly installer_code_url="${release_bucket_url}/box-${revision}.tar.gz"
# runtime consts
readonly installer_code_file="/tmp/box.tar.gz"
readonly installer_tmp_dir="/tmp/box"
readonly cert_folder="/tmp/certificates"
# check for fqdn in /ets/hosts
echo "[INFO] checking for hostname entry"
readonly hostentry_found=$(grep "${fqdn}" /etc/hosts || true)
if [[ -z $hostentry_found ]]; then
echo "[WARNING] No entry for ${fqdn} found in /etc/hosts"
echo "Adding an entry ..."
cat >> /etc/hosts <<EOF
# The following line was added by the Cloudron installer script
127.0.1.1 ${fqdn} ${fqdn}
EOF
else
echo "Valid hostname entry found in /etc/hosts"
fi
echo ""
echo "[INFO] ensure minimal dependencies ..."
apt-get update
apt-get install -y curl
echo ""
echo "[INFO] Generating certificates ..."
rm -rf "${cert_folder}"
mkdir -p "${cert_folder}"
cat > "${cert_folder}/CONFIG" <<EOF
[ req ]
default_bits = 1024
default_keyfile = keyfile.pem
distinguished_name = req_distinguished_name
prompt = no
req_extensions = v3_req
[ req_distinguished_name ]
C = DE
ST = Berlin
L = Berlin
O = Cloudron UG
OU = Cloudron
CN = ${fqdn}
emailAddress = cert@cloudron.io
[ v3_req ]
# Extensions to add to a certificate request
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = ${fqdn}
DNS.2 = *.${fqdn}
EOF
# generate cert files
openssl genrsa 2048 > "${cert_folder}/host.key"
openssl req -new -out "${cert_folder}/host.csr" -key "${cert_folder}/host.key" -config "${cert_folder}/CONFIG"
openssl x509 -req -days 3650 -in "${cert_folder}/host.csr" -signkey "${cert_folder}/host.key" -out "${cert_folder}/host.cert" -extensions v3_req -extfile "${cert_folder}/CONFIG"
# make them json compatible, by collapsing to one line
tls_cert=$(sed ':a;N;$!ba;s/\n/\\n/g' "${cert_folder}/host.cert")
tls_key=$(sed ':a;N;$!ba;s/\n/\\n/g' "${cert_folder}/host.key")
echo ""
echo "[INFO] Fetching installer code ..."
curl "${installer_code_url}" -o "${installer_code_file}"
echo ""
echo "[INFO] Extracting installer code to ${installer_tmp_dir} ..."
rm -rf "${installer_tmp_dir}" && mkdir -p "${installer_tmp_dir}"
tar xvf "${installer_code_file}" -C "${installer_tmp_dir}"
echo ""
echo "Creating initial provisioning config ..."
cat > /root/provision.json <<EOF
{
"sourceTarballUrl": "",
"data": {
"apiServerOrigin": "${api_server_origin}",
"webServerOrigin": "${web_server_origin}",
"fqdn": "${fqdn}",
"token": "",
"isCustomDomain": true,
"boxVersionsUrl": "${versions_url}",
"version": "",
"tlsCert": "${tls_cert}",
"tlsKey": "${tls_key}",
"provider": "${provider}",
"backupConfig": {
"provider": "s3",
"accessKeyId": "${aws_access_key_id}",
"secretAccessKey": "${aws_access_key_secret}",
"bucket": "${aws_backup_bucket}",
"prefix": "backups"
},
"dnsConfig": {
"provider": "route53",
"accessKeyId": "${aws_access_key_id}",
"secretAccessKey": "${aws_access_key_secret}"
},
"tlsConfig": {
"provider": "letsencrypt-<%= env %>"
}
}
}
EOF
echo "[INFO] Running Ubuntu initializing script ..."
/bin/bash "${installer_tmp_dir}/baseimage/initializeBaseUbuntuImage.sh" "${revision}" selfhosting
echo ""
echo "[INFO] Reloading systemd daemon ..."
systemctl daemon-reload
echo ""
echo "[INFO] Restart docker ..."
systemctl restart docker
echo ""
echo "[FINISHED] Now starting Cloudron init jobs ..."
systemctl start box-setup
# TODO this is only for convenience we should probably just let the user do a restart
sleep 5 && sync
systemctl start cloudron-installer
journalctl -u cloudron-installer.service -f
+518 -142
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -21,6 +21,7 @@
"json": "^9.0.3",
"morgan": "^1.5.1",
"proxy-middleware": "^0.15.0",
"request": "^2.72.0",
"safetydance": "0.0.19",
"semver": "^5.1.0",
"superagent": "^0.21.0"
+31 -5
View File
@@ -16,6 +16,7 @@ var assert = require('assert'),
json = require('body-parser').json,
lastMile = require('connect-lastmile'),
morgan = require('morgan'),
request = require('request'),
superagent = require('superagent');
exports = module.exports = {
@@ -29,17 +30,42 @@ var CLOUDRON_CONFIG_FILE = '/home/yellowtent/configs/cloudron.conf';
var gHttpServer = null; // update server; used for updates
function provisionDigitalOcean(callback) {
if (fs.existsSync(CLOUDRON_CONFIG_FILE)) return callback(null); // already provisioned
superagent.get('http://169.254.169.254/metadata/v1.json').end(function (error, result) {
if (error || result.statusCode !== 200) {
console.error('Error getting metadata', error);
return callback(new Error('Error getting metadata'));
}
var userData = JSON.parse(result.body.user_data);
callback(null, JSON.parse(result.body.user_data));
});
}
installer.provision(userData, callback);
function provisionEC2(callback) {
// need to use request, since octet-stream data
request('http://169.254.169.254/latest/user-data', function (error, response, body) {
if (error || response.statusCode !== 200) {
console.error('Error getting metadata', error);
return callback(new Error('Error getting metadata'));
}
callback(null, JSON.parse(body));
});
}
function provision(callback) {
if (fs.existsSync(CLOUDRON_CONFIG_FILE)) return callback(null); // already provisioned
// try first digitalocean, then ec2
provisionDigitalOcean(function (error, userData) {
if (!error) return installer.provision(userData, callback);
provisionEC2(function (error, userData) {
if (!error) return installer.provision(userData, callback);
console.error('Unable to get meta data', error);
callback(new Error('Error getting metadata'));
});
});
}
@@ -122,7 +148,7 @@ function start(callback) {
actions = [
startUpdateServer,
provisionDigitalOcean
provision
];
}
+3
View File
@@ -16,6 +16,9 @@ if [[ -b "/dev/xvda1" ]]; then
disk_device="/dev/xvda1"
fi
# allow root access over ssh
sed -e 's/.* \(ssh-rsa.*\)/\1/' -i /root/.ssh/authorized_keys
# all sizes are in mb
readonly physical_memory=$(free -m | awk '/Mem:/ { print $2 }')
readonly swap_size="${physical_memory}" # if you change this, fix enoughResourcesAvailable() in client.js
@@ -0,0 +1,24 @@
'use strict';
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
exports.up = function(db, callback) {
var cmd = 'CREATE TABLE mailboxes(' +
'name VARCHAR(128) NOT NULL,' +
'aliasTarget VARCHAR(128),' +
'creationTime TIMESTAMP,' +
'PRIMARY KEY (name))';
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE mailboxes', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,25 @@
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
// imports mailbox entries for existing users
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
function addUserMailboxes(done) {
db.all('SELECT username FROM users', function (error, results) {
if (error) return done(error);
async.eachSeries(results, function (r, next) {
if (!r.username) return next();
db.runSql('INSERT INTO mailboxes (name) VALUES (?)', [ r.username ], next);
}, done);
});
},
db.runSql.bind(db, 'COMMIT')
], callback);
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,16 @@
dbm = dbm || require('db-migrate');
var type = dbm.dataType;
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN lastBackupConfigJson', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN lastBackupConfigJson TEXT', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,16 @@
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps MODIFY installationProgress TEXT', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps MODIFY installationProgress VARCHAR(512)', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,15 @@
dbm = dbm || require('db-migrate');
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN xFrameOptions VARCHAR(512)', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN xFrameOptions', function (error) {
if (error) console.error(error);
callback(error);
});
};
+18 -7
View File
@@ -38,7 +38,7 @@ CREATE TABLE IF NOT EXISTS tokens(
identifier VARCHAR(128) NOT NULL,
clientId VARCHAR(128),
scope VARCHAR(512) NOT NULL,
expires BIGINT NOT NULL,
expires BIGINT NOT NULL, // FIXME: make this a timestamp
PRIMARY KEY(accessToken));
CREATE TABLE IF NOT EXISTS clients(
@@ -54,7 +54,7 @@ CREATE TABLE IF NOT EXISTS apps(
id VARCHAR(128) NOT NULL UNIQUE,
appStoreId VARCHAR(128) NOT NULL,
installationState VARCHAR(512) NOT NULL,
installationProgress VARCHAR(512),
installationProgress TEXT,
runState VARCHAR(512),
health VARCHAR(128),
containerId VARCHAR(128),
@@ -62,16 +62,16 @@ 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),
accessRestrictionJson TEXT,
accessRestrictionJson TEXT, // { users: [ ], groups: [ ] }
oauthProxy BOOLEAN DEFAULT 0,
createdAt TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
memoryLimit BIGINT DEFAULT 0,
altDomain VARCHAR(256),
xFrameOptions VARCHAR(512),
lastBackupId VARCHAR(128),
lastBackupConfigJson TEXT, // used for appstore and non-appstore installs. it's here so it's easy to do REST validation
lastBackupId VARCHAR(128), // tracks last valid backup, can be removed
oldConfigJson TEXT, // used to pass old config for apptask
oldConfigJson TEXT, // used to pass old config for apptask, can be removed when we use a queue
PRIMARY KEY(id));
@@ -86,7 +86,7 @@ CREATE TABLE IF NOT EXISTS authcodes(
authCode VARCHAR(128) NOT NULL UNIQUE,
userId VARCHAR(128) NOT NULL,
clientId VARCHAR(128) NOT NULL,
expiresAt BIGINT NOT NULL,
expiresAt BIGINT NOT NULL, // ## FIXME: make this a timestamp
PRIMARY KEY(authCode));
CREATE TABLE IF NOT EXISTS settings(
@@ -115,6 +115,17 @@ CREATE TABLE IF NOT EXISTS eventlog(
action VARCHAR(128) NOT NULL,
source JSON, /* { userId, username, ip }. userId can be null for cron,sysadmin */
data JSON, /* free flowing json based on action */
creationTime TIMESTAMP, /* FIXME: precision must be TIMESTAMP(2) */
PRIMARY KEY (id));
/* Future fields:
* accessRestriction - to determine who can access it. So this has foreign keys
* quota - per mailbox quota
*/
CREATE TABLE IF NOT EXISTS mailboxes(
name VARCHAR(128) NOT NULL,
aliasTarget VARCHAR(128), /* the target name type is an alias */
creationTime TIMESTAMP,
PRIMARY KEY (id));
+4227 -2329
View File
File diff suppressed because it is too large Load Diff
+3 -5
View File
@@ -16,10 +16,9 @@
"async": "^1.2.1",
"aws-sdk": "^2.1.46",
"body-parser": "^1.13.1",
"bytes": "^2.3.0",
"cloudron-manifestformat": "^2.3.0",
"cloudron-manifestformat": "^2.4.0",
"connect-ensure-login": "^0.1.1",
"connect-lastmile": "0.0.13",
"connect-lastmile": "^0.1.0",
"connect-timeout": "^1.5.0",
"cookie-parser": "^1.3.5",
"cookie-session": "^1.1.0",
@@ -33,6 +32,7 @@
"express": "^4.12.4",
"express-session": "^1.11.3",
"hat": "0.0.3",
"ini": "^1.3.4",
"json": "^9.0.3",
"ldapjs": "^0.7.1",
"mime": "^1.3.4",
@@ -56,7 +56,6 @@
"proxy-middleware": "^0.13.0",
"safetydance": "^0.1.1",
"semver": "^4.3.6",
"serve-favicon": "^2.2.0",
"split": "^1.0.0",
"superagent": "^1.8.3",
"supererror": "^0.7.1",
@@ -89,7 +88,6 @@
"mocha": "*",
"nock": "^3.4.0",
"node-sass": "^3.0.0-alpha.0",
"redis": "^2.4.2",
"request": "^2.65.0",
"sinon": "^1.12.2",
"yargs": "^3.15.0"
+20 -9
View File
@@ -10,7 +10,8 @@ arg_fqdn=""
arg_is_custom_domain="false"
arg_restore_key=""
arg_restore_url=""
arg_retire="false"
arg_retire_reason=""
arg_retire_info=""
arg_tls_config=""
arg_tls_cert=""
arg_tls_key=""
@@ -23,20 +24,30 @@ arg_update_config=""
arg_provider=""
arg_app_bundle=""
args=$(getopt -o "" -l "data:,retire" -n "$0" -- "$@")
args=$(getopt -o "" -l "data:,retire-reason:,retire-info:" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--retire)
arg_retire="true"
shift
--retire-reason)
arg_retire_reason="$2"
shift 2
;;
--retire-info)
arg_retire_info="$2"
shift 2
;;
--data)
# only read mandatory non-empty parameters here
read -r arg_api_server_origin arg_web_server_origin arg_fqdn arg_is_custom_domain arg_box_versions_url arg_version <<EOF
$(echo "$2" | $json apiServerOrigin webServerOrigin fqdn isCustomDomain boxVersionsUrl version | tr '\n' ' ')
EOF
# these params must be valid in all cases
arg_fqdn=$(echo "$2" | $json fqdn)
arg_is_custom_domain=$(echo "$2" | $json isCustomDomain)
# only update/restore have this valid (but not migrate)
arg_api_server_origin=$(echo "$2" | $json apiServerOrigin)
arg_web_server_origin=$(echo "$2" | $json webServerOrigin)
arg_box_versions_url=$(echo "$2" | $json boxVersionsUrl)
arg_version=$(echo "$2" | $json version)
# read possibly empty parameters here
arg_app_bundle=$(echo "$2" | $json appBundle)
[[ "${arg_app_bundle}" == "" ]] && arg_app_bundle="[]"
+3
View File
@@ -5,3 +5,6 @@
[mysqld]
performance_schema=OFF
max_connections=50
# on ec2, without this we get a sporadic connection drop when doing the initial migration
max_allowed_packet=32M
-3
View File
@@ -31,6 +31,3 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/collectlogs.sh
Defaults!/home/yellowtent/box/src/scripts/retire.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/retire.sh
Defaults!/home/yellowtent/box/src/scripts/setup_infra.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/setup_infra.sh
+13 -8
View File
@@ -9,8 +9,6 @@ readonly BOX_SRC_DIR="/home/yellowtent/box"
readonly DATA_DIR="/home/yellowtent/data"
readonly ADMIN_LOCATION="my" # keep this in sync with constants.js
source "${script_dir}/../src/INFRA_VERSION" # this injects INFRA_VERSION
echo "Setting up nginx update page"
source "${script_dir}/argparser.sh" "$@" # this injects the arg_* variables used below
@@ -24,17 +22,24 @@ rm -rf "${SETUP_WEBSITE_DIR}" && mkdir -p "${SETUP_WEBSITE_DIR}"
cp -r "${script_dir}/splash/website/"* "${SETUP_WEBSITE_DIR}"
# create nginx config
infra_version="none"
[[ -f "${DATA_DIR}/INFRA_VERSION" ]] && infra_version=$(cat "${DATA_DIR}/INFRA_VERSION")
if [[ "${arg_retire}" == "true" || "${infra_version}" != "${INFRA_VERSION}" ]]; then
readonly current_infra=$(node -e "console.log(require('${script_dir}/../src/infra_version.js').version);")
existing_infra="none"
[[ -f "${DATA_DIR}/INFRA_VERSION" ]] && existing_infra=$(node -e "console.log(JSON.parse(require('fs').readFileSync('${DATA_DIR}/INFRA_VERSION', 'utf8')).version);")
if [[ "${arg_retire_reason}" != "" || "${existing_infra}" != "${current_infra}" ]]; then
echo "Showing progress bar on all subdomains in retired mode or infra update. retire: ${arg_retire_reason} existing: ${existing_infra} current: ${current_infra}"
rm -f ${DATA_DIR}/nginx/applications/*
${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
-O "{ \"vhost\": \"~^(.+)\$\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
-O "{ \"vhost\": \"~^(.+)\$\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\", \"xFrameOptions\": \"SAMEORIGIN\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
else
echo "Show progress bar only on admin domain for normal update"
${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
-O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
-O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\", \"xFrameOptions\": \"SAMEORIGIN\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
fi
echo '{ "update": { "percent": "10", "message": "Updating cloudron software" }, "backup": null }' > "${SETUP_WEBSITE_DIR}/progress.json"
if [[ "${arg_retire_reason}" == "migrate" ]]; then
echo "{ \"migrate\": { \"percent\": \"10\", \"message\": \"Migrating cloudron. This could take up to 15 minutes.\", \"info\": ${arg_retire_info} }, \"backup\": null, \"apiServerOrigin\": \"${arg_api_server_origin}\" }" > "${SETUP_WEBSITE_DIR}/progress.json"
else
echo '{ "update": { "percent": "10", "message": "Updating cloudron software" }, "backup": null }' > "${SETUP_WEBSITE_DIR}/progress.json"
fi
nginx -s reload
+19 -9
View File
@@ -34,6 +34,14 @@ set_progress() {
set_progress "1" "Create container"
$script_dir/container.sh
set_progress "5" "Adjust system settings"
hostnamectl set-hostname "${arg_fqdn}"
# ec2 instances use lots of cpu for swapping, which can be significantly reduced adjusting the swappiness
if [[ "${arg_provider}" == 'ec2' ]]; then
sysctl vm.swappiness=0
fi
set_progress "10" "Ensuring directories"
# keep these in sync with paths.js
[[ "${is_update}" == "false" ]] && btrfs subvolume create "${DATA_DIR}/box"
@@ -107,7 +115,7 @@ if [[ -f "${DATA_DIR}/box/certs/${admin_fqdn}.cert" && -f "${DATA_DIR}/box/certs
admin_key_file="${DATA_DIR}/box/certs/${admin_fqdn}.key"
fi
${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
-O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"admin\", \"sourceDir\": \"${BOX_SRC_DIR}\", \"certFilePath\": \"${admin_cert_file}\", \"keyFilePath\": \"${admin_key_file}\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
-O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"admin\", \"sourceDir\": \"${BOX_SRC_DIR}\", \"certFilePath\": \"${admin_cert_file}\", \"keyFilePath\": \"${admin_key_file}\", \"xFrameOptions\": \"SAMEORIGIN\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
mkdir -p "${DATA_DIR}/nginx/cert"
if [[ -f "${DATA_DIR}/box/certs/host.cert" && -f "${DATA_DIR}/box/certs/host.key" ]]; then
@@ -120,6 +128,7 @@ fi
set_progress "33" "Changing ownership"
chown "${USER}:${USER}" -R "${DATA_DIR}/box" "${DATA_DIR}/nginx" "${DATA_DIR}/collectd" "${DATA_DIR}/addons" "${DATA_DIR}/acme"
chown "${USER}:${USER}" "${DATA_DIR}/INFRA_VERSION" || true
chown "${USER}:${USER}" "${DATA_DIR}"
set_progress "65" "Creating cloudron.conf"
@@ -135,7 +144,6 @@ cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
"fqdn": "${arg_fqdn}",
"isCustomDomain": ${arg_is_custom_domain},
"boxVersionsUrl": "${arg_box_versions_url}",
"adminEmail": "\"Cloudron\" <no-reply@${arg_fqdn}>",
"provider": "${arg_provider}",
"database": {
"hostname": "localhost",
@@ -188,18 +196,20 @@ if [[ ! -z "${arg_tls_config}" ]]; then
-e "REPLACE INTO settings (name, value) VALUES (\"tls_config\", '$arg_tls_config')" box
fi
# Add webadmin oauth client
# 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"
echo "Add webadmin api cient"
readonly ADMIN_SCOPES="cloudron,developer,profile,users,apps,settings"
mysql -u root -p${mysql_root_password} \
-e "REPLACE INTO clients (id, appId, type, clientSecret, redirectURI, scope) VALUES (\"cid-webadmin\", \"webadmin\", \"admin\", \"secret-webadmin\", \"${admin_origin}\", \"${ADMIN_SCOPES}\")" box
-e "REPLACE INTO clients (id, appId, type, clientSecret, redirectURI, scope) VALUES (\"cid-webadmin\", \"Settings\", \"built-in\", \"secret-webadmin\", \"${admin_origin}\", \"${ADMIN_SCOPES}\")" box
echo "Add localhost test oauth client"
ADMIN_SCOPES="root,developer,profile,users,apps,settings"
echo "Add SDK api client"
mysql -u root -p${mysql_root_password} \
-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
-e "REPLACE INTO clients (id, appId, type, clientSecret, redirectURI, scope) VALUES (\"cid-sdk\", \"SDK\", \"built-in\", \"secret-sdk\", \"${admin_origin}\", \"*,roleSdk\")" box
echo "Add cli api client"
mysql -u root -p${mysql_root_password} \
-e "REPLACE INTO clients (id, appId, type, clientSecret, redirectURI, scope) VALUES (\"cid-cli\", \"Cloudron Tool\", \"built-in\", \"secret-cli\", \"${admin_origin}\", \"*,roleSdk\")" box
set_progress "80" "Starting Cloudron"
systemctl start cloudron.target
+4
View File
@@ -24,6 +24,9 @@ server {
ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';
add_header Strict-Transport-Security "max-age=15768000; includeSubDomains";
# https://developer.mozilla.org/en-US/docs/Web/HTTP/X-Frame-Options
add_header X-Frame-Options "<%= xFrameOptions %>";
proxy_http_version 1.1;
proxy_intercept_errors on;
proxy_read_timeout 3500;
@@ -59,6 +62,7 @@ server {
client_max_body_size 1m;
}
# the read timeout is between successive reads and not the whole connection
location ~ ^/api/v1/apps/.*/exec$ {
proxy_pass http://127.0.0.1:3000;
proxy_read_timeout 30m;
+12 -11
View File
@@ -24,7 +24,14 @@ http {
sendfile on;
keepalive_timeout 65;
# timeout for client to finish sending headers
client_header_timeout 30s;
# timeout for reading client request body (successive read timeout and not whole body!)
client_body_timeout 60s;
# keep-alive connections timeout in 65s. this is because many browsers timeout in 60 seconds
keepalive_timeout 65s;
# HTTP server
server {
@@ -50,22 +57,15 @@ http {
}
}
# We have to enable https for nginx to read in the vhost in http request
# and send a 404. This is a side-effect of using wildcard DNS
# This server handles the naked domain for custom domains.
# It can also be used for wildcard subdomain 404. This feature is not used by the Cloudron itself
# because box always sets up DNS records for app subdomains.
server {
listen 443 default_server;
ssl on;
ssl_certificate cert/host.cert;
ssl_certificate_key cert/host.key;
# increase the proxy buffer sizes to not run into buffer issues (http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffers)
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
# Disable check to allow unlimited body sizes
client_max_body_size 0;
error_page 404 = @fallback;
location @fallback {
internal;
@@ -79,6 +79,7 @@ http {
rewrite ^/$ /nakeddomain.html break;
}
# required for /api/v1/cloudron/avatar
location /api/ {
proxy_pass http://127.0.0.1:3000;
client_max_body_size 1m;
-23
View File
@@ -1,23 +0,0 @@
#!/bin/bash
# If you change the infra version, be sure to put a warning
# in the change log
INFRA_VERSION=30
# WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
# These constants are used in the installer script as well
BASE_IMAGE=cloudron/base:0.8.0
MYSQL_IMAGE=cloudron/mysql:0.11.0
POSTGRESQL_IMAGE=cloudron/postgresql:0.9.0
MONGODB_IMAGE=cloudron/mongodb:0.9.0
REDIS_IMAGE=cloudron/redis:0.8.0 # if you change this, fix src/addons.js as well
MAIL_IMAGE=cloudron/mail:0.12.0
GRAPHITE_IMAGE=cloudron/graphite:0.8.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
+142 -158
View File
@@ -1,15 +1,12 @@
'use strict';
exports = module.exports = {
initialize: initialize,
setupAddons: setupAddons,
teardownAddons: teardownAddons,
backupAddons: backupAddons,
restoreAddons: restoreAddons,
getEnvironment: getEnvironment,
getLinksSync: getLinksSync,
getBindsSync: getBindsSync,
getContainerNamesSync: getContainerNamesSync,
@@ -21,29 +18,34 @@ exports = module.exports = {
var appdb = require('./appdb.js'),
assert = require('assert'),
async = require('async'),
certificates = require('./certificates.js'),
clientdb = require('./clientdb.js'),
clients = require('./clients.js'),
config = require('./config.js'),
DatabaseError = require('./databaseerror.js'),
ClientsError = clients.ClientsError,
debug = require('debug')('box:addons'),
docker = require('./docker.js'),
dockerConnection = docker.connection,
fs = require('fs'),
generatePassword = require('password-generator'),
hat = require('hat'),
infra = require('./infra_version.js'),
once = require('once'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
shell = require('./shell.js'),
util = require('util'),
uuid = require('node-uuid');
util = require('util');
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 = {
email: {
setup: setupEmail,
teardown: teardownEmail,
backup: NOOP,
restore: setupEmail
},
ldap: {
setup: setupLdap,
teardown: teardownLdap,
@@ -80,6 +82,12 @@ var KNOWN_ADDONS = {
backup: backupPostgreSql,
restore: restorePostgreSql
},
recvmail: {
setup: setupRecvMail,
teardown: teardownRecvMail,
backup: NOOP,
restore: setupRecvMail
},
redis: {
setup: setupRedis,
teardown: teardownRedis,
@@ -112,8 +120,7 @@ var KNOWN_ADDONS = {
}
};
var RMAPPDIR_CMD = path.join(__dirname, 'scripts/rmappdir.sh'),
SETUP_INFRA_CMD = path.join(__dirname, 'scripts/setup_infra.sh');;
var RMAPPDIR_CMD = path.join(__dirname, 'scripts/rmappdir.sh');
function debugApp(app, args) {
assert(!app || typeof app === 'object');
@@ -122,17 +129,6 @@ function debugApp(app, args) {
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
function initialize(callback) {
if (process.env.BOX_ENV === 'test') return callback();
debug('initializing addon infrastructure');
certificates.getAdminCertificatePath(function (error, certFilePath, keyFilePath) {
if (error) return callback(error);
shell.sudo('seutp_infra', [ SETUP_INFRA_CMD, config.fqdn(), config.adminFqdn(), certFilePath, keyFilePath ], callback);
});
}
function setupAddons(app, addons, callback) {
assert.strictEqual(typeof app, 'object');
assert(!addons || typeof addons === 'object');
@@ -212,28 +208,6 @@ function getEnvironment(app, callback) {
appdb.getAddonConfigByAppId(app.id, callback);
}
function getLinksSync(app, addons) {
assert.strictEqual(typeof app, 'object');
assert(!addons || typeof addons === 'object');
var links = [ ];
if (!addons) return links;
for (var addon in addons) {
switch (addon) {
case 'mysql': links.push('mysql:mysql'); break;
case 'postgresql': links.push('postgresql:postgresql'); break;
case 'sendmail': links.push('mail:mail'); break;
case 'redis': links.push('redis-' + app.id + ':redis-' + app.id); break;
case 'mongodb': links.push('mongodb:mongodb'); break;
default: break;
}
}
return links;
}
function getBindsSync(app, addons) {
assert.strictEqual(typeof app, 'object');
assert(!addons || typeof addons === 'object');
@@ -280,22 +254,18 @@ function setupOauth(app, options, callback) {
assert.strictEqual(typeof callback, 'function');
var appId = app.id;
var id = 'cid-' + uuid.v4();
var clientSecret = hat(256);
var redirectURI = 'https://' + config.appFqdn(app.location);
var scope = 'profile';
debugApp(app, 'setupOauth: id:%s clientSecret:%s', id, clientSecret);
clients.delByAppIdAndType(appId, clients.TYPE_OAUTH, function (error) { // remove existing creds
if (error && error.reason !== ClientsError.NOT_FOUND) return callback(error);
clientdb.delByAppIdAndType(appId, clientdb.TYPE_OAUTH, function (error) { // remove existing creds
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
clientdb.add(id, appId, clientdb.TYPE_OAUTH, clientSecret, redirectURI, scope, function (error) {
clients.add(appId, clients.TYPE_OAUTH, redirectURI, scope, function (error, result) {
if (error) return callback(error);
var env = [
'OAUTH_CLIENT_ID=' + id,
'OAUTH_CLIENT_SECRET=' + clientSecret,
'OAUTH_CLIENT_ID=' + result.id,
'OAUTH_CLIENT_SECRET=' + result.clientSecret,
'OAUTH_ORIGIN=' + config.adminOrigin()
];
@@ -313,8 +283,8 @@ function teardownOauth(app, options, callback) {
debugApp(app, 'teardownOauth');
clientdb.delByAppIdAndType(app.id, clientdb.TYPE_OAUTH, function (error) {
if (error && error.reason !== DatabaseError.NOT_FOUND) console.error(error);
clients.delByAppIdAndType(app.id, clients.TYPE_OAUTH, function (error) {
if (error && error.reason !== ClientsError.NOT_FOUND) console.error(error);
appdb.unsetAddonConfig(app.id, 'oauth', callback);
});
@@ -326,23 +296,20 @@ function setupSimpleAuth(app, options, callback) {
assert.strictEqual(typeof callback, 'function');
var appId = app.id;
var id = 'cid-' + uuid.v4();
var scope = 'profile';
debugApp(app, 'setupSimpleAuth: id:%s', id);
clients.delByAppIdAndType(app.id, clients.TYPE_SIMPLE_AUTH, function (error) { // remove existing creds
if (error && error.reason !== ClientsError.NOT_FOUND) return callback(error);
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) {
clients.add(appId, clients.TYPE_SIMPLE_AUTH, '', scope, function (error, result) {
if (error) return callback(error);
var env = [
'SIMPLE_AUTH_SERVER=172.17.0.1',
'SIMPLE_AUTH_SERVER=172.18.0.1',
'SIMPLE_AUTH_PORT=' + config.get('simpleAuthPort'),
'SIMPLE_AUTH_URL=http://172.17.0.1:' + config.get('simpleAuthPort'), // obsolete, remove
'SIMPLE_AUTH_ORIGIN=http://172.17.0.1:' + config.get('simpleAuthPort'),
'SIMPLE_AUTH_CLIENT_ID=' + id
'SIMPLE_AUTH_URL=http://172.18.0.1:' + config.get('simpleAuthPort'), // obsolete, remove
'SIMPLE_AUTH_ORIGIN=http://172.18.0.1:' + config.get('simpleAuthPort'),
'SIMPLE_AUTH_CLIENT_ID=' + result.id
];
debugApp(app, 'Setting simple auth addon config to %j', env);
@@ -359,26 +326,57 @@ function teardownSimpleAuth(app, options, callback) {
debugApp(app, 'teardownSimpleAuth');
clientdb.delByAppIdAndType(app.id, clientdb.TYPE_SIMPLE_AUTH, function (error) {
if (error && error.reason !== DatabaseError.NOT_FOUND) console.error(error);
clients.delByAppIdAndType(app.id, clients.TYPE_SIMPLE_AUTH, function (error) {
if (error && error.reason !== ClientsError.NOT_FOUND) console.error(error);
appdb.unsetAddonConfig(app.id, 'simpleauth', callback);
});
}
function setupEmail(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
// note that "external" access info can be derived from MAIL_DOMAIN (since it's part of user documentation)
var env = [
'MAIL_SMTP_SERVER=mail',
'MAIL_SMTP_PORT=2525',
'MAIL_IMAP_SERVER=mail',
'MAIL_IMAP_PORT=9993',
'MAIL_SIEVE_SERVER=mail',
'MAIL_SIEVE_PORT=4190',
'MAIL_DOMAIN=' + config.fqdn()
];
debugApp(app, 'Setting up Email');
appdb.setAddonConfig(app.id, 'email', env, callback);
}
function teardownEmail(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Tearing down Email');
appdb.unsetAddonConfig(app.id, 'email', 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.0.1',
'LDAP_SERVER=172.18.0.1',
'LDAP_PORT=' + config.get('ldapPort'),
'LDAP_URL=ldap://172.17.0.1:' + config.get('ldapPort'),
'LDAP_URL=ldap://172.18.0.1:' + config.get('ldapPort'),
'LDAP_USERS_BASE_DN=ou=users,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
'LDAP_BIND_PASSWORD=' + hat(8 * 128) // this is ignored
];
debugApp(app, 'Setting up LDAP');
@@ -401,20 +399,17 @@ function setupSendMail(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var from = (app.location ? app.location : app.manifest.title.replace(/[^a-zA-Z0-9]/, '')) + '-app';
var from = (app.location ? app.location : app.manifest.title.replace(/[^a-zA-Z0-9]/g, '')) + '.app';
var env = [
'MAIL_SMTP_SERVER=mail',
'MAIL_SMTP_PORT=2500', // if you change this, change the mail container
'MAIL_SMTP_USERNAME=' + from, // change this to app.id after apps have moved
'MAIL_SMTP_PASSWORD=' + 'app-' + app.id, // this is ignored
'MAIL_FROM=' + from + '@' + config.fqdn(),
'MAIL_DOMAIN=' + config.fqdn()
];
var cmd = [ '/addons/mail/service.sh', 'add-send', from ];
debugApp(app, 'Setting up sendmail');
docker.execContainer('mail', cmd, { bufferStdout: true }, function (error, stdout) {
if (error) return callback(error);
appdb.setAddonConfig(app.id, 'sendmail', env, callback);
var env = stdout.toString('utf8').split('\n').slice(0, -1); // remove trailing newline
debugApp(app, 'Setting sendmail addon config to %j', env);
appdb.setAddonConfig(app.id, 'sendmail', env, callback);
});
}
function teardownSendMail(app, options, callback) {
@@ -422,9 +417,55 @@ function teardownSendMail(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Tearing down sendmail');
var from = (app.location ? app.location : app.manifest.title.replace(/[^a-zA-Z0-9]/g, '')) + '.app';
appdb.unsetAddonConfig(app.id, 'sendmail', callback);
var cmd = [ '/addons/mail/service.sh', 'remove-send', from ];
debugApp(app, 'Tearing down sendmail : %j', cmd);
docker.execContainer('mail', cmd, { }, function (error) {
if (error) return callback(error);
appdb.unsetAddonConfig(app.id, 'sendmail', callback);
});
}
function setupRecvMail(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Setting up recvmail');
var to = (app.location ? app.location : app.manifest.title.replace(/[^a-zA-Z0-9]/g, '')) + '.app';
var cmd = [ '/addons/mail/service.sh', 'add-recv', to ];
docker.execContainer('mail', cmd, { bufferStdout: true }, function (error, stdout) {
if (error) return callback(error);
var env = stdout.toString('utf8').split('\n').slice(0, -1); // remove trailing newline
debugApp(app, 'Setting recvmail addon config to %j', env);
appdb.setAddonConfig(app.id, 'recvmail', env, callback);
});
}
function teardownRecvMail(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var to = (app.location ? app.location : app.manifest.title.replace(/[^a-zA-Z0-9]/g, '')) + '.app';
var cmd = [ '/addons/mail/service.sh', 'remove-recv', to ];
debugApp(app, 'Tearing down recvmail: %j', cmd);
docker.execContainer('mail', cmd, { }, function (error) {
if (error) return callback(error);
appdb.unsetAddonConfig(app.id, 'recvmail', callback);
});
}
function setupMySql(app, options, callback) {
@@ -617,38 +658,6 @@ function restoreMongoDb(app, options, callback) {
});
}
function forwardRedisPort(appId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
dockerConnection.getContainer('redis-' + appId).inspect(function (error, data) {
if (error) return callback(new Error('Unable to inspect container:' + error));
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'));
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, options, callback) {
assert.strictEqual(typeof app, 'object');
@@ -665,57 +674,32 @@ function setupRedis(app, options, callback) {
if (!safe.fs.mkdirSync(redisDataDir) && safe.error.code !== 'EEXIST') return callback(new Error('Error creating redis data dir:' + safe.error));
var createOptions = {
name: 'redis-' + app.id,
Hostname: 'redis-' + app.location,
Tty: true,
Image: 'cloudron/redis:0.8.0', // if you change this, fix src/INFRA_VERSION as well
Cmd: null,
Volumes: {
'/tmp': {},
'/run': {}
},
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
}
}
};
const tag = infra.images.redis.tag, redisName = 'redis-' + app.id;
const cmd = `docker run --restart=always -d --name=${redisName} \
--net cloudron \
--net-alias ${redisName} \
-m 100m \
--memory-swap 150m \
-v ${redisVarsFile}:/etc/redis/redis_vars.sh:ro \
-v ${redisDataDir}:/var/lib/redis:rw \
--read-only -v /tmp -v /run ${tag}`;
var env = [
'REDIS_URL=redis://redisuser:' + redisPassword + '@redis-' + app.id,
'REDIS_PASSWORD=' + redisPassword,
'REDIS_HOST=redis-' + app.id,
'REDIS_HOST=' + redisName,
'REDIS_PORT=6379'
];
var redisContainer = dockerConnection.getContainer(createOptions.name);
stopAndRemoveRedis(redisContainer, function () {
dockerConnection.createContainer(createOptions, function (error) {
if (error && error.statusCode !== 409) return callback(error); // if not already created
redisContainer.start(function (error) {
if (error && error.statusCode !== 304) return callback(error); // if not already running
appdb.setAddonConfig(app.id, 'redis', env, function (error) {
if (error) return callback(error);
forwardRedisPort(app.id, callback);
});
});
});
async.series([
// stop so that redis can flush itself with SIGTERM
shell.execSync.bind(null, 'stopRedis', `docker stop --time=10 ${redisName} 2>/dev/null || true`),
shell.execSync.bind(null, 'stopRedis', `docker rm --volumes ${redisName} 2>/dev/null || true`),
shell.execSync.bind(null, 'startRedis', cmd),
appdb.setAddonConfig.bind(null, app.id, 'redis', env)
], function (error) {
if (error) debug('Error setting up redis: ', error);
callback(error);
});
}
+21 -16
View File
@@ -26,6 +26,7 @@ exports = module.exports = {
// installation codes (keep in sync in UI)
ISTATE_PENDING_INSTALL: 'pending_install', // installs and fresh reinstalls
ISTATE_PENDING_CLONE: 'pending_clone', // clone
ISTATE_PENDING_CONFIGURE: 'pending_configure', // config (location, port) changes and on infra update
ISTATE_PENDING_UNINSTALL: 'pending_uninstall', // uninstallation
ISTATE_PENDING_RESTORE: 'pending_restore', // restore to previous backup or on upgrade
@@ -58,7 +59,7 @@ var assert = require('assert'),
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.installationProgress', 'apps.runState',
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'apps.location', 'apps.dnsRecordId',
'apps.accessRestrictionJson', 'apps.lastBackupId', 'apps.lastBackupConfigJson', 'apps.oldConfigJson', 'apps.memoryLimit', 'apps.altDomain' ].join(',');
'apps.accessRestrictionJson', 'apps.lastBackupId', 'apps.oldConfigJson', 'apps.memoryLimit', 'apps.altDomain', 'apps.xFrameOptions' ].join(',');
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'environmentVariable', 'appId' ].join(',');
@@ -69,10 +70,6 @@ function postProcess(result) {
result.manifest = safe.JSON.parse(result.manifestJson);
delete result.manifestJson;
assert(result.lastBackupConfigJson === null || typeof result.lastBackupConfigJson === 'string');
result.lastBackupConfig = safe.JSON.parse(result.lastBackupConfigJson);
delete result.lastBackupConfigJson;
assert(result.oldConfigJson === null || typeof result.oldConfigJson === 'string');
result.oldConfig = safe.JSON.parse(result.oldConfigJson);
delete result.oldConfigJson;
@@ -95,6 +92,9 @@ function postProcess(result) {
result.accessRestriction = safe.JSON.parse(result.accessRestrictionJson);
if (result.accessRestriction && !result.accessRestriction.users) result.accessRestriction.users = [];
delete result.accessRestrictionJson;
// TODO remove later once all apps have this attribute
result.xFrameOptions = result.xFrameOptions || 'SAMEORIGIN';
}
function get(id, callback) {
@@ -160,27 +160,32 @@ function getAll(callback) {
});
}
function add(id, appStoreId, manifest, location, portBindings, accessRestriction, memoryLimit, altDomain, callback) {
function add(id, appStoreId, manifest, location, portBindings, data, 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, 'object');
assert.strictEqual(typeof memoryLimit, 'number');
assert(altDomain === null || typeof altDomain === 'string');
assert(data && typeof data === 'object');
assert.strictEqual(typeof callback, 'function');
portBindings = portBindings || { };
var manifestJson = JSON.stringify(manifest);
var accessRestriction = data.accessRestriction || null;
var accessRestrictionJson = JSON.stringify(accessRestriction);
var memoryLimit = data.memoryLimit || 0;
var altDomain = data.altDomain || null;
var xFrameOptions = data.xFrameOptions || '';
var installationState = data.installationState || exports.ISTATE_PENDING_INSTALL;
var lastBackupId = data.lastBackupId || null; // used when cloning
var queries = [ ];
queries.push({
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, exports.ISTATE_PENDING_INSTALL, location, accessRestrictionJson, memoryLimit, altDomain ]
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, lastBackupId) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, lastBackupId ]
});
Object.keys(portBindings).forEach(function (env) {
@@ -284,9 +289,6 @@ function updateWithConstraints(id, app, constraints, callback) {
if (p === 'manifest') {
fields.push('manifestJson = ?');
values.push(JSON.stringify(app[p]));
} else if (p === 'lastBackupConfig') {
fields.push('lastBackupConfigJson = ?');
values.push(JSON.stringify(app[p]));
} else if (p === 'oldConfig') {
fields.push('oldConfigJson = ?');
values.push(JSON.stringify(app[p]));
@@ -345,14 +347,17 @@ function setInstallationCommand(appId, installationState, values, callback) {
// uninstall is allowed in any state
// force update is allowed in any state including pending_uninstall! (for better or worse)
// restore is allowed from installed or error state
// update and configure are allowed only in installed state
// configure is allowed in installed state or currently configuring or in error state
// update and backup are allowed only in installed state
if (installationState === exports.ISTATE_PENDING_UNINSTALL || installationState === exports.ISTATE_PENDING_FORCE_UPDATE) {
updateWithConstraints(appId, values, '', callback);
} else if (installationState === exports.ISTATE_PENDING_RESTORE) {
updateWithConstraints(appId, values, 'AND (installationState = "installed" OR installationState = "error")', callback);
} else if (installationState === exports.ISTATE_PENDING_UPDATE || installationState === exports.ISTATE_PENDING_CONFIGURE || installationState === exports.ISTATE_PENDING_BACKUP) {
} else if (installationState === exports.ISTATE_PENDING_UPDATE || installationState === exports.ISTATE_PENDING_BACKUP) {
updateWithConstraints(appId, values, 'AND installationState = "installed"', callback);
} else if (installationState === exports.ISTATE_PENDING_CONFIGURE) {
updateWithConstraints(appId, values, 'AND (installationState = "installed" OR installationState = "pending_configure" OR installationState = "error")', callback);
} else {
callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, 'invalid installationState'));
}
+12 -5
View File
@@ -17,7 +17,7 @@ exports = module.exports = {
};
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 UNHEALTHY_THRESHOLD = 10 * 60 * 1000; // 10 minutes
var gHealthInfo = { }; // { time, emailSent }
var gRunTimeout = null;
var gDockerEventStream = null;
@@ -138,13 +138,16 @@ function run() {
/*
OOM can be tested using stress tool like so:
docker run -ti -m 100M cloudron/base:0.3.3 /bin/bash
docker run -ti -m 100M cloudron/base:0.8.1 /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');
const OOM_MAIL_LIMIT = 60 * 60 * 1000; // 60 minutes
var lastOomMailTime = new Date(new Date() - OOM_MAIL_LIMIT);
docker.getEvents({ filters: JSON.stringify({ event: [ 'oom' ] }) }, function (error, stream) {
if (error) return console.error(error);
@@ -154,15 +157,19 @@ function processDockerEvents() {
stream.on('data', function (data) {
var ev = JSON.parse(data);
debug('Container ' + ev.id + ' went OOM');
appdb.getByContainerId(ev.id, function (error, app) {
appdb.getByContainerId(ev.id, function (error, app) { // this can error for addons
var program = error || !app.appStoreId ? ev.id : app.appStoreId;
var context = JSON.stringify(ev);
var now = new Date();
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 (error || app.appStoreId !== '') mailer.unexpectedExit(program, context); // app can be null if it's an addon crash
if ((!app || app.appStoreId !== '') && (now - lastOomMailTime > OOM_MAIL_LIMIT)) {
mailer.unexpectedExit(program, context); // app can be null if it's an addon crash
lastOomMailTime = now;
}
});
});
@@ -194,7 +201,7 @@ function stop(callback) {
assert.strictEqual(typeof callback, 'function');
clearTimeout(gRunTimeout);
gDockerEventStream.end();
if (gDockerEventStream) gDockerEventStream.end();
callback();
}
+424 -223
View File
@@ -15,6 +15,7 @@ exports = module.exports = {
uninstall: uninstall,
restore: restore,
clone: clone,
update: update,
@@ -30,7 +31,12 @@ exports = module.exports = {
checkManifestConstraints: checkManifestConstraints,
autoupdateApps: autoupdateApps,
updateApps: updateApps,
restoreInstalledApps: restoreInstalledApps,
configureInstalledApps: configureInstalledApps,
getAppConfig: getAppConfig,
// exported for testing
_validateHostname: validateHostname,
@@ -43,6 +49,7 @@ var addons = require('./addons.js'),
assert = require('assert'),
async = require('async'),
backups = require('./backups.js'),
BackupsError = backups.BackupsError,
certificates = require('./certificates.js'),
config = require('./config.js'),
constants = require('./constants.js'),
@@ -61,7 +68,9 @@ var addons = require('./addons.js'),
split = require('split'),
superagent = require('superagent'),
taskmanager = require('./taskmanager.js'),
url = require('url'),
util = require('util'),
uuid = require('node-uuid'),
validator = require('validator');
// http://dustinsenos.com/articles/customErrorsInNode
@@ -103,16 +112,16 @@ AppsError.BAD_CERTIFICATE = 'Invalid certificate';
// https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
// We are validating the validity of the location-fqdn as host name
function validateHostname(location, fqdn) {
var RESERVED_LOCATIONS = [ constants.ADMIN_LOCATION, constants.API_LOCATION, constants.SMTP_LOCATION, constants.IMAP_LOCATION ];
var RESERVED_LOCATIONS = [ constants.ADMIN_LOCATION, constants.API_LOCATION, constants.SMTP_LOCATION, constants.IMAP_LOCATION, constants.MAIL_LOCATION, constants.POSTMAN_LOCATION ];
if (RESERVED_LOCATIONS.indexOf(location) !== -1) return new Error(location + ' is reserved');
if (RESERVED_LOCATIONS.indexOf(location) !== -1) return new AppsError(AppsError.BAD_FIELD, location + ' is reserved');
if (location === '') return null; // bare location
if ((location.length + 1 /*+ hyphen */ + fqdn.indexOf('.')) > 63) return new Error('Hostname length cannot be greater than 63');
if (location.match(/^[A-Za-z0-9-]+$/) === null) return new Error('Hostname can only contain alphanumerics and hyphen');
if (location[0] === '-' || location[location.length-1] === '-') return new Error('Hostname cannot start or end with hyphen');
if (location.length + 1 /* hyphen */ + fqdn.length > 253) return new Error('FQDN length exceeds 253 characters');
if ((location.length + 1 /*+ hyphen */ + fqdn.indexOf('.')) > 63) return new AppsError(AppsError.BAD_FIELD, 'Hostname length cannot be greater than 63');
if (location.match(/^[A-Za-z0-9-]+$/) === null) return new AppsError(AppsError.BAD_FIELD, 'Hostname can only contain alphanumerics and hyphen');
if (location[0] === '-' || location[location.length-1] === '-') return new AppsError(AppsError.BAD_FIELD, 'Hostname cannot start or end with hyphen');
if (location.length + 1 /* hyphen */ + fqdn.length > 253) return new AppsError(AppsError.BAD_FIELD, 'FQDN length exceeds 253 characters');
return null;
}
@@ -137,10 +146,12 @@ function validatePortBindings(portBindings, tcpPorts) {
2020, /* install server */
config.get('port'), /* app server (lo) */
config.get('sysadminPort'), /* sysadmin app server (lo) */
config.get('smtpPort'), /* internal smtp port (lo) */
config.get('ldapPort'), /* ldap server (lo) */
config.get('oauthProxyPort'), /* oauth proxy server (lo) */
config.get('simpleAuthPort'), /* simple auth server (lo) */
3306, /* mysql (lo) */
4190, /* managesieve */
8000 /* graphite (lo) */
];
@@ -150,8 +161,8 @@ function validatePortBindings(portBindings, tcpPorts) {
for (env in portBindings) {
if (!/^[a-zA-Z0-9_]+$/.test(env)) return new AppsError(AppsError.BAD_FIELD, env + ' is not valid environment variable');
if (!Number.isInteger(portBindings[env])) return new Error(portBindings[env] + ' is not an integer');
if (portBindings[env] <= 0 || portBindings[env] > 65535) return new Error(portBindings[env] + ' is out of range');
if (!Number.isInteger(portBindings[env])) return new AppsError(AppsError.BAD_FIELD, portBindings[env] + ' is not an integer');
if (portBindings[env] <= 0 || portBindings[env] > 65535) return new AppsError(AppsError.BAD_FIELD, portBindings[env] + ' is out of range');
if (RESERVED_PORTS.indexOf(portBindings[env]) !== -1) return new AppsError(AppsError.PORT_RESERVED, String(portBindings[env]));
}
@@ -174,19 +185,20 @@ function validateAccessRestriction(accessRestriction) {
var noUsers = true, noGroups = true;
if (accessRestriction.users) {
if (!Array.isArray(accessRestriction.users)) return new Error('users array property required');
if (!accessRestriction.users.every(function (e) { return typeof e === 'string'; })) return new Error('All users have to be strings');
if (!Array.isArray(accessRestriction.users)) return new AppsError(AppsError.BAD_FIELD, 'users array property required');
if (!accessRestriction.users.every(function (e) { return typeof e === 'string'; })) return new AppsError(AppsError.BAD_FIELD, 'All users have to be strings');
noUsers = accessRestriction.users.length === 0;
}
if (accessRestriction.groups) {
if (!Array.isArray(accessRestriction.groups)) return new Error('groups array property required');
if (!accessRestriction.groups.every(function (e) { return typeof e === 'string'; })) return new Error('All groups have to be strings');
if (!Array.isArray(accessRestriction.groups)) return new AppsError(AppsError.BAD_FIELD, 'groups array property required');
if (!accessRestriction.groups.every(function (e) { return typeof e === 'string'; })) return new AppsError(AppsError.BAD_FIELD, 'All groups have to be strings');
noGroups = accessRestriction.groups.length === 0;
}
if (noUsers && noGroups) return new Error('users and groups array cannot both be empty');
if (noUsers && noGroups) return new AppsError(AppsError.BAD_FIELD, 'users and groups array cannot both be empty');
// TODO: maybe validate if the users and groups actually exist
return null;
}
@@ -201,12 +213,26 @@ function validateMemoryLimit(manifest, memoryLimit) {
// this is needed so an app update can change the value in the manifest, and if not set by the user, the new value should be used
if (memoryLimit === 0) return null;
if (memoryLimit < min) return new Error('memoryLimit too small');
if (memoryLimit > max) return new Error('memoryLimit too large');
if (memoryLimit < min) return new AppsError(AppsError.BAD_FIELD, 'memoryLimit too small');
if (memoryLimit > max) return new AppsError(AppsError.BAD_FIELD, 'memoryLimit too large');
return null;
}
// https://tools.ietf.org/html/rfc7034
function validateXFrameOptions(xFrameOptions) {
assert.strictEqual(typeof xFrameOptions, 'string');
if (xFrameOptions === 'DENY') return null;
if (xFrameOptions === 'SAMEORIGIN') return null;
var parts = xFrameOptions.split(' ');
if (parts.length !== 2 || parts[0] !== 'ALLOW-FROM') return new AppsError(AppsError.BAD_FIELD, 'xFrameOptions must be "DENY", "SAMEORIGIN" or "ALLOW-FROM uri"' );
var uri = url.parse(parts[1]);
return (uri.protocol === 'http:' || uri.protocol === 'https:') ? null : new AppsError(AppsError.BAD_FIELD, 'xFrameOptions ALLOW-FROM uri must be a valid http[s] uri' );
}
function getDuplicateErrorDetails(location, portBindings, error) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof portBindings, 'object');
@@ -229,6 +255,18 @@ function getDuplicateErrorDetails(location, portBindings, error) {
return new AppsError(AppsError.ALREADY_EXISTS);
}
function getAppConfig(app) {
return {
manifest: app.manifest,
location: app.location,
accessRestriction: app.accessRestriction,
portBindings: app.portBindings,
memoryLimit: app.memoryLimit,
xFrameOptions: app.xFrameOptions || 'SAMEORIGIN',
altDomain: app.altDomain
};
}
function getIconUrlSync(app) {
var iconPath = paths.APPICONS_DIR + '/' + app.id + '.png';
return fs.existsSync(iconPath) ? '/api/v1/apps/' + app.id + '/icon' : null;
@@ -342,145 +380,194 @@ function purchase(appStoreId, callback) {
});
}
function install(appId, appStoreId, manifest, location, portBindings, accessRestriction, icon, cert, key, memoryLimit, altDomain, auditSource, 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, 'object');
assert(!icon || typeof icon === 'string');
assert(cert === null || typeof cert === 'string');
assert(key === null || typeof key === 'string');
assert.strictEqual(typeof memoryLimit, 'number');
assert(altDomain === null || typeof altDomain === 'string');
function downloadManifest(appStoreId, manifest, callback) {
if (!appStoreId && !manifest) return callback(new AppsError(AppsError.BAD_FIELD, 'Neither manifest nor appStoreId provided'));
if (!appStoreId) return callback(null, '', manifest);
var parts = appStoreId.split('@');
var url = config.apiServerOrigin() + '/api/v1/apps/' + parts[0] + (parts[1] ? '/versions/' + parts[1] : '');
debug('downloading manifest from %s', url);
superagent.get(url).end(function (error, result) {
if (error && !error.response) return callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Network error downloading manifest:' + error.message));
if (result.statusCode !== 200) return callback(new AppsError(AppsError.BAD_FIELD, util.format('Failed to get app info from store.', result.statusCode, result.text)));
callback(null, parts[0], result.body.manifest);
});
}
function install(data, auditSource, callback) {
assert(data && typeof data === 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
var error = manifestFormat.parse(manifest);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest error: ' + error.message));
var location = data.location.toLowerCase(),
portBindings = data.portBindings || null,
accessRestriction = data.accessRestriction || null,
icon = data.icon || null,
cert = data.cert || null,
key = data.key || null,
memoryLimit = data.memoryLimit || 0,
altDomain = data.altDomain || null,
xFrameOptions = data.xFrameOptions || 'SAMEORIGIN';
error = checkManifestConstraints(manifest);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest cannot be installed: ' + error.message));
assert(data.appStoreId || data.manifest); // atleast one of them is required
error = validateHostname(location, config.fqdn());
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
error = validatePortBindings(portBindings, manifest.tcpPorts);
if (error) return callback(error);
error = validateAccessRestriction(accessRestriction);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
error = validateMemoryLimit(manifest, memoryLimit);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
// memoryLimit might come in as 0 if not specified
memoryLimit = memoryLimit || manifest.memoryLimit || constants.DEFAULT_MEMORY_LIMIT;
if (altDomain !== null && !validator.isFQDN(altDomain)) return callback(new AppsError(AppsError.BAD_FIELD, 'Invalid alt domain'));
// 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'));
if (!safe.fs.writeFileSync(path.join(paths.APPICONS_DIR, appId + '.png'), new Buffer(icon, 'base64'))) {
return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving icon:' + safe.error.message));
}
}
error = certificates.validateCertificate(cert, key, config.appFqdn(location));
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
debug('Will install app with id : ' + appId);
purchase(appStoreId, function (error) {
downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) {
if (error) return callback(error);
appdb.add(appId, appStoreId, manifest, location.toLowerCase(), portBindings, accessRestriction, memoryLimit, altDomain, 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));
error = manifestFormat.parse(manifest);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest error: ' + error.message));
// save cert to data/box/certs
if (cert && key) {
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.cert'), cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.key'), key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
error = checkManifestConstraints(manifest);
if (error) return callback(error);
error = validateHostname(location, config.fqdn());
if (error) return callback(error);
error = validatePortBindings(portBindings, manifest.tcpPorts);
if (error) return callback(error);
error = validateAccessRestriction(accessRestriction);
if (error) return callback(error);
error = validateMemoryLimit(manifest, memoryLimit);
if (error) return callback(error);
error = validateXFrameOptions(xFrameOptions);
if (error) return callback(error);
// memoryLimit might come in as 0 if not specified
memoryLimit = memoryLimit || manifest.memoryLimit || constants.DEFAULT_MEMORY_LIMIT;
if (altDomain !== null && !validator.isFQDN(altDomain)) return callback(new AppsError(AppsError.BAD_FIELD, 'Invalid alt domain'));
// 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));
var appId = uuid.v4();
if (icon) {
if (!validator.isBase64(icon)) return callback(new AppsError(AppsError.BAD_FIELD, 'icon is not base64'));
if (!safe.fs.writeFileSync(path.join(paths.APPICONS_DIR, appId + '.png'), new Buffer(icon, 'base64'))) {
return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving icon:' + safe.error.message));
}
}
taskmanager.restartAppTask(appId);
error = certificates.validateCertificate(cert, key, config.appFqdn(location));
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId: appId, location: location, manifest: manifest });
debug('Will install app with id : ' + appId);
callback(null);
purchase(appStoreId, function (error) {
if (error) return callback(error);
var data = {
accessRestriction: accessRestriction,
memoryLimit: memoryLimit,
altDomain: altDomain,
xFrameOptions: xFrameOptions
};
appdb.add(appId, appStoreId, manifest, location, portBindings, data, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
// save cert to data/box/certs
if (cert && key) {
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.cert'), cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.key'), key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
}
taskmanager.restartAppTask(appId);
eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId: appId, location: location, manifest: manifest });
callback(null, { id : appId });
});
});
});
}
function configure(appId, location, portBindings, accessRestriction, cert, key, memoryLimit, altDomain, auditSource, callback) {
function configure(appId, data, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof portBindings, 'object');
assert.strictEqual(typeof accessRestriction, 'object');
assert(cert === null || typeof cert === 'string');
assert(key === null || typeof key === 'string');
assert.strictEqual(typeof memoryLimit, 'number');
assert(altDomain === null || typeof altDomain === 'string');
assert(data && typeof data === 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
var error = validateHostname(location, config.fqdn());
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
error = validateAccessRestriction(accessRestriction);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
error = certificates.validateCertificate(cert, key, config.appFqdn(location));
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
if (altDomain !== null && !validator.isFQDN(altDomain)) return callback(new AppsError(AppsError.BAD_FIELD, 'Invalid alt domain'));
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
error = validatePortBindings(portBindings, app.manifest.tcpPorts);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
error = validateMemoryLimit(app.manifest, memoryLimit);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
// memoryLimit might come in as 0 if not specified
memoryLimit = memoryLimit || app.memoryLimit || app.manifest.memoryLimit || constants.DEFAULT_MEMORY_LIMIT;
// save cert to data/box/certs
if (cert && key) {
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.cert'), cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.key'), key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
var location, portBindings, values = { };
if ('location' in data) {
location = values.location = data.location.toLowerCase();
error = validateHostname(values.location, config.fqdn());
if (error) return callback(error);
} else {
location = app.location;
}
var values = {
location: location.toLowerCase(),
accessRestriction: accessRestriction,
portBindings: portBindings,
memoryLimit: memoryLimit,
altDomain: altDomain,
if ('accessRestriction' in data) {
values.accessRestriction = data.accessRestriction;
error = validateAccessRestriction(values.accessRestriction);
if (error) return callback(error);
}
oldConfig: {
location: app.location,
accessRestriction: app.accessRestriction,
portBindings: app.portBindings,
memoryLimit: app.memoryLimit,
altDomain: altDomain
if ('altDomain' in data) {
values.altDomain = data.altDomain;
if (values.altDomain !== null && !validator.isFQDN(values.altDomain)) return callback(new AppsError(AppsError.BAD_FIELD, 'Invalid alt domain'));
}
if ('portBindings' in data) {
portBindings = values.portBindings = data.portBindings;
error = validatePortBindings(values.portBindings, app.manifest.tcpPorts);
if (error) return callback(error);
} else {
portBindings = app.portBindings;
}
if ('memoryLimit' in data) {
values.memoryLimit = data.memoryLimit;
error = validateMemoryLimit(app.manifest, values.memoryLimit);
if (error) return callback(error);
// memoryLimit might come in as 0 if not specified
values.memoryLimit = values.memoryLimit || app.memoryLimit || app.manifest.memoryLimit || constants.DEFAULT_MEMORY_LIMIT;
}
if ('xFrameOptions' in data) {
values.xFrameOptions = data.xFrameOptions;
error = validateXFrameOptions(values.xFrameOptions);
if (error) return callback(error);
}
// save cert to data/box/certs. TODO: move this to apptask when we have a real task queue
if ('cert' in data && 'key' in data) {
if (data.cert && data.key) {
error = certificates.validateCertificate(data.cert, data.key, config.appFqdn(location));
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.cert'), data.cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.key'), data.key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
} else { // remove existing cert/key
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.cert'))) debug('Error removing cert: ' + safe.error.message);
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.key'))) debug('Error removing key: ' + safe.error.message);
}
};
}
values.oldConfig = getAppConfig(app);
debug('Will configure app with id:%s values:%j', appId, values);
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_CONFIGURE, values, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location.toLowerCase(), portBindings, error));
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
@@ -493,76 +580,75 @@ function configure(appId, location, portBindings, accessRestriction, cert, key,
});
}
function update(appId, force, manifest, portBindings, icon, auditSource, callback) {
function update(appId, data, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof force, 'boolean');
assert(manifest && typeof manifest === 'object');
assert(typeof portBindings === 'object'); // can be null
assert(!icon || typeof icon === 'string');
assert(data && typeof data === 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug('Will update app with id:%s', appId);
var error = manifestFormat.parse(manifest);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest error:' + error.message));
downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) {
if (error) return callback(error);
error = checkManifestConstraints(manifest);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest cannot be installed:' + error.message));
var values = { };
error = validatePortBindings(portBindings, manifest.tcpPorts);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
error = manifestFormat.parse(manifest);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest error:' + error.message));
if (icon) {
if (!validator.isBase64(icon)) return callback(new AppsError(AppsError.BAD_FIELD, 'icon is not base64'));
error = checkManifestConstraints(manifest);
if (error) return callback(error);
if (!safe.fs.writeFileSync(path.join(paths.APPICONS_DIR, appId + '.png'), new Buffer(icon, 'base64'))) {
return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving icon:' + safe.error.message));
}
}
values.manifest = manifest;
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
var appStoreId = app.appStoreId;
// prevent user from installing a app with different manifest id over an existing app
// this allows cloudron install -f --app <appid> for an app installed from the appStore
if (app.manifest.id !== manifest.id) {
if (!force) return callback(new AppsError(AppsError.BAD_FIELD, 'manifest id does not match. force to override'));
// clear appStoreId so that this app does not get updates anymore. this will mark is a dev app
appStoreId = '';
if ('portBindings' in data) {
values.portBindings = data.portBindings;
error = validatePortBindings(data.portBindings, values.manifest.tcpPorts);
if (error) return callback(error);
}
// Ensure we update the memory limit in case the new app requires more memory as a minimum
var memoryLimit = manifest.memoryLimit ? (app.memoryLimit < manifest.memoryLimit ? manifest.memoryLimit : app.memoryLimit) : app.memoryLimit;
if ('icon' in data) {
if (data.icon) {
if (!validator.isBase64(data.icon)) return callback(new AppsError(AppsError.BAD_FIELD, 'icon is not base64'));
var values = {
appStoreId: appStoreId,
manifest: manifest,
portBindings: portBindings,
memoryLimit: memoryLimit,
oldConfig: {
manifest: app.manifest,
portBindings: app.portBindings,
accessRestriction: app.accessRestriction,
memoryLimit: app.memoryLimit,
altDomain: app.altDomain
if (!safe.fs.writeFileSync(path.join(paths.APPICONS_DIR, appId + '.png'), new Buffer(data.icon, 'base64'))) {
return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving icon:' + safe.error.message));
}
} else {
safe.fs.unlinkSync(path.join(paths.APPICONS_DIR, appId + '.png'));
}
};
}
appdb.setInstallationCommand(appId, force ? appdb.ISTATE_PENDING_FORCE_UPDATE : appdb.ISTATE_PENDING_UPDATE, values, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails('' /* location cannot conflict */, portBindings, error));
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
taskmanager.restartAppTask(appId);
// prevent user from installing a app with different manifest id over an existing app
// this allows cloudron install -f --app <appid> for an app installed from the appStore
if (app.manifest.id !== values.manifest.id) {
if (!data.force) return callback(new AppsError(AppsError.BAD_FIELD, 'manifest id does not match. force to override'));
// clear appStoreId so that this app does not get updates anymore. this will mark it as a dev app
values.appStoreId = '';
}
eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId: appId, toManifest: manifest, fromManifest: app.manifest });
// Ensure we update the memory limit in case the new app requires more memory as a minimum
if (values.manifest.memoryLimit && app.memoryLimit < values.manifest.memoryLimit) {
values.memoryLimit = values.manifest.memoryLimit;
}
callback(null);
values.oldConfig = getAppConfig(app);
appdb.setInstallationCommand(appId, data.force ? appdb.ISTATE_PENDING_FORCE_UPDATE : appdb.ISTATE_PENDING_UPDATE, values, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails('' /* location cannot conflict */, values.portBindings, error));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
taskmanager.restartAppTask(appId);
eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId: appId, toManifest: manifest, fromManifest: app.manifest, force: data.force });
callback(null);
});
});
});
}
@@ -612,8 +698,9 @@ function getLogs(appId, lines, follow, callback) {
});
}
function restore(appId, auditSource, callback) {
function restore(appId, data, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -623,43 +710,100 @@ function restore(appId, auditSource, callback) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
// restore without a backup is the same as re-install
var restoreConfig = app.lastBackupConfig, values = { };
if (restoreConfig) {
// re-validate because this new box version may not accept old configs.
// if we restore location, it should be validated here as well
error = checkManifestConstraints(restoreConfig.manifest);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest cannot be installed: ' + error.message));
// for empty or null backupId, use existing manifest to mimic a reinstall
var func = data.backupId ? backups.getRestoreConfig.bind(null, data.backupId) : function (next) { return next(null, { manifest: app.manifest }); };
error = validatePortBindings(restoreConfig.portBindings, restoreConfig.manifest.tcpPorts); // maybe new ports got reserved now
if (error) return callback(error);
// ## should probably query new location, access restriction from user
values = {
manifest: restoreConfig.manifest,
portBindings: restoreConfig.portBindings,
memoryLimit: restoreConfig.memoryLimit,
oldConfig: {
location: app.location,
accessRestriction: app.accessRestriction,
portBindings: app.portBindings,
memoryLimit: app.memoryLimit,
manifest: app.manifest,
altDomain: app.altDomain
}
};
}
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_RESTORE, values, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
func(function (error, restoreConfig) {
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));
taskmanager.restartAppTask(appId);
if (!restoreConfig) callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Could not get restore config'));
eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { appId: appId });
// re-validate because this new box version may not accept old configs
error = checkManifestConstraints(restoreConfig.manifest);
if (error) return callback(error);
callback(null);
var values = {
lastBackupId: data.backupId || null, // when null, apptask simply reinstalls
manifest: restoreConfig.manifest,
oldConfig: getAppConfig(app)
};
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_RESTORE, values, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
taskmanager.restartAppTask(appId);
eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { appId: appId });
callback(null);
});
});
});
}
function clone(appId, data, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug('Will clone app with id:%s', appId);
var location = data.location.toLowerCase(),
portBindings = data.portBindings || null,
backupId = data.backupId;
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof portBindings, 'object');
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
backups.getRestoreConfig(backupId, function (error, restoreConfig) {
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (!restoreConfig) callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Could not get restore config'));
// re-validate because this new box version may not accept old configs
error = checkManifestConstraints(restoreConfig.manifest);
if (error) return callback(error);
error = validateHostname(location, config.fqdn());
if (error) return callback(error);
error = validatePortBindings(portBindings, restoreConfig.manifest.tcpPorts);
if (error) return callback(error);
var newAppId = uuid.v4(), appStoreId = app.appStoreId, manifest = restoreConfig.manifest;
purchase(appStoreId, function (error) {
if (error) return callback(error);
var data = {
installationState: appdb.ISTATE_PENDING_CLONE,
memoryLimit: app.memoryLimit,
accessRestriction: app.accessRestriction,
xFrameOptions: app.xFrameOptions,
lastBackupId: backupId
};
appdb.add(newAppId, appStoreId, manifest, location, portBindings, data, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
taskmanager.restartAppTask(newAppId);
eventlog.add(eventlog.ACTION_APP_CLONE, auditSource, { appId: newAppId, oldAppId: appId, backupId: backupId, location: location, manifest: manifest });
callback(null, { id : newAppId });
});
});
});
});
}
@@ -716,14 +860,16 @@ function stop(appId, callback) {
}
function checkManifestConstraints(manifest) {
if (!manifest.dockerImage) return new Error('Missing dockerImage'); // dockerImage is optional in manifest
assert(manifest && typeof manifest === 'object');
if (!manifest.dockerImage) return new AppsError(AppsError.BAD_FIELD, 'Missing dockerImage'); // dockerImage is optional in manifest
if (semver.valid(manifest.maxBoxVersion) && semver.gt(config.version(), manifest.maxBoxVersion)) {
return new Error('Box version exceeds Apps maxBoxVersion');
return new AppsError(AppsError.BAD_FIELD, 'Box version exceeds Apps maxBoxVersion');
}
if (semver.valid(manifest.minBoxVersion) && semver.gt(manifest.minBoxVersion, config.version())) {
return new Error('minBoxVersion exceeds Box version');
return new AppsError(AppsError.BAD_FIELD, 'minBoxVersion exceeds Box version');
}
return null;
@@ -747,10 +893,14 @@ function exec(appId, options, callback) {
var container = docker.connection.getContainer(app.containerId);
var execOptions = {
var execOptions = {
AttachStdin: true,
AttachStdout: true,
AttachStderr: true,
// A pseudo tty is a terminal which processes can detect (for example, disable colored output)
// Creating a pseudo terminal also assigns a terminal driver which detects control sequences
// When passing binary data, tty must be disabled. In addition, the stdout/stderr becomes a single
// unified stream because of the nature of a tty (see https://github.com/docker/docker/issues/19696)
Tty: options.tty,
Cmd: cmd
};
@@ -760,9 +910,18 @@ function exec(appId, options, callback) {
var startOptions = {
Detach: false,
Tty: options.tty,
stdin: true // this is a dockerode option that enabled openStdin in the modem
// hijacking upgrades the docker connection from http to tcp. because of this upgrade,
// we can work with half-close connections (not defined in http). this way, the client
// can properly signal that stdin is EOF by closing it's side of the socket. In http,
// the whole connection will be dropped when stdin get EOF.
// https://github.com/apocas/dockerode/commit/b4ae8a03707fad5de893f302e4972c1e758592fe
hijack: true,
stream: true,
stdin: true,
stdout: true,
stderr: true
};
exec.start(startOptions, function(error, stream) {
exec.start(startOptions, function(error, stream /* in hijacked mode, this is a net.socket */) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (options.rows && options.columns) {
@@ -775,23 +934,25 @@ function exec(appId, options, callback) {
});
}
function autoupdateApps(updateInfo, callback) { // updateInfo is { appId -> { manifest } }
function updateApps(updateInfo, auditSource, callback) { // updateInfo is { appId -> { manifest } }
assert.strictEqual(typeof updateInfo, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
function canAutoupdateApp(app, newManifest) {
var tcpPorts = newManifest.tcpPorts || { };
var newTcpPorts = newManifest.tcpPorts || { };
var oldTcpPorts = app.manifest.tcpPorts || { };
var portBindings = app.portBindings; // this is never null
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');
for (var env in tcpPorts) {
if (!(env in portBindings)) return new Error(env + ' is required from user');
for (var env in newTcpPorts) {
if (!(env in oldTcpPorts)) return new Error(env + ' is required from user');
}
// it's fine if one or more keys got removed
for (env in portBindings) {
if (!(env in newTcpPorts)) return new Error(env + ' was in use but new update removes it');
}
// it's fine if one or more (unused) keys got removed
return null;
}
@@ -810,8 +971,12 @@ function autoupdateApps(updateInfo, callback) { // updateInfo is { appId -> { ma
return iteratorDone();
}
update(appId, false /* force */, updateInfo[appId].manifest, app.portBindings,
null /* icon */, { userId: null, username: 'autoupdater' }, function (error) {
var data = {
manifest: updateInfo[appId].manifest,
force: false
};
update(appId, data, auditSource, function (error) {
if (error) debug('Error initiating autoupdate of %s. %s', appId, error.message);
iteratorDone(null);
@@ -858,3 +1023,39 @@ function listBackups(page, perPage, appId, callback) {
});
});
}
function restoreInstalledApps(callback) {
assert.strictEqual(typeof callback, 'function');
appdb.getAll(function (error, apps) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
async.map(apps, function (app, iteratorDone) {
debug('marking %s for restore', app.location || app.id);
appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_RESTORE, { oldConfig: null }, function (error) {
if (error) debug('did not mark %s for restore', app.location || app.id, error);
iteratorDone(); // always succeed
});
}, callback);
});
}
function configureInstalledApps(callback) {
assert.strictEqual(typeof callback, 'function');
appdb.getAll(function (error, apps) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
async.map(apps, function (app, iteratorDone) {
debug('marking %s for reconfigure', app.location || app.id);
appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_CONFIGURE, { oldConfig: null }, function (error) {
if (error) debug('did not mark %s for reconfigure', app.location || app.id, error);
iteratorDone(); // always succeed
});
}, callback);
});
}
+23 -17
View File
@@ -36,15 +36,14 @@ var addons = require('./addons.js'),
async = require('async'),
backups = require('./backups.js'),
certificates = require('./certificates.js'),
clientdb = require('./clientdb.js'),
clients = require('./clients.js'),
config = require('./config.js'),
ClientsError = clients.ClientsError,
database = require('./database.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:apptask'),
docker = require('./docker.js'),
ejs = require('ejs'),
fs = require('fs'),
hat = require('hat'),
manifestFormat = require('cloudron-manifestformat'),
net = require('net'),
nginx = require('./nginx.js'),
@@ -57,7 +56,6 @@ var addons = require('./addons.js'),
superagent = require('superagent'),
sysinfo = require('./sysinfo.js'),
util = require('util'),
uuid = require('node-uuid'),
waitForDns = require('./waitfordns.js'),
_ = require('underscore');
@@ -163,20 +161,18 @@ function allocateOAuthProxyCredentials(app, callback) {
if (!nginx.requiresOAuthProxy(app)) return callback(null);
var id = 'cid-' + uuid.v4();
var clientSecret = hat(256);
var redirectURI = 'https://' + config.appFqdn(app.location);
var scope = 'profile';
clientdb.add(id, app.id, clientdb.TYPE_PROXY, clientSecret, redirectURI, scope, callback);
clients.add(app.id, clients.TYPE_PROXY, redirectURI, scope, callback);
}
function removeOAuthProxyCredentials(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
clientdb.delByAppIdAndType(app.id, clientdb.TYPE_PROXY, function (error) {
if (error && error.reason !== DatabaseError.NOT_FOUND) {
clients.delByAppIdAndType(app.id, clients.TYPE_PROXY, function (error) {
if (error && error.reason !== ClientsError.NOT_FOUND) {
debugApp(app, 'Error removing OAuth client id', error);
return callback(error);
}
@@ -335,7 +331,9 @@ function waitForDnsPropagation(app, callback) {
function waitForAltDomainDnsPropagation(app, callback) {
if (!app.altDomain) return callback(null);
waitForDns(app.altDomain, config.appFqdn(app.location), 'CNAME', callback); // waits forever
// try for 10 minutes before giving up. this allows the user to "reconfigure" the app in the case where
// an app has an external domain and cloudron is migrated to custom domain.
waitForDns(app.altDomain, config.appFqdn(app.location), 'CNAME', { interval: 10000, times: 60 }, callback);
}
// updates the app object and the database
@@ -417,7 +415,7 @@ function install(app, callback) {
updateApp.bind(null, app, { installationProgress: '85, Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Waiting for Alt Domain DNS propagation' }),
updateApp.bind(null, app, { installationProgress: '90, Waiting for External Domain CNAME setup' }),
exports._waitForAltDomainDnsPropagation.bind(null, app), // required when restoring and !lastBackupId
updateApp.bind(null, app, { installationProgress: '95, Configure nginx' }),
@@ -443,7 +441,7 @@ function backup(app, callback) {
async.series([
updateApp.bind(null, app, { installationProgress: '10, Backing up' }),
backups.backupApp.bind(null, app, app.manifest.addons),
backups.backupApp.bind(null, app, app.manifest),
// done!
function (callback) {
@@ -521,7 +519,7 @@ function restore(app, callback) {
updateApp.bind(null, app, { installationProgress: '85, Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Waiting for Alt Domain DNS propagation' }),
updateApp.bind(null, app, { installationProgress: '90, Waiting for External Domain CNAME setup' }),
exports._waitForAltDomainDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '95, Configuring Nginx' }),
@@ -583,7 +581,7 @@ function configure(app, callback) {
updateApp.bind(null, app, { installationProgress: '80, Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '85, Waiting for Alt Domain DNS propagation' }),
updateApp.bind(null, app, { installationProgress: '85, Waiting for External Domain CNAME setup' }),
exports._waitForAltDomainDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Configuring Nginx' }),
@@ -629,7 +627,6 @@ function update(app, callback) {
removeCollectdProfile.bind(null, app),
stopApp.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();
@@ -641,11 +638,14 @@ function update(app, callback) {
if (app.installationState === appdb.ISTATE_PENDING_FORCE_UPDATE) return next(null);
async.series([
updateApp.bind(null, app, { installationProgress: '30, Backup app' }),
backups.backupApp.bind(null, app, app.oldConfig.manifest.addons)
updateApp.bind(null, app, { installationProgress: '30, Backing up app' }),
backups.backupApp.bind(null, app, app.oldConfig.manifest)
], next);
},
// only delete unused addons after backup
addons.teardownAddons.bind(null, app, unusedAddons),
updateApp.bind(null, app, { installationProgress: '45, Downloading icon' }),
downloadIcon.bind(null, app),
@@ -780,6 +780,7 @@ function startTask(appId, callback) {
case appdb.ISTATE_PENDING_BACKUP: return backup(app, callback);
case appdb.ISTATE_INSTALLED: return handleRunCommand(app, callback);
case appdb.ISTATE_PENDING_INSTALL: return install(app, callback);
case appdb.ISTATE_PENDING_CLONE: return restore(app, callback);
case appdb.ISTATE_PENDING_FORCE_UPDATE: return update(app, callback);
case appdb.ISTATE_ERROR:
debugApp(app, 'Internal error. apptask launched with error status.');
@@ -796,6 +797,11 @@ if (require.main === module) {
debug('Apptask for %s', process.argv[2]);
process.on('SIGTERM', function () {
debug('taskmanager sent SIGTERM since it got a new task for this app');
process.exit(0);
});
initialize(function (error) {
if (error) throw error;
+9 -38
View File
@@ -1,5 +1,3 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
@@ -10,17 +8,16 @@ exports = module.exports = {
var assert = require('assert'),
BasicStrategy = require('passport-http').BasicStrategy,
BearerStrategy = require('passport-http-bearer').Strategy,
clientdb = require('./clientdb'),
clients = require('./clients'),
ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy,
ClientsError = clients.ClientsError,
DatabaseError = require('./databaseerror'),
debug = require('debug')('box:auth'),
LocalStrategy = require('passport-local').Strategy,
crypto = require('crypto'),
groups = require('./groups'),
passport = require('passport'),
tokendb = require('./tokendb'),
user = require('./user'),
userdb = require('./userdb'),
UserError = user.UserError,
_ = require('underscore');
@@ -32,7 +29,7 @@ function initialize(callback) {
});
passport.deserializeUser(function(userId, callback) {
userdb.get(userId, function (error, result) {
user.get(userId, function (error, result) {
if (error) return callback(error);
var md5 = crypto.createHash('md5').update(result.email.toLowerCase()).digest('hex');
@@ -67,8 +64,8 @@ function initialize(callback) {
debug('BasicStrategy: detected client id %s instead of username:password', username);
// username is actually client id here
// password is client secret
clientdb.get(username, function (error, client) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
clients.get(username, function (error, client) {
if (error && error.reason === ClientsError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
if (client.clientSecret != password) return callback(null, false);
return callback(null, client);
@@ -85,8 +82,8 @@ function initialize(callback) {
}));
passport.use(new ClientPasswordStrategy(function (clientId, clientSecret, callback) {
clientdb.get(clientId, function(error, client) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
clients.get(clientId, function(error, client) {
if (error && error.reason === ClientsError.NOT_FOUND) return callback(null, false);
if (error) { return callback(error); }
if (client.clientSecret != clientSecret) { return callback(null, false); }
return callback(null, client);
@@ -101,37 +98,12 @@ function initialize(callback) {
// scopes here can define what capabilities that token carries
// passport put the 'info' object into req.authInfo, where we can further validate the scopes
var info = { scope: token.scope };
var tokenType;
if (token.identifier.indexOf(tokendb.PREFIX_DEV) === 0) {
token.identifier = token.identifier.slice(tokendb.PREFIX_DEV.length);
tokenType = tokendb.TYPE_DEV;
} else if (token.identifier.indexOf(tokendb.PREFIX_APP) === 0) {
tokenType = tokendb.TYPE_APP;
return callback(null, { id: token.identifier.slice(tokendb.PREFIX_APP.length), tokenType: tokenType }, info);
} else if (token.identifier.indexOf(tokendb.PREFIX_USER) === 0) {
tokenType = tokendb.TYPE_USER;
token.identifier = token.identifier.slice(tokendb.PREFIX_USER.length);
} else {
// legacy tokens assuming a user access token
tokenType = tokendb.TYPE_USER;
}
userdb.get(token.identifier, function (error, user) {
user.get(token.identifier, function (error, user) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
// amend the tokenType of the token owner
user.tokenType = tokenType;
// amend the admin flag
groups.isMember(groups.ADMIN_GROUP_ID, user.id, function (error, isAdmin) {
if (error) return callback(error);
user.admin = isAdmin;
callback(null, user, info);
});
callback(null, user, info);
});
});
}));
@@ -144,4 +116,3 @@ function uninitialize(callback) {
callback(null);
}
+62 -38
View File
@@ -7,6 +7,7 @@ exports = module.exports = {
getByAppIdPaged: getByAppIdPaged,
getRestoreUrl: getRestoreUrl,
getRestoreConfig: getRestoreConfig,
ensureBackup: ensureBackup,
@@ -36,6 +37,7 @@ var addons = require('./addons.js'),
safe = require('safetydance'),
shell = require('./shell.js'),
settings = require('./settings.js'),
superagent = require('superagent'),
util = require('util'),
webhooks = require('./webhooks.js');
@@ -135,12 +137,13 @@ function getBoxBackupCredentials(appBackupIds, callback) {
});
}
function getAppBackupCredentials(app, callback) {
function getAppBackupCredentials(app, manifest, callback) {
assert.strictEqual(typeof app, 'object');
assert(manifest && typeof manifest === 'object');
assert.strictEqual(typeof callback, 'function');
var now = new Date();
var filebase = util.format('appbackup_%s_%s-v%s', app.id, now.toISOString(), app.manifest.version);
var filebase = util.format('appbackup_%s_%s-v%s', app.id, now.toISOString(), manifest.version);
var configFilename = filebase + '.json', dataFilename = filebase + '.tar.gz';
settings.getBackupConfig(function (error, backupConfig) {
@@ -161,6 +164,32 @@ function getAppBackupCredentials(app, callback) {
});
}
// backupId is the s3 filename. appbackup_%s_%s-v%s.tar.gz
function getRestoreConfig(backupId, callback) {
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof callback, 'function');
var configFile = backupId.replace(/\.tar\.gz$/, '.json');
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
api(backupConfig.provider).getRestoreUrl(backupConfig, configFile, function (error, result) {
if (error) return callback(error);
superagent.get(result.url).buffer(true).end(function (error, response) {
if (error && !error.response) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
if (response.statusCode !== 200) return callback(new Error('Invalid response code when getting config.json : ' + response.statusCode));
var config = safe.JSON.parse(response.text);
if (!config) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Error in config:' + safe.error.message));
return callback(null, config);
});
});
});
}
// backupId is the s3 filename. appbackup_%s_%s-v%s.tar.gz
function getRestoreUrl(backupId, callback) {
assert.strictEqual(typeof backupId, 'string');
@@ -185,14 +214,15 @@ function getRestoreUrl(backupId, callback) {
});
}
function copyLastBackup(app, callback) {
function copyLastBackup(app, manifest, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof app.lastBackupId, 'string');
assert(manifest && typeof manifest === 'object');
assert.strictEqual(typeof callback, 'function');
var now = new Date();
var toFilenameArchive = util.format('appbackup_%s_%s-v%s.tar.gz', app.id, now.toISOString(), app.manifest.version);
var toFilenameConfig = util.format('appbackup_%s_%s-v%s.json', app.id, now.toISOString(), app.manifest.version);
var toFilenameArchive = util.format('appbackup_%s_%s-v%s.tar.gz', app.id, now.toISOString(), manifest.version);
var toFilenameConfig = util.format('appbackup_%s_%s-v%s.json', app.id, now.toISOString(), manifest.version);
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
@@ -225,7 +255,8 @@ function backupBoxWithAppBackupIds(appBackupIds, callback) {
debug('backupBoxWithAppBackupIds: %j', result);
var args = [ result.s3Url, result.accessKeyId, result.secretAccessKey, result.sessionToken, result.region, result.backupKey ];
var args = [ result.s3Url, result.accessKeyId, result.secretAccessKey, result.region, result.backupKey ];
if (result.sessionToken) args.push(result.sessionToken);
shell.sudo('backupBox', [ BACKUP_BOX_CMD ].concat(args), function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
@@ -267,11 +298,12 @@ function canBackupApp(app) {
// 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 reuseOldAppBackup(app, callback) {
function reuseOldAppBackup(app, manifest, callback) {
assert.strictEqual(typeof app.lastBackupId, 'string');
assert(manifest && typeof manifest === 'object');
assert.strictEqual(typeof callback, 'function');
copyLastBackup(app, function (error, newBackupId) {
copyLastBackup(app, manifest, function (error, newBackupId) {
if (error) return callback(error);
debugApp(app, 'reuseOldAppBackup: reused old backup %s as %s', app.lastBackupId, newBackupId);
@@ -280,28 +312,28 @@ function reuseOldAppBackup(app, callback) {
});
}
function createNewAppBackup(app, addonsToBackup, callback) {
function createNewAppBackup(app, manifest, callback) {
assert.strictEqual(typeof app, 'object');
assert(!addonsToBackup || typeof addonsToBackup, 'object');
assert(manifest && typeof manifest === 'object');
assert.strictEqual(typeof callback, 'function');
getAppBackupCredentials(app, function (error, result) {
getAppBackupCredentials(app, manifest, function (error, result) {
if (error) return callback(error);
debugApp(app, 'createNewAppBackup: backup url:%s backup config url:%s', result.s3DataUrl, result.s3ConfigUrl);
var args = [ app.id, result.s3ConfigUrl, result.s3DataUrl, result.accessKeyId, result.secretAccessKey,
result.sessionToken, result.region, result.backupKey ];
var args = [ app.id, result.s3ConfigUrl, result.s3DataUrl, result.accessKeyId, result.secretAccessKey, result.region, result.backupKey ];
if (result.sessionToken) args.push(result.sessionToken);
async.series([
addons.backupAddons.bind(null, app, addonsToBackup),
addons.backupAddons.bind(null, app, manifest.addons),
shell.sudo.bind(null, 'backupApp', [ BACKUP_APP_CMD ].concat(args))
], function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
debugApp(app, 'createNewAppBackup: %s done', result.id);
backupdb.add({ id: result.id, version: app.manifest.version, type: backupdb.BACKUP_TYPE_APP, dependsOn: [ ] }, function (error) {
backupdb.add({ id: result.id, version: manifest.version, type: backupdb.BACKUP_TYPE_APP, dependsOn: [ ] }, function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
callback(null, result.id);
@@ -310,13 +342,12 @@ function createNewAppBackup(app, addonsToBackup, callback) {
});
}
function setRestorePoint(appId, lastBackupId, lastBackupConfig, callback) {
function setRestorePoint(appId, lastBackupId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof lastBackupId, 'string');
assert.strictEqual(typeof lastBackupConfig, 'object');
assert.strictEqual(typeof callback, 'function');
appdb.update(appId, { lastBackupId: lastBackupId, lastBackupConfig: lastBackupConfig }, function (error) {
appdb.update(appId, { lastBackupId: lastBackupId }, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new BackupsError(BackupsError.NOT_FOUND, 'No such app'));
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
@@ -324,12 +355,12 @@ function setRestorePoint(appId, lastBackupId, lastBackupConfig, callback) {
});
}
function backupApp(app, addonsToBackup, callback) {
function backupApp(app, manifest, callback) {
assert.strictEqual(typeof app, 'object');
assert(!addonsToBackup || typeof addonsToBackup, 'object');
assert(manifest && typeof manifest === 'object');
assert.strictEqual(typeof callback, 'function');
var appConfig = null, backupFunction;
var backupFunction;
if (!canBackupApp(app)) {
if (!app.lastBackupId) {
@@ -337,17 +368,11 @@ function backupApp(app, addonsToBackup, callback) {
return callback(new BackupsError(BackupsError.BAD_STATE, 'App not healthy and never backed up previously'));
}
appConfig = app.lastBackupConfig;
backupFunction = reuseOldAppBackup.bind(null, app);
backupFunction = reuseOldAppBackup.bind(null, app, manifest);
} else {
appConfig = {
manifest: app.manifest,
location: app.location,
portBindings: app.portBindings,
accessRestriction: app.accessRestriction,
memoryLimit: app.memoryLimit
};
backupFunction = createNewAppBackup.bind(null, app, addonsToBackup);
var appConfig = apps.getAppConfig(app);
appConfig.manifest = manifest;
backupFunction = createNewAppBackup.bind(null, app, manifest);
if (!safe.fs.writeFileSync(path.join(paths.DATA_DIR, app.id + '/config.json'), JSON.stringify(appConfig), 'utf8')) {
return callback(safe.error);
@@ -359,7 +384,7 @@ function backupApp(app, addonsToBackup, callback) {
debugApp(app, 'backupApp: successful id:%s', backupId);
setRestorePoint(app.id, backupId, appConfig, function (error) {
setRestorePoint(app.id, backupId, function (error) {
if (error) return callback(error);
return callback(null, backupId);
@@ -386,7 +411,7 @@ function backupBoxAndApps(auditSource, callback) {
async.mapSeries(allApps, function iterator(app, iteratorCallback) {
++processed;
backupApp(app, app.manifest.addons, function (error, backupId) {
backupApp(app, app.manifest, function (error, backupId) {
if (error && error.reason !== BackupsError.BAD_STATE) {
debugApp(app, 'Unable to backup', error);
return iteratorCallback(error);
@@ -425,7 +450,7 @@ function backup(auditSource, callback) {
progress.set(progress.BACKUP, 0, 'Starting'); // ensure tools can 'wait' on progress
backupBoxAndApps(auditSource, function (error) { // start the backup operation in the background
if (error) console.error('backup failed.', error);
if (error) debug('backup failed.', error);
locker.unlock(locker.OP_FULL_BACKUP);
});
@@ -433,8 +458,8 @@ function backup(auditSource, callback) {
callback(null);
}
function ensureBackup(callback) {
callback = callback || NOOP_CALLBACK;
function ensureBackup(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
getPaged(1, 1, function (error, backups) {
if (error) {
@@ -447,8 +472,7 @@ function ensureBackup(callback) {
return callback(null);
}
var eventSource = { userId: null, username: 'cron' };
backup(eventSource, callback);
backup(auditSource, callback);
});
}
+4 -1
View File
@@ -19,7 +19,10 @@ var CA_PROD = 'https://acme-v01.api.letsencrypt.org',
LE_AGREEMENT = 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf';
exports = module.exports = {
getCertificate: getCertificate
getCertificate: getCertificate,
// testing
_name: 'acme'
};
function AcmeError(reason, errorOrMessage) {
+4 -1
View File
@@ -1,7 +1,10 @@
'use strict';
exports = module.exports = {
getCertificate: getCertificate
getCertificate: getCertificate,
// testing
_name: 'caas'
};
var assert = require('assert'),
+29 -23
View File
@@ -2,13 +2,16 @@
exports = module.exports = {
installAdminCertificate: installAdminCertificate,
autoRenew: autoRenew,
renewAll: renewAll,
setFallbackCertificate: setFallbackCertificate,
setAdminCertificate: setAdminCertificate,
CertificatesError: CertificatesError,
validateCertificate: validateCertificate,
ensureCertificate: ensureCertificate,
getAdminCertificatePath: getAdminCertificatePath
getAdminCertificatePath: getAdminCertificatePath,
// exported for testing
_getApi: getApi
};
var acme = require('./cert/acme.js'),
@@ -16,7 +19,6 @@ var acme = require('./cert/acme.js'),
assert = require('assert'),
async = require('async'),
caas = require('./cert/caas.js'),
cloudron = require('./cloudron.js'),
config = require('./config.js'),
constants = require('./constants.js'),
debug = require('debug')('box:src/certificates'),
@@ -34,8 +36,6 @@ var acme = require('./cert/acme.js'),
waitForDns = require('./waitfordns.js'),
x509 = require('x509');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function CertificatesError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
@@ -66,17 +66,22 @@ function getApi(app, callback) {
settings.getTlsConfig(function (error, tlsConfig) {
if (error) return callback(error);
var api = !app.altDomain && tlsConfig.provider === 'caas' ? caas : acme;
// use acme if we have altDomain or the tlsConfig is not caas
var api = (app.altDomain || tlsConfig.provider) !== 'caas' ? acme : caas;
var options = { };
options.prod = tlsConfig.provider.match(/.*-prod/) !== null;
if (tlsConfig.provider === 'caas') {
options.prod = !config.isDev(); // with altDomain, we will choose acme setting based on this
} else { // acme
options.prod = tlsConfig.provider.match(/.*-prod/) !== null;
}
// registering user with an email requires A or MX record (https://github.com/letsencrypt/boulder/issues/1197)
// we cannot use admin@fqdn because the user might not have set it up.
// we simply update the account with the latest email we have each time when getting letsencrypt certs
// https://github.com/ietf-wg-acme/acme/issues/30
user.getOwner(function (error, owner) {
options.email = error ? 'admin@cloudron.io' : owner.email; // can error if not activated yet
options.email = error ? 'support@cloudron.io' : owner.email; // can error if not activated yet
callback(null, api, options);
});
@@ -84,8 +89,6 @@ function getApi(app, callback) {
}
function installAdminCertificate(callback) {
if (cloudron.isConfiguredSync()) return callback();
settings.getTlsConfig(function (error, tlsConfig) {
if (error) return callback(error);
@@ -94,8 +97,8 @@ function installAdminCertificate(callback) {
sysinfo.getIp(function (error, ip) {
if (error) return callback(error);
waitForDns(config.adminFqdn(), ip, 'A', function (error) {
if (error) return callback(error); // this cannot happen because we retry forever
waitForDns(config.adminFqdn(), ip, 'A', { interval: 30000, times: 50000 }, function (error) {
if (error) return callback(error);
ensureCertificate({ location: constants.ADMIN_LOCATION }, function (error, certFilePath, keyFilePath) {
if (error) { // currently, this can never happen
@@ -123,9 +126,11 @@ function isExpiringSync(certFilePath, hours) {
return result.status === 1; // 1 - expired 0 - not expired
}
function autoRenew(callback) {
debug('autoRenew: Checking certificates for renewal');
callback = callback || NOOP_CALLBACK;
function renewAll(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug('renewAll: Checking certificates for renewal');
apps.getAll(function (error, allApps) {
if (error) return callback(error);
@@ -139,7 +144,7 @@ function autoRenew(callback) {
var keyFilePath = path.join(paths.APP_CERTS_DIR, appDomain + '.key');
if (!safe.fs.existsSync(keyFilePath)) {
debug('autoRenew: no existing key file for %s. skipping', appDomain);
debug('renewAll: no existing key file for %s. skipping', appDomain);
continue;
}
@@ -148,7 +153,7 @@ function autoRenew(callback) {
}
}
debug('autoRenew: %j needs to be renewed', expiringApps.map(function (a) { return a.altDomain || config.appFqdn(a.location); }));
debug('renewAll: %j needs to be renewed', expiringApps.map(function (a) { return a.altDomain || config.appFqdn(a.location); }));
async.eachSeries(expiringApps, function iterator(app, iteratorCallback) {
var domain = app.altDomain || config.appFqdn(app.location);
@@ -156,28 +161,29 @@ function autoRenew(callback) {
getApi(app, function (error, api, apiOptions) {
if (error) return callback(error);
debug('autoRenew: renewing cert for %s with options %j', domain, apiOptions);
debug('renewAll: renewing cert for %s with options %j', domain, apiOptions);
api.getCertificate(domain, apiOptions, function (error) {
var certFilePath = path.join(paths.APP_CERTS_DIR, domain + '.cert');
var keyFilePath = path.join(paths.APP_CERTS_DIR, domain + '.key');
var errorMessage = error ? error.message : '';
eventlog.add(eventlog.ACTION_CERTIFICATE_RENEWAL, { userId: null, username: 'cron' }, { domain: domain, errorMessage: errorMessage });
mailer.certificateRenewed(domain, errorMessage);
eventlog.add(eventlog.ACTION_CERTIFICATE_RENEWAL, auditSource, { domain: domain, errorMessage: errorMessage });
if (error) {
debug('autoRenew: could not renew cert for %s because %s', domain, error);
debug('renewAll: could not renew cert for %s because %s', domain, error);
mailer.certificateRenewalError(domain, errorMessage);
// check if we should fallback if we expire in the coming day
if (!isExpiringSync(certFilePath, 24 * 1)) return iteratorCallback();
debug('autoRenew: using fallback certs for %s since it expires soon', domain, error);
debug('renewAll: using fallback certs for %s since it expires soon', domain, error);
certFilePath = 'cert/host.cert';
keyFilePath = 'cert/host.key';
} else {
debug('autoRenew: certificate for %s renewed', domain);
debug('renewAll: certificate for %s renewed', domain);
}
// reconfigure and reload nginx. this is required for the case where we got a renewed cert after fallback
+19 -14
View File
@@ -5,6 +5,7 @@
exports = module.exports = {
get: get,
getAll: getAll,
getAllWithTokenCount: getAllWithTokenCount,
getAllWithTokenCountByIdentifier: getAllWithTokenCountByIdentifier,
add: add,
del: del,
@@ -14,13 +15,7 @@ exports = module.exports = {
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'
_clear: clear
};
var assert = require('assert'),
@@ -52,14 +47,24 @@ function getAll(callback) {
});
}
function getAllWithTokenCount(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + CLIENTS_FIELDS_PREFIXED + ',COUNT(tokens.clientId) AS tokenCount FROM clients LEFT OUTER JOIN tokens ON clients.id=tokens.clientId GROUP BY clients.id', [], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null, results);
});
}
function getAllWithTokenCountByIdentifier(identifier, callback) {
assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + CLIENTS_FIELDS_PREFIXED + ',COUNT(tokens.clientId) AS tokenCount FROM clients LEFT OUTER JOIN tokens ON clients.id=tokens.clientId WHERE tokens.identifier=? GROUP BY clients.id', [ identifier ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null, results);
callback(null, results);
});
}
@@ -71,7 +76,7 @@ function getByAppId(appId, callback) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
return callback(null, result[0]);
callback(null, result[0]);
});
}
@@ -84,7 +89,7 @@ function getByAppIdAndType(appId, type, callback) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
return callback(null, result[0]);
callback(null, result[0]);
});
}
@@ -127,7 +132,7 @@ function delByAppId(appId, callback) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
return callback(null);
callback(null);
});
}
@@ -140,17 +145,17 @@ function delByAppIdAndType(appId, type, callback) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
return callback(null);
callback(null);
});
}
function clear(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM clients WHERE appId!="webadmin"', function (error) {
database.query('DELETE FROM clients WHERE id!="cid-webadmin" AND id!="cid-sdk" AND id!="cid-cli"', function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
return callback(null);
callback(null);
});
}
+130 -25
View File
@@ -6,16 +6,32 @@ exports = module.exports = {
add: add,
get: get,
del: del,
getAllWithDetailsByUserId: getAllWithDetailsByUserId,
getAll: getAll,
getByAppIdAndType: getByAppIdAndType,
getClientTokensByUserId: getClientTokensByUserId,
delClientTokensByUserId: delClientTokensByUserId,
delByAppIdAndType: delByAppIdAndType,
addClientTokenByUserId: addClientTokenByUserId,
delToken: delToken,
// keep this in sync with start.sh ADMIN_SCOPES that generates the cid-webadmin
SCOPE_APPS: 'apps',
SCOPE_DEVELOPER: 'developer',
SCOPE_PROFILE: 'profile',
SCOPE_ROOT: 'root',
SCOPE_CLOUDRON: 'cloudron',
SCOPE_SETTINGS: 'settings',
SCOPE_USERS: 'users'
SCOPE_USERS: 'users',
// roles are handled just like the above scopes, they are parallel to scopes
// scopes enclose API groups, roles specify the usage role
SCOPE_ROLE_SDK: 'roleSdk',
// client type enums
TYPE_EXTERNAL: 'external',
TYPE_BUILT_IN: 'built-in',
TYPE_OAUTH: 'addon-oauth',
TYPE_SIMPLE_AUTH: 'addon-simpleauth',
TYPE_PROXY: 'addon-proxy'
};
var assert = require('assert'),
@@ -23,7 +39,6 @@ var assert = require('assert'),
hat = require('hat'),
appdb = require('./appdb.js'),
tokendb = require('./tokendb.js'),
constants = require('./constants.js'),
async = require('async'),
clientdb = require('./clientdb.js'),
DatabaseError = require('./databaseerror.js'),
@@ -50,6 +65,22 @@ function ClientsError(reason, errorOrMessage) {
util.inherits(ClientsError, Error);
ClientsError.INVALID_SCOPE = 'Invalid scope';
ClientsError.INVALID_CLIENT = 'Invalid client';
ClientsError.INVALID_TOKEN = 'Invalid token';
ClientsError.BAD_FIELD = 'Bad field';
ClientsError.NOT_FOUND = 'Not found';
ClientsError.INTERNAL_ERROR = 'Internal Error';
ClientsError.NOT_ALLOWED = 'Not allowed to remove this client';
function validateName(name) {
assert.strictEqual(typeof name, 'string');
if (name.length < 1) return new ClientsError(ClientsError.BAD_FIELD, 'Name must be atleast 1 character');
if (name.length > 128) return new ClientsError(ClientsError.BAD_FIELD, 'Name too long');
if (/[^a-zA-Z0-9\-]/.test(name)) return new ClientsError(ClientsError.BAD_FIELD, 'Username can only contain alphanumerals and dash');
return null;
}
function validateScope(scope) {
assert.strictEqual(typeof scope, 'string');
@@ -58,16 +89,17 @@ function validateScope(scope) {
exports.SCOPE_APPS,
exports.SCOPE_DEVELOPER,
exports.SCOPE_PROFILE,
exports.SCOPE_ROOT,
exports.SCOPE_CLOUDRON,
exports.SCOPE_SETTINGS,
exports.SCOPE_USERS
exports.SCOPE_USERS,
'*', // includes all scopes, but not roles
exports.SCOPE_ROLE_SDK
];
if (scope === '') return new ClientsError(ClientsError.INVALID_SCOPE);
if (scope === '*') return null;
if (scope === '') return new ClientsError(ClientsError.INVALID_SCOPE, 'Empty scope not allowed');
var allValid = scope.split(',').every(function (s) { return VALID_SCOPES.indexOf(s) !== -1; });
if (!allValid) return new ClientsError(ClientsError.INVALID_SCOPE);
if (!allValid) return new ClientsError(ClientsError.INVALID_SCOPE, 'Invalid scope. Available scopes are ' + VALID_SCOPES.join(', '));
return null;
}
@@ -79,11 +111,18 @@ function add(appId, type, redirectURI, scope, callback) {
assert.strictEqual(typeof scope, 'string');
assert.strictEqual(typeof callback, 'function');
// allow whitespace
scope = scope.split(',').map(function (s) { return s.trim(); }).join(',');
var error = validateScope(scope);
if (error) return callback(error);
// appId is also client name
error = validateName(appId);
if (error) return callback(error);
var id = 'cid-' + uuid.v4();
var clientSecret = hat(256);
var clientSecret = hat(8 * 128);
clientdb.add(id, appId, type, clientSecret, redirectURI, scope, function (error) {
if (error) return callback(error);
@@ -106,6 +145,7 @@ function get(id, callback) {
assert.strictEqual(typeof callback, 'function');
clientdb.get(id, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new ClientsError(ClientsError.NOT_FOUND, 'No such client'));
if (error) return callback(error);
callback(null, result);
});
@@ -116,24 +156,24 @@ function del(id, callback) {
assert.strictEqual(typeof callback, 'function');
clientdb.del(id, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new ClientsError(ClientsError.NOT_FOUND, 'No such client'));
if (error) return callback(error);
callback(null, result);
});
}
function getAllWithDetailsByUserId(userId, callback) {
assert.strictEqual(typeof userId, 'string');
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
clientdb.getAllWithTokenCountByIdentifier(tokendb.PREFIX_USER + userId, function (error, results) {
clientdb.getAll(function (error, results) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, []);
if (error) return callback(error);
var tmp = [];
async.each(results, function (record, callback) {
if (record.type === clientdb.TYPE_ADMIN) {
record.name = constants.ADMIN_NAME;
record.location = constants.ADMIN_LOCATION;
if (record.type === exports.TYPE_EXTERNAL || record.type === exports.TYPE_BUILT_IN) {
// the appId in this case holds the name
record.name = record.appId;
tmp.push(record);
@@ -142,14 +182,13 @@ function getAllWithDetailsByUserId(userId, callback) {
appdb.get(record.appId, function (error, result) {
if (error) {
console.error('Failed to get app details for oauth client', result, error);
console.error('Failed to get app details for oauth client', record.appId, error);
return callback(null); // ignore error so we continue listing clients
}
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';
if (record.type === exports.TYPE_PROXY) record.name = result.manifest.title + ' Website Proxy';
if (record.type === exports.TYPE_OAUTH) record.name = result.manifest.title + ' OAuth';
if (record.type === exports.TYPE_SIMPLE_AUTH) record.name = result.manifest.title + ' Simple Auth';
record.location = result.location;
@@ -164,15 +203,27 @@ function getAllWithDetailsByUserId(userId, callback) {
});
}
function getByAppIdAndType(appId, type, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
clientdb.getByAppIdAndType(appId, type, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new ClientsError(ClientsError.NOT_FOUND, 'No such client'));
if (error) return callback(error);
callback(null, result);
});
}
function getClientTokensByUserId(clientId, userId, callback) {
assert.strictEqual(typeof clientId, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
tokendb.getByIdentifierAndClientId(tokendb.PREFIX_USER + userId, clientId, function (error, result) {
tokendb.getByIdentifierAndClientId(userId, clientId, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) {
// this can mean either that there are no tokens or the clientId is actually unknown
clientdb.get(clientId, function (error/*, result*/) {
get(clientId, function (error/*, result*/) {
if (error) return callback(error);
callback(null, []);
});
@@ -188,10 +239,10 @@ function delClientTokensByUserId(clientId, userId, callback) {
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
tokendb.delByIdentifierAndClientId(tokendb.PREFIX_USER + userId, clientId, function (error) {
tokendb.delByIdentifierAndClientId(userId, clientId, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) {
// this can mean either that there are no tokens or the clientId is actually unknown
clientdb.get(clientId, function (error/*, result*/) {
get(clientId, function (error/*, result*/) {
if (error) return callback(error);
callback(null);
});
@@ -201,3 +252,57 @@ function delClientTokensByUserId(clientId, userId, callback) {
callback(null);
});
}
function delByAppIdAndType(appId, type, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
clientdb.delByAppIdAndType(appId, type, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new ClientsError(ClientsError.NOT_FOUND, 'No such client'));
if (error) return callback(error);
callback(null);
});
}
function addClientTokenByUserId(clientId, userId, expiresAt, callback) {
assert.strictEqual(typeof clientId, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof expiresAt, 'number');
assert.strictEqual(typeof callback, 'function');
get(clientId, function (error, result) {
if (error) return callback(error);
var token = tokendb.generateToken();
tokendb.add(token, userId, result.id, expiresAt, result.scope, function (error) {
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
callback(null, {
accessToken: token,
identifier: userId,
clientId: result.id,
scope: result.id,
expires: expiresAt
});
});
});
}
function delToken(clientId, tokenId, callback) {
assert.strictEqual(typeof clientId, 'string');
assert.strictEqual(typeof tokenId, 'string');
assert.strictEqual(typeof callback, 'function');
get(clientId, function (error, result) {
if (error) return callback(error);
tokendb.del(tokenId, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new ClientsError(ClientsError.INVALID_TOKEN, 'Invalid token'));
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
callback(null);
});
});
}
+126 -67
View File
@@ -12,9 +12,9 @@ exports = module.exports = {
sendHeartbeat: sendHeartbeat,
updateToLatest: updateToLatest,
update: update,
reboot: reboot,
retire: retire,
migrate: migrate,
isConfiguredSync: isConfiguredSync,
@@ -31,8 +31,9 @@ var apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
backups = require('./backups.js'),
clientdb = require('./clientdb.js'),
clients = require('./clients.js'),
config = require('./config.js'),
constants = require('./constants.js'),
debug = require('debug')('box:cloudron'),
df = require('node-df'),
eventlog = require('./eventlog.js'),
@@ -45,6 +46,7 @@ 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'),
@@ -53,9 +55,9 @@ var apps = require('./apps.js'),
updateChecker = require('./updatechecker.js'),
user = require('./user.js'),
UserError = user.UserError,
userdb = require('./userdb.js'),
user = require('./user.js'),
util = require('util'),
uuid = require('node-uuid');
_ = require('underscore');
var REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'),
INSTALLER_UPDATE_URL = 'http://127.0.0.1:2020/api/v1/installer/update',
@@ -63,9 +65,21 @@ var REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'),
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
// result to not depend on the appstore
const BOX_AND_USER_TEMPLATE = {
box: {
region: null,
size: null,
plan: 'Custom Plan'
},
user: {
billing: false,
currency: ''
}
};
var gUpdatingDns = false, // flag for dns update reentrancy
gCloudronDetails = null, // cached cloudron details like region,size...
gAppstoreUserDetails = {},
gBoxAndUserDetails = null, // cached cloudron details like region,size...
gIsConfigured = null; // cached configured state so that return value is synchronous. null means we are not initialized yet
function CloudronError(reason, errorOrMessage) {
@@ -91,13 +105,10 @@ CloudronError.BAD_FIELD = 'Field error';
CloudronError.INTERNAL_ERROR = 'Internal Error';
CloudronError.EXTERNAL_ERROR = 'External Error';
CloudronError.ALREADY_PROVISIONED = 'Already Provisioned';
CloudronError.BAD_USERNAME = 'Bad username';
CloudronError.BAD_EMAIL = 'Bad email';
CloudronError.BAD_PASSWORD = 'Bad password';
CloudronError.BAD_NAME = 'Bad name';
CloudronError.BAD_STATE = 'Bad state';
CloudronError.ALREADY_UPTODATE = 'No Update Available';
CloudronError.NOT_FOUND = 'Not found';
CloudronError.SELF_UPGRADE_NOT_SUPPORTED = 'Self upgrade not supported';
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -231,19 +242,17 @@ function activate(username, password, email, displayName, ip, auditSource, callb
user.createOwner(username, password, email, displayName, auditSource, 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 && error.reason === UserError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_FIELD, error.message));
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
clientdb.getByAppIdAndType('webadmin', clientdb.TYPE_ADMIN, function (error, result) {
clients.get('cid-webadmin', function (error, result) {
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) {
tokendb.add(token, userObject.id, result.id, expires, '*', function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
// EE API is sync. do not keep the REST API reponse waiting
@@ -260,7 +269,7 @@ function activate(username, password, email, displayName, ip, auditSource, callb
function getStatus(callback) {
assert.strictEqual(typeof callback, 'function');
userdb.count(function (error, count) {
user.count(function (error, count) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
settings.getCloudronName(function (error, cloudronName) {
@@ -278,47 +287,34 @@ function getStatus(callback) {
});
}
function getCloudronDetails(callback) {
function getBoxAndUserDetails(callback) {
assert.strictEqual(typeof callback, 'function');
if (gCloudronDetails) return callback(null, gCloudronDetails);
if (gBoxAndUserDetails) return callback(null, gBoxAndUserDetails);
if (!config.token()) {
gCloudronDetails = {
region: null,
size: null
};
return callback(null, gCloudronDetails);
}
// only supported for caas
if (config.provider() !== 'caas') return callback(null, {});
superagent
.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn())
.query({ token: config.token() })
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 200) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
if (error && !error.response) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, 'Cannot reach appstore'));
if (result.statusCode !== 200) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
gCloudronDetails = result.body.box;
gAppstoreUserDetails = result.body.user;
gBoxAndUserDetails = result.body;
return callback(null, gCloudronDetails);
return callback(null, gBoxAndUserDetails);
});
}
function getConfig(callback) {
assert.strictEqual(typeof callback, 'function');
getCloudronDetails(function (error, result) {
if (error) {
debug('Failed to fetch cloudron details.', error);
getBoxAndUserDetails(function (error, result) {
if (error) debug('Failed to fetch cloudron details.', error.reason, error.message);
// set fallback values to avoid dependency on appstore
result = {
region: result ? result.region : null,
size: result ? result.size : null
};
}
result = _.extend(BOX_AND_USER_TEMPLATE, result || {});
settings.getCloudronName(function (error, cloudronName) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
@@ -340,9 +336,11 @@ function getConfig(callback) {
progress: progress.get(),
isCustomDomain: config.isCustomDomain(),
developerMode: developerMode,
region: result.region,
size: result.size,
billing: !!gAppstoreUserDetails.billing,
region: result.box.region,
size: result.box.size,
billing: !!result.user.billing,
plan: result.box.plan,
currency: result.user.currency,
memory: os.totalmem(),
provider: config.provider(),
cloudronName: cloudronName
@@ -444,24 +442,28 @@ function addDnsRecords() {
sysinfo.getIp(function (error, ip) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
var webadminRecord = { subdomain: 'my', type: 'A', values: [ ip ] };
var webadminRecord = { subdomain: constants.ADMIN_LOCATION, type: 'A', values: [ ip ] };
// t=s limits the domainkey to this domain and not it's subdomains
var dkimRecord = { subdomain: DKIM_SELECTOR + '._domainkey', type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] };
// DMARC requires special setup if report email id is in different domain
var dmarcRecord = { subdomain: '_dmarc', type: 'TXT', values: [ '"v=DMARC1; p=none; pct=100; rua=mailto:' + DMARC_REPORT_EMAIL + '; ruf=' + DMARC_REPORT_EMAIL + '"' ] };
var mxRecord = { subdomain: '', type: 'MX', values: [ '10 ' + config.mailFqdn() + '.' ] };
var records = [ ];
if (config.isCustomDomain()) {
records.push(webadminRecord);
records.push(dkimRecord);
records.push(mxRecord);
} else {
// for custom domains, we show a nakeddomain.html page
// for non-custom domains, we show a nakeddomain.html page
var nakedDomainRecord = { subdomain: '', type: 'A', values: [ ip ] };
records.push(nakedDomainRecord);
records.push(webadminRecord);
records.push(dkimRecord);
records.push(dmarcRecord);
records.push(mxRecord);
}
debug('addDnsRecords: %j', records);
@@ -497,8 +499,9 @@ function reboot(callback) {
shell.sudo('reboot', [ REBOOT_CMD ], callback);
}
function update(boxUpdateInfo, callback) {
function update(boxUpdateInfo, auditSource, callback) {
assert.strictEqual(typeof boxUpdateInfo, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
if (!boxUpdateInfo) return callback(null);
@@ -506,6 +509,8 @@ function update(boxUpdateInfo, callback) {
var error = locker.lock(locker.OP_BOX_UPDATE);
if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message));
eventlog.add(eventlog.ACTION_UPDATE, auditSource, { boxUpdateInfo: boxUpdateInfo });
// ensure tools can 'wait' on progress
progress.set(progress.UPDATE, 0, 'Starting');
@@ -544,9 +549,9 @@ function updateToLatest(auditSource, callback) {
var boxUpdateInfo = updateChecker.getUpdateInfo().box;
if (!boxUpdateInfo) return callback(new CloudronError(CloudronError.ALREADY_UPTODATE, 'No update available'));
eventlog.add(eventlog.ACTION_UPDATE, auditSource, { boxUpdateInfo: boxUpdateInfo });
if (boxUpdateInfo.upgrade && config.provider() !== 'caas') return callback(new CloudronError(CloudronError.SELF_UPGRADE_NOT_SUPPORTED));
update(boxUpdateInfo, callback);
update(boxUpdateInfo, auditSource, callback);
}
function doShortCircuitUpdate(boxUpdateInfo, callback) {
@@ -583,7 +588,7 @@ function doUpgrade(boxUpdateInfo, callback) {
// no need to unlock since this is the last thing we ever do on this box
callback();
retire();
retire('upgrade');
});
});
}
@@ -607,6 +612,7 @@ function doUpdate(boxUpdateInfo, callback) {
// this data is opaque to the installer
data: {
provider: config.provider(),
token: config.token(),
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin(),
@@ -656,23 +662,16 @@ function installAppBundle(callback) {
}
async.eachSeries(bundle, function (appInfo, iteratorCallback) {
var appstoreId = appInfo.appstoreId;
var parts = appstoreId.split('@');
debug('autoInstall: installing %s at %s', appInfo.appstoreId, appInfo.location);
var url = config.apiServerOrigin() + '/api/v1/apps/' + parts[0] + (parts[1] ? '/versions/' + parts[1] : '');
var data = {
appStoreId: appInfo.appstoreId,
location: appInfo.location,
portBindings: appInfo.portBindings || null,
accessRestriction: appInfo.accessRestriction || null,
};
superagent.get(url).end(function (error, result) {
if (error && !error.response) return iteratorCallback(new Error('Network error: ' + error.message));
if (result.statusCode !== 200) return iteratorCallback(util.format('Failed to get app info from store.', result.statusCode, result.text));
debug('autoInstall: installing %s at %s', appstoreId, appInfo.location);
apps.install(uuid.v4(), appstoreId, result.body.manifest, appInfo.location,
appInfo.portBindings || null, appInfo.accessRestriction || null,
null /* icon */, null /* cert */, null /* key */, 0 /* default mem limit */,
null /* altDomain */, { userId: null, username: 'autoinstaller' }, iteratorCallback);
});
apps.install(data, { userId: null, username: 'autoinstaller' }, iteratorCallback);
}, function (error) {
if (error) debug('autoInstallApps: ', error);
@@ -694,7 +693,7 @@ function checkDiskSpace(callback) {
var oos = entries.some(function (entry) {
return (entry.mount === paths.DATA_DIR && entry.capacity >= 0.90) ||
(entry.mount === '/' && entry.used <= (1.25 * 1024 * 1024)); // 1.5G
(entry.mount === '/' && entry.available <= (1.25 * 1024 * 1024)); // 1.5G
});
debug('Disk space checked. ok: %s', !oos);
@@ -705,13 +704,73 @@ function checkDiskSpace(callback) {
});
}
function retire(callback) {
function retire(reason, info, callback) {
assert(reason === 'migrate' || reason === 'upgrade');
info = info || { };
callback = callback || NOOP_CALLBACK;
var data = {
apiServerOrigin: config.apiServerOrigin(),
isCustomDomain: config.isCustomDomain(),
fqdn: config.fqdn()
};
shell.sudo('retire', [ RETIRE_CMD, JSON.stringify(data) ], callback);
shell.sudo('retire', [ RETIRE_CMD, reason, JSON.stringify(info), JSON.stringify(data) ], callback);
}
function doMigrate(options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var error = locker.lock(locker.OP_MIGRATE);
if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message));
function unlock(error) {
debug('Failed to migrate', error);
locker.unlock(locker.OP_MIGRATE);
progress.set(progress.MIGRATE, -1, 'Backup failed: ' + error.message);
}
progress.set(progress.MIGRATE, 10, 'Backing up for migration');
// initiate the migration in the background
backups.backupBoxAndApps({ userId: null, username: 'migrator' }, function (error, backupId) {
if (error) return unlock(error);
debug('migrate: domain: %s size %s region %s', options.domain, options.size, options.region);
options.restoreKey = backupId;
superagent
.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/migrate')
.query({ token: config.token() })
.send(options)
.end(function (error, result) {
if (error && !error.response) return unlock(error); // network error
if (result.statusCode === 409) return unlock(new CloudronError(CloudronError.BAD_STATE));
if (result.statusCode === 404) return unlock(new CloudronError(CloudronError.NOT_FOUND));
if (result.statusCode !== 202) return unlock(new CloudronError(CloudronError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
progress.set(progress.MIGRATE, 10, 'Migrating');
retire('migrate', _.pick(options, 'domain', 'size', 'region'));
});
});
callback(null);
}
function migrate(options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
if (!options.domain) return doMigrate(options, callback);
var dnsConfig = _.pick(options, 'domain', 'provider', 'accessKeyId', 'secretAccessKey', 'region', 'endpoint');
settings.setDnsConfig(dnsConfig, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_FIELD, error.message));
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
doMigrate(options, callback);
});
}
+6 -7
View File
@@ -28,9 +28,9 @@ exports = module.exports = {
internalAdminOrigin: internalAdminOrigin,
sysadminOrigin: sysadminOrigin, // caas routes
adminFqdn: adminFqdn,
mailFqdn: mailFqdn,
appFqdn: appFqdn,
zoneName: zoneName,
adminEmail: adminEmail,
isDev: isDev,
@@ -73,11 +73,11 @@ function initConfig() {
data.fqdn = 'localhost';
data.token = null;
data.adminEmail = null;
data.boxVersionsUrl = null;
data.version = null;
data.isCustomDomain = false;
data.webServerOrigin = null;
data.smtpPort = 2525; // // this value comes from mail container
data.sysadminPort = 3001;
data.ldapPort = 3002;
data.oauthProxyPort = 3003;
@@ -100,7 +100,6 @@ function initConfig() {
name: 'boxtest'
};
data.token = 'APPSTORE_TOKEN';
data.adminEmail = 'test@cloudron.foo';
} else {
assert(false, 'Unknown environment. This should not happen!');
}
@@ -139,10 +138,6 @@ function get(key) {
return safe.query(data, key);
}
function adminEmail() {
return get('adminEmail');
}
function apiServerOrigin() {
return get('apiServerOrigin');
}
@@ -167,6 +162,10 @@ function adminFqdn() {
return appFqdn(constants.ADMIN_LOCATION);
}
function mailFqdn() {
return appFqdn(constants.MAIL_LOCATION);
}
function adminOrigin() {
return 'https://' + appFqdn(constants.ADMIN_LOCATION);
}
+4
View File
@@ -6,12 +6,16 @@ exports = module.exports = {
API_LOCATION: 'api', // this is unused but reserved for future use (#403)
SMTP_LOCATION: 'smtp',
IMAP_LOCATION: 'imap',
MAIL_LOCATION: 'my', // not a typo! should be same as admin location until we figure out certificates
POSTMAN_LOCATION: 'postman', // used in dovecot bounces
ADMIN_NAME: 'Settings',
ADMIN_CLIENT_ID: 'webadmin', // oauth client id
ADMIN_APPID: 'admin', // admin appid (settingsdb)
GHOST_USER_FILE: '/tmp/cloudron_ghost.json',
DEFAULT_MEMORY_LIMIT: (256 * 1024 * 1024) // see also client.js
};
+20 -6
View File
@@ -13,6 +13,7 @@ var apps = require('./apps.js'),
config = require('./config.js'),
CronJob = require('cron').CronJob,
debug = require('debug')('box:cron'),
eventlog = require('./eventlog.js'),
janitor = require('./janitor.js'),
scheduler = require('./scheduler.js'),
settings = require('./settings.js'),
@@ -27,9 +28,11 @@ var gAutoupdaterJob = null,
gDockerVolumeCleanerJob = null,
gSchedulerSyncJob = null,
gCertificateRenewJob = null,
gCheckDiskSpaceJob = null;
gCheckDiskSpaceJob = null,
gCleanupEventlogJob = null;
var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
var AUDIT_SOURCE = { userId: null, username: 'cron' };
// cron format
// Seconds: 0-59
@@ -65,8 +68,8 @@ function recreateJobs(unusedTimeZone, callback) {
if (gBackupJob) gBackupJob.stop();
gBackupJob = new CronJob({
cronTime: '00 00 */4 * * *', // every 4 hours
onTick: backups.ensureBackup,
cronTime: '00 00 */4 * * *', // every 4 hours. backups.ensureBackup() will only trigger a backup once per day
onTick: backups.ensureBackup.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
start: true,
timeZone: allSettings[settings.TIME_ZONE_KEY]
});
@@ -103,6 +106,14 @@ function recreateJobs(unusedTimeZone, callback) {
timeZone: allSettings[settings.TIME_ZONE_KEY]
});
if (gCleanupEventlogJob) gCleanupEventlogJob.stop();
gCleanupEventlogJob = new CronJob({
cronTime: '00 */30 * * * *', // every 30 minutes
onTick: eventlog.cleanup,
start: true,
timeZone: allSettings[settings.TIME_ZONE_KEY]
});
if (gDockerVolumeCleanerJob) gDockerVolumeCleanerJob.stop();
gDockerVolumeCleanerJob = new CronJob({
cronTime: '00 00 */12 * * *', // every 12 hours
@@ -122,7 +133,7 @@ function recreateJobs(unusedTimeZone, callback) {
if (gCertificateRenewJob) gCertificateRenewJob.stop();
gCertificateRenewJob = new CronJob({
cronTime: '00 00 */12 * * *', // every 12 hours
onTick: certificates.autoRenew,
onTick: certificates.renewAll.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
start: true,
timeZone: allSettings[settings.TIME_ZONE_KEY]
});
@@ -154,10 +165,10 @@ function autoupdatePatternChanged(pattern) {
var updateInfo = updateChecker.getUpdateInfo();
if (updateInfo.box) {
debug('Starting autoupdate to %j', updateInfo.box);
cloudron.update(updateInfo.box, NOOP_CALLBACK);
cloudron.updateToLatest(AUDIT_SOURCE, NOOP_CALLBACK);
} else if (updateInfo.apps) {
debug('Starting app update to %j', updateInfo.apps);
apps.autoupdateApps(updateInfo.apps, NOOP_CALLBACK);
apps.updateApps(updateInfo.apps, AUDIT_SOURCE, NOOP_CALLBACK);
} else {
debug('No auto updates available');
}
@@ -193,6 +204,9 @@ function uninitialize(callback) {
if (gCleanupTokensJob) gCleanupTokensJob.stop();
gCleanupTokensJob = null;
if (gCleanupEventlogJob) gCleanupEventlogJob.stop();
gCleanupEventlogJob = null;
if (gDockerVolumeCleanerJob) gDockerVolumeCleanerJob.stop();
gDockerVolumeCleanerJob = null;
+2 -3
View File
@@ -1,5 +1,3 @@
/* jslint node: true */
'use strict';
exports = module.exports = {
@@ -122,7 +120,8 @@ function clear(callback) {
require('./groupdb.js')._clear,
require('./userdb.js')._clear,
require('./settingsdb.js')._clear,
require('./eventlogdb.js')._clear
require('./eventlogdb.js')._clear,
require('./mailboxdb.js')._clear
], callback);
}
+7 -5
View File
@@ -5,7 +5,7 @@
exports = module.exports = {
DeveloperError: DeveloperError,
enabled: enabled,
isEnabled: isEnabled,
setEnabled: setEnabled,
issueDeveloperToken: issueDeveloperToken,
getNonApprovedApps: getNonApprovedApps
@@ -13,6 +13,7 @@ exports = module.exports = {
var assert = require('assert'),
config = require('./config.js'),
clients = require('./clients.js'),
debug = require('debug')('box:developer'),
eventlog = require('./eventlog.js'),
tokendb = require('./tokendb.js'),
@@ -42,7 +43,7 @@ util.inherits(DeveloperError, Error);
DeveloperError.INTERNAL_ERROR = 'Internal Error';
DeveloperError.EXTERNAL_ERROR = 'External Error';
function enabled(callback) {
function isEnabled(callback) {
assert.strictEqual(typeof callback, 'function');
settings.getDeveloperMode(function (error, enabled) {
@@ -72,13 +73,14 @@ function issueDeveloperToken(user, auditSource, callback) {
var token = tokendb.generateToken();
var expiresAt = Date.now() + 24 * 60 * 60 * 1000; // 1 day
var scopes = '*,' + clients.SCOPE_ROLE_SDK;
tokendb.add(token, tokendb.PREFIX_DEV + user.id, '', expiresAt, 'developer,apps,settings,users,profile', function (error) {
tokendb.add(token, user.id, 'cid-cli', expiresAt, scopes, function (error) {
if (error) return callback(new DeveloperError(DeveloperError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource, { authType: 'cli', userId: user.id, username: user.username });
callback(null, { token: token, expiresAt: expiresAt });
callback(null, { token: token, expiresAt: new Date(expiresAt).toISOString() });
});
}
@@ -88,7 +90,7 @@ function getNonApprovedApps(callback) {
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/apps';
superagent.get(url).query({ token: config.token(), boxVersion: config.version() }).end(function (error, result) {
if (error && !error.response) return callback(new DeveloperError(DeveloperError.EXTERNAL_ERROR, error));
if (result.statusCode === 401) {
if (result.statusCode === 401 || result.statusCode === 403) {
debug('Failed to list apps in development. Appstore token invalid or missing. Returning empty list.', result.body);
return callback(null, []);
}
+24 -6
View File
@@ -1,5 +1,3 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
@@ -7,12 +5,14 @@ exports = module.exports = {
get: get,
del: del,
update: update,
getChangeStatus: getChangeStatus
getChangeStatus: getChangeStatus,
// not part of "dns" interface
getHostedZone: getHostedZone
};
var assert = require('assert'),
AWS = require('aws-sdk'),
config = require('../config.js'),
debug = require('debug')('box:dns/route53'),
SubdomainError = require('../subdomains.js').SubdomainError,
util = require('util'),
@@ -52,6 +52,24 @@ function getZoneByName(dnsConfig, zoneName, callback) {
});
}
function getHostedZone(dnsConfig, zoneName, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof callback, 'function');
getZoneByName(dnsConfig, zoneName, function (error, zone) {
if (error) return callback(error);
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
route53.getHostedZone({ Id: zone.Id }, function (error, result) {
if (error && error.code === 'AccessDenied') return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message));
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
callback(null, result);
});
});
}
function add(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
@@ -65,7 +83,7 @@ function add(dnsConfig, zoneName, subdomain, type, values, callback) {
getZoneByName(dnsConfig, zoneName, function (error, zone) {
if (error) return callback(error);
var fqdn = config.appFqdn(subdomain);
var fqdn = subdomain === '' ? zoneName : subdomain + '.' + zoneName;
var records = values.map(function (v) { return { Value: v }; });
var params = {
@@ -153,7 +171,7 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
getZoneByName(dnsConfig, zoneName, function (error, zone) {
if (error) return callback(error);
var fqdn = config.appFqdn(subdomain);
var fqdn = subdomain === '' ? zoneName : subdomain + '.' + zoneName;
var records = values.map(function (v) { return { Value: v }; });
var resourceRecordSet = {
+9 -13
View File
@@ -43,7 +43,6 @@ var addons = require('./addons.js'),
debug = require('debug')('box:src/docker.js'),
once = require('once'),
safe = require('safetydance'),
semver = require('semver'),
spawn = child_process.spawn,
util = require('util'),
_ = require('underscore');
@@ -133,7 +132,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
assert.strictEqual(typeof callback, 'function');
var docker = exports.connection,
isAppContainer = !cmd;
isAppContainer = !cmd; // non app-containers are like scheduler containers
var manifest = app.manifest;
var developmentMode = !!manifest.developmentMode;
@@ -172,18 +171,16 @@ function createSubcontainer(app, name, cmd, options, callback) {
// developerMode does not restrict memory usage
memoryLimit = developmentMode ? 0 : memoryLimit;
// for subcontainers, this should ideally be false. but docker does not allow network sharing if the app container is not running
// this means cloudron exec does not work
var isolatedNetworkNs = true;
addons.getEnvironment(app, function (error, addonEnv) {
if (error) return callback(new Error('Error getting addon environment : ' + error));
// do no set hostname of containers to location as it might conflict with addons names. for example, an app installed in mail
// location may not reach mail container anymore by DNS. We cannot set hostname to fqdn either as that sets up the dns
// name to look up the internal docker ip. this makes curl from within container fail
// Note that Hostname has no effect on DNS. We have to use the --net-alias for dns.
// Hostname cannot be set with container NetworkMode
var containerOptions = {
name: name, // used for filtering logs
// do _not_ set hostname to app fqdn. doing so sets up the dns name to look up the internal docker ip. this makes curl from within container fail
// for subcontainers, this should not be set because we already share the network namespace with app container
Hostname: isolatedNetworkNs ? (semver.gte(targetBoxVersion(app.manifest), '0.0.77') ? app.location : config.appFqdn(app.location)) : null,
Tty: isAppContainer,
Image: app.manifest.dockerImage,
Cmd: (isAppContainer && developmentMode) ? [ '/bin/bash', '-c', 'echo "Development mode. Use cloudron exec to debug. Sleeping" && sleep infinity' ] : cmd,
@@ -211,8 +208,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
},
CpuShares: 512, // relative to 1024 for system processes
VolumesFrom: isAppContainer ? null : [ app.containerId + ":rw" ],
NetworkMode: isolatedNetworkNs ? 'default' : ('container:' + app.containerId), // share network namespace with parent
Links: isolatedNetworkNs ? addons.getLinksSync(app, app.manifest.addons) : null, // links is redundant with --net=container
NetworkMode: isAppContainer ? 'cloudron' : ('container:' + app.containerId), // share network namespace with parent
SecurityOpt: config.CLOUDRON ? [ "apparmor:docker-cloudron-app" ] : null // profile available only on cloudron
}
};
@@ -375,10 +371,10 @@ function getContainerIdByIp(ip, callback) {
var bridge;
result.forEach(function (n) {
if (n.Name === 'bridge') bridge = n;
if (n.Name === 'cloudron') bridge = n;
});
if (!bridge) return callback(new Error('Unable to find the bridge network'));
if (!bridge) return callback(new Error('Unable to find the cloudron network'));
var containerId;
for (var id in bridge.Containers) {
+15
View File
@@ -6,9 +6,11 @@ exports = module.exports = {
add: add,
get: get,
getAllPaged: getAllPaged,
cleanup: cleanup,
// keep in sync with webadmin index.js filter
ACTION_ACTIVATE: 'cloudron.activate',
ACTION_APP_CLONE: 'app.clone',
ACTION_APP_CONFIGURE: 'app.configure',
ACTION_APP_INSTALL: 'app.install',
ACTION_APP_RESTORE: 'app.restore',
@@ -99,3 +101,16 @@ function getAllPaged(action, search, page, perPage, callback) {
callback(null, boxes);
});
}
function cleanup(callback) {
callback = callback || NOOP_CALLBACK;
var d = new Date();
d.setDate(d.getDate() - 7); // 7 days ago
eventlogdb.delByCreationTime(d, function (error) {
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
callback(null);
});
}
+13 -1
View File
@@ -5,6 +5,7 @@ exports = module.exports = {
getAllPaged: getAllPaged,
add: add,
count: count,
delByCreationTime: delByCreationTime,
_clear: clear
};
@@ -13,7 +14,8 @@ var assert = require('assert'),
database = require('./database.js'),
DatabaseError = require('./databaseerror'),
mysql = require('mysql'),
safe = require('safetydance');
safe = require('safetydance'),
util = require('util');
var EVENTLOGS_FIELDS = [ 'id', 'action', 'source', 'data', 'creationTime' ].join(',');
@@ -102,3 +104,13 @@ function clear(callback) {
});
}
function delByCreationTime(creationTime, callback) {
assert(util.isDate(creationTime));
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM eventlog WHERE creationTime < ?', [ creationTime ], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(error);
});
}
+14
View File
@@ -4,6 +4,7 @@ exports = module.exports = {
get: get,
getWithMembers: getWithMembers,
getAll: getAll,
getAllWithMembers: getAllWithMembers,
add: add,
del: del,
count: count,
@@ -65,6 +66,19 @@ function getAll(callback) {
});
}
function getAllWithMembers(callback) {
database.query('SELECT ' + GROUPS_FIELDS + ',GROUP_CONCAT(groupMembers.userId) AS userIds ' +
' FROM groups LEFT OUTER JOIN groupMembers ON groups.id = groupMembers.groupId ' +
' GROUP BY groups.id', function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
results.forEach(function (result) { result.userIds = result.userIds ? result.userIds.split(',') : [ ]; });
callback(null, results);
});
}
function add(id, name, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof name, 'string');
+16 -5
View File
@@ -10,6 +10,7 @@ exports = module.exports = {
get: get,
getWithMembers: getWithMembers,
getAll: getAll,
getAllWithMembers: getAllWithMembers,
getMembers: getMembers,
addMember: addMember,
@@ -51,7 +52,7 @@ util.inherits(GroupError, Error);
GroupError.INTERNAL_ERROR = 'Internal Error';
GroupError.ALREADY_EXISTS = 'Already Exists';
GroupError.NOT_FOUND = 'Not Found';
GroupError.BAD_NAME = 'Bad name';
GroupError.BAD_FIELD = 'Field error';
GroupError.NOT_EMPTY = 'Not Empty';
GroupError.NOT_ALLOWED = 'Not Allowed';
@@ -59,12 +60,12 @@ function validateGroupname(name) {
assert.strictEqual(typeof name, 'string');
var RESERVED = [ 'admins', 'users' ]; // ldap code uses 'users' pseudo group
if (name.length <= 2) return new GroupError(GroupError.BAD_NAME, 'name must be atleast 2 chars');
if (name.length >= 200) return new GroupError(GroupError.BAD_NAME, 'name too long');
if (name.length <= 2) return new GroupError(GroupError.BAD_FIELD, 'name must be atleast 2 chars');
if (name.length >= 200) return new GroupError(GroupError.BAD_FIELD, 'name too long');
if (!/^[A-Za-z0-9_-]*$/.test(name)) return new GroupError(GroupError.BAD_NAME, 'name can only have A-Za-z0-9_-');
if (!/^[A-Za-z0-9_-]*$/.test(name)) return new GroupError(GroupError.BAD_FIELD, 'name can only have A-Za-z0-9_-');
if (RESERVED.indexOf(name) !== -1) return new GroupError(GroupError.BAD_NAME, 'name is reserved');
if (RESERVED.indexOf(name) !== -1) return new GroupError(GroupError.BAD_FIELD, 'name is reserved');
return null;
}
@@ -133,6 +134,16 @@ function getAll(callback) {
});
}
function getAllWithMembers(callback) {
assert.strictEqual(typeof callback, 'function');
groupdb.getAllWithMembers(function (error, result) {
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
return callback(null, result);
});
}
function getMembers(groupId, callback) {
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof callback, 'function');
+22
View File
@@ -0,0 +1,22 @@
'use strict';
// WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
// These constants are used in the installer script as well
// Do not require anything here!
exports = module.exports = {
// a version bump means that all containers (apps and addons) are recreated
'version': 40,
'baseImage': 'cloudron/base:0.8.1',
'images': {
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:0.12.0' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:0.11.0' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:0.10.0' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:0.9.0' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:0.18.0' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:0.9.0' }
}
};
+211 -141
View File
@@ -12,7 +12,9 @@ var assert = require('assert'),
eventlog = require('./eventlog.js'),
user = require('./user.js'),
UserError = user.UserError,
ldap = require('ldapjs');
ldap = require('ldapjs'),
mailboxes = require('./mailboxes.js'),
MailboxError = mailboxes.MailboxError;
var gServer = null;
@@ -35,8 +37,204 @@ function getAppByRequest(req, callback) {
if (sourceIp.split('.').length !== 4) return callback(new ldap.InsufficientAccessRightsError('Missing source identifier'));
apps.getByIpAddress(sourceIp, function (error, app) {
// we currently allow access in case we can't find the source app
callback(null, app || null);
if (error) return callback(new ldap.OperationsError(error.message));
if (!app) return callback(new ldap.OperationsError('Could not detect app source'));
callback(null, app);
});
}
function userSearch(req, res, next) {
debug('user search: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id);
user.list(function (error, result) {
if (error) return next(new ldap.OperationsError(error.toString()));
// send user objects
result.forEach(function (entry) {
var dn = ldap.parseDN('cn=' + entry.id + ',ou=users,dc=cloudron');
var groups = [ GROUP_USERS_DN ];
if (entry.admin) groups.push(GROUP_ADMINS_DN);
var displayName = entry.displayName || entry.username;
var nameParts = displayName.split(' ');
var firstName = nameParts[0];
var lastName = nameParts.length > 1 ? nameParts[nameParts.length - 1] : ''; // choose last part, if it exists
var obj = {
dn: dn.toString(),
attributes: {
objectclass: ['user'],
objectcategory: 'person',
cn: entry.id,
uid: entry.id,
mail: entry.email,
// TODO: check mailboxes before we send this
mailAlternateAddress: entry.username + '@' + config.fqdn(),
displayname: displayName,
givenName: firstName,
username: entry.username,
samaccountname: entry.username, // to support ActiveDirectory clients
memberof: groups
}
};
// http://www.zytrax.com/books/ldap/ape/core-schema.html#sn has 'name' as SUP which is a DirectoryString
// which is required to have atleast one character if present
if (lastName.length !== 0) obj.attributes.sn = lastName;
// ensure all filter values are also lowercase
var lowerCaseFilter = ldap.parseFilter(req.filter.toString().toLowerCase());
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) {
res.send(obj);
}
});
res.end();
});
}
function groupSearch(req, res, next) {
debug('group search: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id);
user.list(function (error, result){
if (error) return next(new ldap.OperationsError(error.toString()));
var groups = [{
name: 'users',
admin: false
}, {
name: 'admins',
admin: true
}];
groups.forEach(function (group) {
var dn = ldap.parseDN('cn=' + group.name + ',ou=groups,dc=cloudron');
var members = group.admin ? result.filter(function (entry) { return entry.admin; }) : result;
var obj = {
dn: dn.toString(),
attributes: {
objectclass: ['group'],
cn: group.name,
memberuid: members.map(function(entry) { return entry.id; })
}
};
// ensure all filter values are also lowercase
var lowerCaseFilter = ldap.parseFilter(req.filter.toString().toLowerCase());
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) {
res.send(obj);
}
});
res.end();
});
}
function mailboxSearch(req, res, next) {
debug('mailbox search: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id);
mailboxes.getAll(function (error, result) {
if (error) return next(new ldap.OperationsError(error.toString()));
result.forEach(function (entry) {
var dn = ldap.parseDN('cn=' + entry.name + ',ou=mailboxes,dc=cloudron');
// TODO: send aliases
var obj = {
dn: dn.toString(),
attributes: {
objectclass: ['mailbox'],
objectcategory: 'mailbox',
cn: entry.name,
uid: entry.name,
mail: entry.name + '@' + config.fqdn()
}
};
// ensure all filter values are also lowercase
var lowerCaseFilter = ldap.parseFilter(req.filter.toString().toLowerCase());
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) {
res.send(obj);
}
});
res.end();
});
}
function authenticateUser(req, res, next) {
debug('user bind: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
// extract the common name which might have different attribute names
var attributeName = Object.keys(req.dn.rdns[0])[0];
var commonName = req.dn.rdns[0][attributeName];
if (!commonName) return next(new ldap.NoSuchObjectError(req.dn.toString()));
var api;
if (attributeName === 'mail') {
api = user.verifyWithEmail;
} else if (commonName.indexOf('@') !== -1) { // if mail is specified, enforce mail check
var parts = commonName.split('@');
if (parts[1] === config.fqdn()) { // internal email, verify with username
commonName = parts[0];
api = user.verifyWithUsername;
} else { // external email
api = user.verifyWithEmail;
}
} else if (commonName.indexOf('uid-') === 0) {
api = user.verify;
} else {
api = user.verifyWithUsername;
}
api(commonName, req.credentials || '', function (error, user) {
if (error && error.reason === UserError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error && error.reason === UserError.WRONG_PASSWORD) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
req.user = user;
next();
});
}
function authorizeUserForApp(req, res, next) {
assert(req.user);
getAppByRequest(req, function (error, app) {
if (error) return next(error);
apps.hasAccessTo(app, req.user, function (error, result) {
if (error) return next(new ldap.OperationsError(error.toString()));
// we return no such object, to avoid leakage of a users existence
if (!result) return next(new ldap.NoSuchObjectError(req.dn.toString()));
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: app.id }, { userId: req.user.id });
res.end();
});
});
}
function authorizeUserForMailbox(req, res, next) {
assert(req.user);
// We simply authorize the user to access a mailbox by his own name
mailboxes.get(req.user.username, function (error) {
if (error && error.reason === MailboxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: req.user.username }, { userId: req.user.username });
res.end();
});
}
@@ -45,155 +243,27 @@ function start(callback) {
gServer = ldap.createServer({ log: gLogger });
gServer.search('ou=users,dc=cloudron', function (req, res, next) {
debug('user search: dn %s, scope %s, filter %s', req.dn.toString(), req.scope, req.filter.toString());
gServer.search('ou=users,dc=cloudron', userSearch);
gServer.search('ou=groups,dc=cloudron', groupSearch);
gServer.bind('ou=users,dc=cloudron', authenticateUser, authorizeUserForApp);
user.list(function (error, result) {
if (error) return next(new ldap.OperationsError(error.toString()));
gServer.search('ou=mailboxes,dc=cloudron', mailboxSearch);
gServer.bind('ou=mailboxes,dc=cloudron', authenticateUser, authorizeUserForMailbox);
// send user objects
result.forEach(function (entry) {
var dn = ldap.parseDN('cn=' + entry.id + ',ou=users,dc=cloudron');
var groups = [ GROUP_USERS_DN ];
if (entry.admin) groups.push(GROUP_ADMINS_DN);
var displayName = entry.displayName || entry.username;
var nameParts = displayName.split(' ');
var firstName = nameParts[0];
var lastName = nameParts.length > 1 ? nameParts[nameParts.length - 1] : ''; // choose last part, if it exists
var obj = {
dn: dn.toString(),
attributes: {
objectclass: ['user'],
objectcategory: 'person',
cn: entry.id,
uid: entry.id,
mail: entry.email,
displayname: displayName,
givenName: firstName,
username: entry.username,
samaccountname: entry.username, // to support ActiveDirectory clients
memberof: groups
}
};
// http://www.zytrax.com/books/ldap/ape/core-schema.html#sn has 'name' as SUP which is a DirectoryString
// which is required to have atleast one character if present
if (lastName.length !== 0) obj.attributes.sn = lastName;
// ensure all filter values are also lowercase
var lowerCaseFilter = ldap.parseFilter(req.filter.toString().toLowerCase());
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) {
res.send(obj);
}
});
res.end();
});
});
gServer.search('ou=groups,dc=cloudron', function (req, res, next) {
debug('group search: dn %s, scope %s, filter %s', req.dn.toString(), req.scope, req.filter.toString());
user.list(function (error, result){
if (error) return next(new ldap.OperationsError(error.toString()));
var groups = [{
name: 'users',
admin: false
}, {
name: 'admins',
admin: true
}];
groups.forEach(function (group) {
var dn = ldap.parseDN('cn=' + group.name + ',ou=groups,dc=cloudron');
var members = group.admin ? result.filter(function (entry) { return entry.admin; }) : result;
var obj = {
dn: dn.toString(),
attributes: {
objectclass: ['group'],
cn: group.name,
memberuid: members.map(function(entry) { return entry.id; })
}
};
// ensure all filter values are also lowercase
var lowerCaseFilter = ldap.parseFilter(req.filter.toString().toLowerCase());
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) {
res.send(obj);
}
});
res.end();
});
});
// this is the bind for the mail addon to authorize apps
gServer.bind('ou=sendmail,dc=cloudron', function(req, res, next) {
// TODO: validate password
debug('sendmail bind: %s', req.dn.toString()); // note: cn can be email or id
// this is the bind for addons (after bind, they might search and authenticate)
gServer.bind('ou=addons,dc=cloudron', function(req, res, next) {
debug('addons bind: %s', req.dn.toString()); // note: cn can be email or id
res.end();
});
// this is the bind for apps (after bind, they might search and authenticate user)
gServer.bind('ou=apps,dc=cloudron', function(req, res, next) {
// TODO: validate password
debug('application bind: %s', req.dn.toString());
res.end();
});
gServer.bind('ou=users,dc=cloudron', function(req, res, next) {
debug('user bind: %s', req.dn.toString());
// extract the common name which might have different attribute names
var attributeName = Object.keys(req.dn.rdns[0])[0];
var commonName = req.dn.rdns[0][attributeName];
if (!commonName) return next(new ldap.NoSuchObjectError(req.dn.toString()));
var api;
// if mail is specified, enforce mail check
if (commonName.indexOf('@') !== -1 || attributeName === 'mail') {
api = user.verifyWithEmail;
} else if (commonName.indexOf('uid-') === 0) {
api = user.verify;
} else {
api = user.verifyWithUsername;
}
// TODO this should be done after we verified the app has access to avoid leakage of user existence
api(commonName, req.credentials || '', function (error, userObject) {
if (error && error.reason === UserError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error && error.reason === UserError.WRONG_PASSWORD) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error));
getAppByRequest(req, function (error, app) {
if (error) return next(error);
if (!app) {
debug('no app found for this container, allow access');
return res.end();
}
apps.hasAccessTo(app, userObject, function (error, result) {
if (error) return next(new ldap.OperationsError(error.toString()));
// we return no such object, to avoid leakage of a users existence
if (!result) return next(new ldap.NoSuchObjectError(req.dn.toString()));
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: app.id }, { userId: userObject.id });
res.end();
});
});
});
});
gServer.listen(config.get('ldapPort'), callback);
gServer.listen(config.get('ldapPort'), '0.0.0.0', callback);
}
function stop(callback) {
@@ -1,13 +1,10 @@
<%if (format === 'text') { %>
Dear Cloudron Team,
<% if (message) { %>
<%= domain %> was not renewed.
<%- message %>
<% } else { %>
<%= domain %> was renewed.
<% } %>
Thank you,
Your Cloudron
<% } else { %>
+1 -1
View File
@@ -4,7 +4,7 @@ Dear Cloudron Team,
<%= fqdn %> is running out of disk space.
Please see some excerpts of the logs below.
Disk space logs are attached.
Thank you,
Your Cloudron
+1 -1
View File
@@ -2,7 +2,7 @@
Dear Admin,
User with name <%= user.email %> was added in the Cloudron at <%= fqdn %>.
User with email <%= user.email %> was added in the Cloudron at <%= fqdn %>.
You are receiving this email because you are an Admin of the Cloudron at <%= fqdn %>.
+127
View File
@@ -0,0 +1,127 @@
'use strict';
exports = module.exports = {
add: add,
del: del,
get: get,
getAll: getAll,
getAliases: getAliases,
setAliases: setAliases,
_clear: clear
};
var assert = require('assert'),
database = require('./database.js'),
DatabaseError = require('./databaseerror.js'),
util = require('util');
var MAILBOX_FIELDS = [ 'name', 'aliasTarget', 'creationTime' ].join(',');
function add(name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO mailboxes (name) VALUES (?)', [ name ], function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}
function clear(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('TRUNCATE TABLE mailboxes', [], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}
function del(name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
// deletes aliases as well
database.query('DELETE FROM mailboxes WHERE name=? OR aliasTarget = ?', [ name, name ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null);
});
}
function postProcess(result) {
result.aliases = result.aliases ? result.aliases.split(',') : [ ];
}
function get(name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
var query = 'SELECT m1.name, m1.creationTime, GROUP_CONCAT(m2.name) AS aliases ' +
'FROM mailboxes as m1 ' +
'LEFT OUTER JOIN mailboxes as m2 ON m1.name = m2.aliasTarget ' +
'WHERE m1.name=? AND m1.aliasTarget IS NULL ' +
'GROUP BY m1.name';
database.query(query, [ name ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
postProcess(results[0]);
callback(null, results[0]);
});
}
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
var query = 'SELECT m1.name, m1.creationTime, GROUP_CONCAT(m2.name) AS aliases ' +
'FROM mailboxes as m1 ' +
'LEFT OUTER JOIN mailboxes as m2 ON m1.name = m2.aliasTarget ' +
'WHERE m1.aliasTarget IS NULL ' +
'GROUP BY m1.name';
database.query(query, function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results.forEach(postProcess);
callback(null, results);
});
}
function setAliases(name, aliases, callback) {
assert.strictEqual(typeof name, 'string');
assert(util.isArray(aliases));
assert.strictEqual(typeof callback, 'function');
// also cleanup the groupMembers table
var queries = [];
queries.push({ query: 'DELETE FROM mailboxes WHERE aliasTarget = ?', args: [ name ] });
aliases.forEach(function (alias) {
queries.push({ query: 'INSERT INTO mailboxes (name, aliasTarget) VALUES (?, ?)', args: [ alias, name ] });
});
database.transaction(queries, function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error.message));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}
function getAliases(name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT name FROM mailboxes WHERE aliasTarget=? ORDER BY name', [ name ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results = results.map(function (r) { return r.name; });
callback(null, results);
});
}
+199
View File
@@ -0,0 +1,199 @@
'use strict';
exports = module.exports = {
add: add,
del: del,
get: get,
getAll: getAll,
setAliases: setAliases,
getAliases: getAliases,
setupAliases: setupAliases,
MailboxError: MailboxError
};
var assert = require('assert'),
async = require('async'),
config = require('./config.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:mailboxes'),
docker = require('./docker.js'),
mailboxdb = require('./mailboxdb.js'),
util = require('util');
function MailboxError(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(MailboxError, Error);
MailboxError.ALREADY_EXISTS = 'already exists';
MailboxError.BAD_FIELD = 'Field error';
MailboxError.NOT_FOUND = 'not found';
MailboxError.INTERNAL_ERROR = 'internal error';
MailboxError.EXTERNAL_ERROR = 'external error';
function validateName(name) {
var RESERVED_NAMES = [ 'no-reply', 'postmaster', 'mailer-daemon' ];
if (!name.length) return new MailboxError(MailboxError.BAD_FIELD, "name cannot be empty");
if (name.length < 2) return new MailboxError(MailboxError.BAD_FIELD, 'name too small');
if (name.length > 127) return new MailboxError(MailboxError.BAD_FIELD, 'name too long');
if (RESERVED_NAMES.indexOf(name) !== -1) return new MailboxError(MailboxError.BAD_FIELD, 'name is reserved');
if (/[^a-zA-Z0-9.]/.test(name)) return new MailboxError(MailboxError.BAD_FIELD, 'name can only contain alphanumerals and dot');
if (name.indexOf('.app') !== -1) return new MailboxError(MailboxError.BAD_FIELD, 'alias pattern is reserved for apps');
return null;
}
function add(name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
name = name.toLowerCase();
var error = validateName(name);
if (error) return callback(error);
mailboxdb.add(name, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new MailboxError(MailboxError.ALREADY_EXISTS));
if (error) return callback(new MailboxError(MailboxError.INTERNAL_ERROR, error));
debug('Added mailbox %s', name);
var mailbox = {
name: name
};
callback(null, mailbox);
});
}
function pushAlias(name, aliases, callback) {
if (process.env.BOX_ENV === 'test') return callback();
var cmd = [ '/addons/mail/service.sh', 'set-alias', name ].concat(aliases);
debug('pushing alias for %s : %j', name, aliases);
docker.execContainer('mail', cmd, { }, function (error) {
if (error) return callback(new MailboxError(MailboxError.EXTERNAL_ERROR, error));
callback();
});
}
function del(name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
pushAlias(name, [ ], function (error) {
if (error) return callback(error);
mailboxdb.del(name, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailboxError(MailboxError.NOT_FOUND));
if (error) return callback(new MailboxError(MailboxError.INTERNAL_ERROR, error));
debug('deleted mailbox %s', name);
callback();
});
});
}
function get(name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
mailboxdb.get(name, function (error, mailbox) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailboxError(MailboxError.NOT_FOUND));
if (error) return callback(new MailboxError(MailboxError.INTERNAL_ERROR, error));
callback(null, mailbox);
});
}
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
mailboxdb.getAll(function (error, results) {
if (error) return callback(new MailboxError(MailboxError.INTERNAL_ERROR, error));
callback(null, results);
});
}
function setAliases(name, aliases, callback) {
assert.strictEqual(typeof name, 'string');
assert(util.isArray(aliases));
assert.strictEqual(typeof callback, 'function');
for (var i = 0; i < aliases.length; i++) {
aliases[i] = aliases[i].toLowerCase();
var error = validateName(aliases[i]);
if (error) return callback(error);
}
pushAlias(name, aliases, function (error) {
if (error) return callback(error);
mailboxdb.setAliases(name, aliases, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new MailboxError(MailboxError.ALREADY_EXISTS, error.message))
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailboxError(MailboxError.NOT_FOUND));
if (error) return callback(new MailboxError(MailboxError.INTERNAL_ERROR, error));
callback(null);
});
});
}
function getAliases(name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
mailboxdb.getAliases(name, function (error, aliases) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailboxError(MailboxError.NOT_FOUND));
if (error) return callback(new MailboxError(MailboxError.INTERNAL_ERROR, error));
callback(null, aliases);
});
}
// push aliases to the mail container on startup
function setupAliases(callback) {
assert.strictEqual(typeof callback, 'function');
getAll(function (error, mailboxes) {
if (error) return callback(error);
async.each(mailboxes, function (mailbox, iteratorDone) {
getAliases(mailbox.name, function (error, aliases) {
if (error) return iteratorDone(error);
if (aliases.length === 0) return iteratorDone();
pushAlias(mailbox.name, aliases, iteratorDone);
});
}, callback);
});
}
+41 -32
View File
@@ -18,7 +18,7 @@ exports = module.exports = {
outOfDiskSpace: outOfDiskSpace,
certificateRenewed: certificateRenewed,
certificateRenewalError: certificateRenewalError,
FEEDBACK_TYPE_FEEDBACK: 'feedback',
FEEDBACK_TYPE_TICKET: 'ticket',
@@ -41,6 +41,7 @@ var assert = require('assert'),
ejs = require('ejs'),
nodemailer = require('nodemailer'),
path = require('path'),
platform = require('./platform.js'),
safe = require('safetydance'),
smtpTransport = require('nodemailer-smtp-transport'),
users = require('./user.js'),
@@ -113,7 +114,7 @@ function getTxtRecords(callback) {
function checkDns() {
getTxtRecords(function (error, records) {
if (error || !records) {
debug('checkDns: DNS error or no records looking up TXT records for %s %s', config.adminFqdn(), error, records);
debug('checkDns: DNS error or no records looking up TXT records for %s %s', config.fqdn(), error, records);
gCheckDnsTimerId = setTimeout(checkDns, 60000);
return;
}
@@ -154,19 +155,19 @@ function sendMails(queue) {
docker.getContainer('mail').inspect(function (error, data) {
if (error) return console.error(error);
var mailServerIp = safe.query(data, 'NetworkSettings.IPAddress');
var mailServerIp = safe.query(data, 'NetworkSettings.Networks.cloudron.IPAddress');
if (!mailServerIp) return debug('Error querying mail server IP');
var transport = nodemailer.createTransport(smtpTransport({
host: mailServerIp,
port: 2500, // this value comes from mail container
port: config.get('smtpPort'),
auth: {
user: 'no-reply', // derive from adminEmail
pass: 'supersecret'
user: platform.mailConfig().username,
pass: platform.mailConfig().password
}
}));
debug('Processing mail queue of size %d (through %s:2500)', queue.length, mailServerIp);
debug('Processing mail queue of size %d (through %s:2525)', queue.length, mailServerIp);
async.mapSeries(queue, function iterator(mailOptions, callback) {
transport.sendMail(mailOptions, function (error) {
@@ -222,7 +223,7 @@ function mailUserEventToAdmins(user, event) {
adminEmails = _.difference(adminEmails, [ user.email ]);
var mailOptions = {
from: config.adminEmail(),
from: platform.mailConfig().from,
to: adminEmails.join(', '),
subject: util.format('%s %s in Cloudron %s', user.username || user.email, event, config.fqdn()),
text: render('user_event.ejs', { fqdn: config.fqdn(), user: user, event: event, format: 'text' }),
@@ -248,7 +249,7 @@ function sendInvite(user, invitor) {
};
var mailOptions = {
from: config.adminEmail(),
from: platform.mailConfig().from,
to: user.email,
subject: util.format('Welcome to Cloudron %s', config.fqdn()),
text: render('welcome_user.ejs', templateData)
@@ -271,7 +272,7 @@ function userAdded(user, inviteSent) {
var inviteLink = inviteSent ? null : config.adminOrigin() + '/api/v1/session/account/setup.html?reset_token=' + user.resetToken;
var mailOptions = {
from: config.adminEmail(),
from: platform.mailConfig().from,
to: adminEmails.join(', '),
subject: util.format('%s added in Cloudron %s', user.email, config.fqdn()),
text: render('user_added.ejs', { fqdn: config.fqdn(), user: user, inviteLink: inviteLink, format: 'text' }),
@@ -306,7 +307,7 @@ function passwordReset(user) {
var resetLink = config.adminOrigin() + '/api/v1/session/password/reset.html?reset_token=' + user.resetToken;
var mailOptions = {
from: config.adminEmail(),
from: platform.mailConfig().from,
to: user.email,
subject: 'Password Reset Request',
text: render('password_reset.ejs', { fqdn: config.fqdn(), user: user, resetLink: resetLink, format: 'text' })
@@ -324,7 +325,7 @@ function appDied(app) {
if (error) return console.log('Error getting admins', error);
var mailOptions = {
from: config.adminEmail(),
from: platform.mailConfig().from,
to: adminEmails.concat('support@cloudron.io').join(', '),
subject: util.format('App %s is down', app.location),
text: render('app_down.ejs', { fqdn: config.fqdn(), title: app.manifest.title, appFqdn: config.appFqdn(app.location), format: 'text' })
@@ -342,7 +343,7 @@ function boxUpdateAvailable(newBoxVersion, changelog) {
if (error) return console.log('Error getting admins', error);
var mailOptions = {
from: config.adminEmail(),
from: platform.mailConfig().from,
to: adminEmails.join(', '),
subject: util.format('%s has a new update available', config.fqdn()),
text: render('box_update_available.ejs', { fqdn: config.fqdn(), webadminUrl: config.adminOrigin(), newBoxVersion: newBoxVersion, changelog: changelog, format: 'text' })
@@ -360,7 +361,7 @@ function appUpdateAvailable(app, updateInfo) {
if (error) return console.log('Error getting admins', error);
var mailOptions = {
from: config.adminEmail(),
from: platform.mailConfig().from,
to: adminEmails.join(', '),
subject: util.format('%s has a new update available', app.fqdn),
text: render('app_update_available.ejs', { fqdn: config.fqdn(), webadminUrl: config.adminOrigin(), app: app, updateInfo: updateInfo, format: 'text' })
@@ -373,28 +374,36 @@ function appUpdateAvailable(app, updateInfo) {
function outOfDiskSpace(message) {
assert.strictEqual(typeof message, 'string');
var mailOptions = {
from: config.adminEmail(),
to: 'admin@cloudron.io',
subject: util.format('[%s] Out of disk space alert', config.fqdn()),
text: render('out_of_disk_space.ejs', { fqdn: config.fqdn(), message: message, format: 'text' })
};
getAdminEmails(function (error, adminEmails) {
if (error) return console.log('Error getting admins', error);
sendMails([ mailOptions ]);
var mailOptions = {
from: platform.mailConfig().from,
to: config.provider() === 'caas' ? 'support@cloudron.io' : adminEmails.join(', '),
subject: util.format('[%s] Out of disk space alert', config.fqdn()),
text: render('out_of_disk_space.ejs', { fqdn: config.fqdn(), message: message, format: 'text' })
};
sendMails([ mailOptions ]);
});
}
function certificateRenewed(domain, message) {
function certificateRenewalError(domain, message) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof message, 'string');
var mailOptions = {
from: config.adminEmail(),
to: 'admin@cloudron.io',
subject: util.format('[%s] Certificate was %s renewed', domain, message ? 'not' : ''),
text: render('certificate_renewed.ejs', { domain: domain, message: message, format: 'text' })
};
getAdminEmails(function (error, adminEmails) {
if (error) return console.log('Error getting admins', error);
sendMails([ mailOptions ]);
var mailOptions = {
from: platform.mailConfig().from,
to: config.provider() === 'caas' ? 'support@cloudron.io' : adminEmails.join(', '),
subject: util.format('[%s] Certificate renewal error', domain),
text: render('certificate_renewal_error.ejs', { domain: domain, message: message, format: 'text' })
};
sendMails([ mailOptions ]);
});
}
// this function bypasses the queue intentionally. it is also expected to work without the mailer module initialized
@@ -404,8 +413,8 @@ function unexpectedExit(program, context) {
assert.strictEqual(typeof context, 'string');
var mailOptions = {
from: config.adminEmail(),
to: 'admin@cloudron.io',
from: platform.mailConfig().from,
to: 'support@cloudron.io',
subject: util.format('[%s] %s exited unexpectedly', config.fqdn(), program),
text: render('unexpected_exit.ejs', { fqdn: config.fqdn(), program: program, context: context, format: 'text' })
};
@@ -426,7 +435,7 @@ function sendFeedback(user, type, subject, description) {
type === exports.FEEDBACK_TYPE_APP_ERROR);
var mailOptions = {
from: config.adminEmail(),
from: platform.mailConfig().from,
to: 'support@cloudron.io',
subject: util.format('[%s] %s - %s', type, config.fqdn(), subject),
text: render('feedback.ejs', { fqdn: config.fqdn(), type: type, user: user, subject: subject, description: description, format: 'text'})
+1
View File
@@ -20,6 +20,7 @@ module.exports = function cors(options) {
if (!requestOrigin) return next();
requestOrigin = url.parse(requestOrigin);
if (!requestOrigin.host) return res.status(405).send('CORS not allowed from this domain');
var hostname = requestOrigin.host.split(':')[0]; // remove any port
var originAllowed = origins.some(function (o) { return o === '*' || o === hostname; });
-1
View File
@@ -4,7 +4,6 @@ exports = module.exports = {
cookieParser: require('cookie-parser'),
cors: require('./cors'),
csrf: require('csurf'),
favicon: require('serve-favicon'),
json: require('body-parser').json,
morgan: require('morgan'),
proxy: require('proxy-middleware'),
+4 -2
View File
@@ -45,7 +45,8 @@ function configureAdmin(certFilePath, keyFilePath, callback) {
vhost: config.adminFqdn(),
endpoint: 'admin',
certFilePath: certFilePath,
keyFilePath: keyFilePath
keyFilePath: keyFilePath,
xFrameOptions: 'SAMEORIGIN'
};
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, 'admin.conf');
@@ -73,7 +74,8 @@ function configureApp(app, certFilePath, keyFilePath, callback) {
port: app.httpPort,
endpoint: endpoint,
certFilePath: certFilePath,
keyFilePath: keyFilePath
keyFilePath: keyFilePath,
xFrameOptions: app.xFrameOptions || 'SAMEORIGIN' // once all apps have been updated/
};
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
+11 -1
View File
@@ -8,7 +8,10 @@
// very basic angular app
var app = angular.module('Application', []);
app.controller('Controller', [function () {}]);
app.controller('Controller', ['$scope', function ($scope) {
$scope.username = '<%= (user && user.username) ? user.username : '' %>';
$scope.displayName = '<%= (user && user.displayName) ? user.displayName : '' %>';
}]);
</script>
@@ -28,6 +31,12 @@ app.controller('Controller', [function () {}]);
<center><p class="has-error"><%= error %></p></center>
<% if (user && user.username) { %>
<div class="form-group"">
<label class="control-label">Username</label>
<input type="text" class="form-control" ng-model="username" name="username" readonly required>
</div>
<% } else { %>
<div class="form-group" ng-class="{ 'has-error': (setupForm.username.$dirty && setupForm.username.$invalid) }">
<label class="control-label">Username</label>
<div class="control-label" ng-show="setupForm.username.$dirty && setupForm.username.$invalid">
@@ -37,6 +46,7 @@ app.controller('Controller', [function () {}]);
</div>
<input type="text" class="form-control" ng-model="username" name="username" ng-maxlength="512" ng-minlength="3" required autofocus>
</div>
<% } %>
<div class="form-group">
<label class="control-label">Display Name</label>
+2 -2
View File
@@ -7,7 +7,7 @@ exports = module.exports = {
var appdb = require('./appdb.js'),
assert = require('assert'),
clientdb = require('./clientdb.js'),
clients = require('./clients.js'),
config = require('./config.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:proxy'),
@@ -124,7 +124,7 @@ function authenticate(req, res, next) {
return res.send(500, 'Unknown app.');
}
clientdb.getByAppIdAndType(result.id, clientdb.TYPE_PROXY, function (error, result) {
clients.getByAppIdAndType(result.id, clients.TYPE_PROXY, function (error, result) {
if (error) {
console.error('Unknown OAuth client.', error);
return res.send(500, 'Unknown OAuth client.');
+3 -1
View File
@@ -27,5 +27,7 @@ exports = module.exports = {
UPDATE_CHECKER_FILE: path.join(config.baseDir(), 'data/box/updatechecker.json'),
ACME_CHALLENGES_DIR: path.join(config.baseDir(), 'data/acme'),
ACME_ACCOUNT_KEY_FILE: path.join(config.baseDir(), 'data/box/acme/acme.key')
ACME_ACCOUNT_KEY_FILE: path.join(config.baseDir(), 'data/box/acme/acme.key'),
INFRA_VERSION_FILE: path.join(config.baseDir(), 'data/INFRA_VERSION')
};
+312
View File
@@ -0,0 +1,312 @@
'use strict';
exports = module.exports = {
initialize: initialize,
uninitialize: uninitialize,
events: new (require('events').EventEmitter)(),
EVENT_READY: 'ready',
isReadySync: isReadySync,
mailConfig: mailConfig
};
var apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
config = require('./config.js'),
certificates = require('./certificates.js'),
debug = require('debug')('box:platform'),
fs = require('fs'),
hat = require('hat'),
infra = require('./infra_version.js'),
ini = require('ini'),
mailboxes = require('./mailboxes.js'),
paths = require('./paths.js'),
safe = require('safetydance'),
shell = require('./shell.js'),
util = require('util'),
_ = require('underscore');
var gAddonVars = null,
gPlatformReadyTimer = null;
function initialize(callback) {
if (process.env.BOX_ENV === 'test' && !process.env.CREATE_INFRA) return callback();
debug('initializing addon infrastructure');
var existingInfra = { version: 'none' };
if (fs.existsSync(paths.INFRA_VERSION_FILE)) {
existingInfra = safe.JSON.parse(fs.readFileSync(paths.INFRA_VERSION_FILE, 'utf8'));
if (!existingInfra) existingInfra = { version: 'corrupt' };
}
// short-circuit for the restart case
if (_.isEqual(infra, existingInfra)) {
debug('platform is uptodate at version %s', infra.version);
process.nextTick(function () { exports.events.emit(exports.EVENT_READY); });
return loadAddonVars(callback);
}
debug('Updating infrastructure from %s to %s', existingInfra.version, infra.version);
async.series([
stopContainers.bind(null, existingInfra),
createDockerNetwork,
startAddons.bind(null, existingInfra),
removeOldImages,
startApps.bind(null, existingInfra),
loadAddonVars,
fs.writeFile.bind(fs, paths.INFRA_VERSION_FILE, JSON.stringify(infra))
], function (error) {
if (error) return callback(error);
// give 30 seconds for the platform to "settle". For example, mysql might still be initing the
// database dir and we cannot call service scripts until that's done.
// TODO: make this smarter to not wait for 30secs for the crash-restart case
gPlatformReadyTimer = setTimeout(function () {
debug('emitting platform ready');
gPlatformReadyTimer = null;
exports.events.emit(exports.EVENT_READY);
}, 30000);
callback();
});
}
function uninitialize(callback) {
clearTimeout(gPlatformReadyTimer);
gPlatformReadyTimer = null;
callback();
}
function isReadySync() {
return gPlatformReadyTimer === null;
}
function removeOldImages(callback) {
debug('removing old addon images');
for (var imageName in infra.images) {
var image = infra.images[imageName];
debug('cleaning up images of %j', image);
var cmd = 'docker images "%s" | tail -n +2 | awk \'{ print $1 ":" $2 }\' | grep -v "%s" | xargs --no-run-if-empty docker rmi';
shell.execSync('removeOldImagesSync', util.format(cmd, image.repo, image.tag));
}
callback();
}
function stopContainers(existingInfra, callback) {
// TODO: be nice and stop addons cleanly (example, shutdown commands)
if (existingInfra.version !== infra.version) { // infra upgrade
debug('stopping all containers for infra upgrade');
shell.execSync('stopContainers', 'docker ps -qa | xargs --no-run-if-empty docker rm -f');
} else {
assert(typeof infra.images, 'object');
var changedAddons = [ ];
for (var imageName in infra.images) {
if (infra.images[imageName].tag !== existingInfra.images[imageName].tag) changedAddons.push(imageName);
}
debug('stopping addons for incremental infra update: %j', changedAddons);
shell.execSync('stopContainers', 'docker rm -f ' + changedAddons.join(' '));
}
callback();
}
function createDockerNetwork(callback) {
shell.execSync('createDockerNetwork', 'docker network create --subnet=172.18.0.0/16 cloudron || true', callback);
}
function startGraphite(callback) {
const tag = infra.images.graphite.tag;
const dataDir = paths.DATA_DIR;
const cmd = `docker run --restart=always -d --name="graphite" \
--net cloudron \
--net-alias 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 "${dataDir}/graphite:/app/data" \
--read-only -v /tmp -v /run "${tag}"`;
shell.execSync('startGraphite', cmd);
callback();
}
function startMysql(callback) {
const tag = infra.images.mysql.tag;
const dataDir = paths.DATA_DIR;
const rootPassword = hat(8 * 128);
if (!safe.fs.writeFileSync(paths.DATA_DIR + '/addons/mysql_vars.sh',
'MYSQL_ROOT_PASSWORD=' + rootPassword +'\nMYSQL_ROOT_HOST=172.18.0.1', 'utf8')) {
return callback(new Error('Could not create mysql var file:' + safe.error.message));
}
const cmd = `docker run --restart=always -d --name="mysql" \
--net cloudron \
--net-alias mysql \
-m 256m \
--memory-swap 512m \
-v "${dataDir}/mysql:/var/lib/mysql" \
-v "${dataDir}/addons/mysql_vars.sh:/etc/mysql/mysql_vars.sh:ro" \
--read-only -v /tmp -v /run "${tag}"`;
shell.execSync('startMysql', cmd);
callback();
}
function startPostgresql(callback) {
const tag = infra.images.postgresql.tag;
const dataDir = paths.DATA_DIR;
const rootPassword = hat(8 * 128);
if (!safe.fs.writeFileSync(paths.DATA_DIR + '/addons/postgresql_vars.sh', 'POSTGRESQL_ROOT_PASSWORD=' + rootPassword, 'utf8')) {
return callback(new Error('Could not create postgresql var file:' + safe.error.message));
}
const cmd = `docker run --restart=always -d --name="postgresql" \
--net cloudron \
--net-alias postgresql \
-m 100m \
--memory-swap 200m \
-v "${dataDir}/postgresql:/var/lib/postgresql" \
-v "${dataDir}/addons/postgresql_vars.sh:/etc/postgresql/postgresql_vars.sh:ro" \
--read-only -v /tmp -v /run "${tag}"`;
shell.execSync('startPostgresql', cmd);
callback();
}
function startMongodb(callback) {
const tag = infra.images.mongodb.tag;
const dataDir = paths.DATA_DIR;
const rootPassword = hat(8 * 128);
if (!safe.fs.writeFileSync(paths.DATA_DIR + '/addons/mongodb_vars.sh', 'MONGODB_ROOT_PASSWORD=' + rootPassword, 'utf8')) {
return callback(new Error('Could not create mongodb var file:' + safe.error.message));
}
const cmd = `docker run --restart=always -d --name="mongodb" \
--net cloudron \
--net-alias mongodb \
-m 100m \
--memory-swap 200m \
-v "${dataDir}/mongodb:/var/lib/mongodb" \
-v "${dataDir}/addons/mongodb_vars.sh:/etc/mongodb_vars.sh:ro" \
--read-only -v /tmp -v /run "${tag}"`;
shell.execSync('startMongodb', cmd);
callback();
}
function startMail(callback) {
// mail (note: 2525 is hardcoded in mail container and app use this port)
// MAIL_SERVER_NAME is the hostname of the mailserver i.e server uses these certs
// MAIL_DOMAIN is the domain for which this server is relaying mails
// mail container uses /app/data for backed up data and /run for restart-able data
const tag = infra.images.mail.tag;
const dataDir = paths.DATA_DIR;
const rootPassword = hat(8 * 128);
const fqdn = config.fqdn();
const mailFqdn = config.adminFqdn();
if (!safe.fs.writeFileSync(paths.DATA_DIR + '/addons/mail_vars.sh',
'MAIL_ROOT_USERNAME=no-reply\nMAIL_ROOT_PASSWORD=' + rootPassword, 'utf8')) {
return callback(new Error('Could not create mail var file:' + safe.error.message));
}
certificates.getAdminCertificatePath(function (error, certFilePath, keyFilePath) {
if (error) return callback(error);
const cmd = `docker run --restart=always -d --name="mail" \
--net cloudron \
--net-alias mail \
-m 75m \
--memory-swap 150m \
-e "MAIL_DOMAIN=${fqdn}" \
-e "MAIL_SERVER_NAME=${mailFqdn}" \
-v "${dataDir}/box/mail:/app/data" \
-v "${dataDir}/mail:/run" \
-v "${dataDir}/addons/mail_vars.sh:/etc/mail/mail_vars.sh:ro" \
-v "${certFilePath}:/etc/tls_cert.pem:ro" \
-v "${keyFilePath}:/etc/tls_key.pem:ro" \
-p 587:2525 \
-p 993:9993 \
-p 4190:4190 \
-p 25:2525 \
--read-only -v /tmp ${tag}`;
shell.execSync('startMail', cmd);
mailboxes.setupAliases(callback);
});
}
function startAddons(existingInfra, callback) {
var startFuncs = [ ];
if (existingInfra.version !== infra.version) {
debug('startAddons: no existing infra or infra upgrade. starting all addons');
startFuncs.push(startGraphite, startMysql, startPostgresql, startMongodb, startMail);
} else {
assert.strictEqual(typeof existingInfra.images, 'object');
if (infra.images.graphite.tag !== existingInfra.images.graphite.tag) startFuncs.push(startGraphite);
if (infra.images.mysql.tag !== existingInfra.images.mysql.tag) startFuncs.push(startMysql);
if (infra.images.postgresql.tag !== existingInfra.images.postgresql.tag) startFuncs.push(startPostgresql);
if (infra.images.mongodb.tag !== existingInfra.images.mongodb.tag) startFuncs.push(startMongodb);
if (infra.images.mail.tag !== existingInfra.images.mail.tag) startFuncs.push(startMail);
debug('startAddons: existing infra. incremental addon create %j', startFuncs.map(function (f) { return f.name; }));
}
async.series(startFuncs, callback);
}
function startApps(existingInfra, callback) {
if (existingInfra.version === infra.version) {
debug('startApp: apps are already uptodate');
callback();
} else if (existingInfra.version === 'none') {
debug('startApps: restoring installed apps');
apps.restoreInstalledApps(callback);
} else {
debug('startApps: reconfiguring installed apps');
apps.configureInstalledApps(callback);
}
}
function loadAddonVars(callback) {
gAddonVars = {
mail: ini.parse(fs.readFileSync(paths.DATA_DIR + '/addons/mail_vars.sh', 'utf8')),
postgresql: ini.parse(fs.readFileSync(paths.DATA_DIR + '/addons/postgresql_vars.sh', 'utf8')),
mysql: ini.parse(fs.readFileSync(paths.DATA_DIR + '/addons/mysql_vars.sh', 'utf8')),
mongodb: ini.parse(fs.readFileSync(paths.DATA_DIR + '/addons/mongodb_vars.sh', 'utf8'))
};
callback();
}
function mailConfig() {
if (!gAddonVars) return { username: 'no-reply', from: 'no-reply@' + config.fqdn(), password: 'doesnotwork' }; // for tests which don't run infra
return {
username: gAddonVars.mail.MAIL_ROOT_USERNAME,
from: '"Cloudron" <' + gAddonVars.mail.MAIL_ROOT_USERNAME + '@' + config.fqdn() + '>',
password: gAddonVars.mail.MAIL_ROOT_PASSWORD
};
}
+6 -6
View File
@@ -1,5 +1,3 @@
/* jslint node: true */
'use strict';
exports = module.exports = {
@@ -8,7 +6,8 @@ exports = module.exports = {
get: get,
UPDATE: 'update',
BACKUP: 'backup'
BACKUP: 'backup',
MIGRATE: 'migrate'
};
var assert = require('assert'),
@@ -18,12 +17,13 @@ var assert = require('assert'),
// otherwise no such operation is currently ongoing
var progress = {
update: null,
backup: null
backup: null,
migrate: null
};
// We use -1 for percentage to indicate errors
function set(tag, percent, message) {
assert(tag === exports.UPDATE || tag === exports.BACKUP);
assert.strictEqual(typeof tag, 'string');
assert.strictEqual(typeof percent, 'number');
assert.strictEqual(typeof message, 'string');
@@ -36,7 +36,7 @@ function set(tag, percent, message) {
}
function clear(tag) {
assert(tag === exports.UPDATE || tag === exports.BACKUP);
assert.strictEqual(typeof tag, 'string');
progress[tag] = null;
+109 -53
View File
@@ -16,7 +16,9 @@ exports = module.exports = {
stopApp: stopApp,
startApp: startApp,
exec: exec
exec: exec,
cloneApp: cloneApp
};
var apps = require('../apps.js'),
@@ -28,8 +30,7 @@ var apps = require('../apps.js'),
HttpSuccess = require('connect-lastmile').HttpSuccess,
paths = require('../paths.js'),
safe = require('safetydance'),
util = require('util'),
uuid = require('node-uuid');
util = require('util');
function auditSource(req) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
@@ -52,7 +53,8 @@ function removeInternalAppFields(app) {
iconUrl: app.iconUrl,
fqdn: app.fqdn,
memoryLimit: app.memoryLimit,
altDomain: app.altDomain
altDomain: app.altDomain,
xFrameOptions: app.xFrameOptions
};
}
@@ -90,82 +92,77 @@ function getAppIcon(req, res, next) {
});
}
/*
* Installs an app
* @bodyparam {string} appStoreId The id of the app to be installed
* @bodyparam {manifest} manifest The app manifest
* @bodyparam {string} password The user's password
* @bodyparam {string} location The subdomain where the app is to be installed
* @bodyparam {object} portBindings map from environment variable name to (public) host port. can be null.
If a value in manifest.tcpPorts is missing in portBindings, the port/service is disabled
* @bodyparam {icon} icon Base64 encoded image
*/
function installApp(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
var data = req.body;
if (!data) return next(new HttpError(400, 'Cannot parse data field'));
if (!data.manifest || typeof data.manifest !== 'object') return next(new HttpError(400, 'manifest is required'));
if (typeof data.appStoreId !== 'string') return next(new HttpError(400, 'appStoreId is required'));
// atleast one
if ('manifest' in data && typeof data.manifest !== 'object') return next(new HttpError(400, 'manifest must be an object'));
if ('appStoreId' in data && typeof data.appStoreId !== 'string') return next(new HttpError(400, 'appStoreId must be a string'));
if (!data.manifest && !data.appStoreId) return next(new HttpError(400, 'appStoreId or manifest is required'));
// 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 !== 'object') return next(new HttpError(400, 'accessRestriction is required'));
// optional
if (('portBindings' in data) && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
if ('icon' in data && typeof data.icon !== 'string') return next(new HttpError(400, 'icon is not a string'));
if (data.cert && typeof data.cert !== 'string') return next(new HttpError(400, 'cert must be a string'));
if (data.key && typeof data.key !== 'string') return next(new HttpError(400, 'key must be a string'));
// falsy values in cert and key unset the cert
if (data.key && typeof data.cert !== 'string') return next(new HttpError(400, 'cert must be a string'));
if (data.cert && typeof data.key !== 'string') return next(new HttpError(400, 'key must be a string'));
if (data.cert && !data.key) return next(new HttpError(400, 'key must be provided'));
if (!data.cert && data.key) return next(new HttpError(400, 'cert must be provided'));
if ('memoryLimit' in data && typeof data.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
// falsy value in altDomain unsets it
if (data.altDomain && typeof data.altDomain !== 'string') return next(new HttpError(400, 'altDomain must be 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();
if (data.xFrameOptions && typeof data.xFrameOptions !== 'string') return next(new HttpError(400, 'xFrameOptions must be a string'));
debug('Installing app id:%s storeid:%s loc:%s port:%j accessRestriction:%j memoryLimit:%s manifest:%j', appId, data.appStoreId, data.location, data.portBindings, data.accessRestriction, data.memoryLimit, data.manifest);
debug('Installing app :%j', data);
apps.install(appId, data.appStoreId, data.manifest, data.location, data.portBindings || null, data.accessRestriction, data.icon || null, data.cert || null, data.key || null, data.memoryLimit || 0, data.altDomain || null, auditSource(req), function (error) {
apps.install(data, auditSource(req), function (error, app) {
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.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error && error.reason === AppsError.BAD_CERTIFICATE) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.USER_REQUIRED) return next(new HttpError(400, 'accessRestriction must specify one user'));
if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(503, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, { id: appId } ));
next(new HttpSuccess(202, app));
});
}
/*
* Configure an app
* @bodyparam {string} password The user's password
* @bodyparam {string} location The subdomain where the app is to be installed
* @bodyparam {object} portBindings map from env to (public) host port. can be null.
If a value in manifest.tcpPorts is missing in portBindings, the port/service is disabled
*/
function configureApp(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
var data = req.body;
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 !== 'object') return next(new HttpError(400, 'accessRestriction is required'));
if (data.cert && typeof data.cert !== 'string') return next(new HttpError(400, 'cert must be a string'));
if (data.key && typeof data.key !== 'string') return next(new HttpError(400, 'key must be a string'));
if ('location' in data && typeof data.location !== 'string') return next(new HttpError(400, 'location must be string'));
if ('portBindings' in data && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
if ('accessRestriction' in data && typeof data.accessRestriction !== 'object') return next(new HttpError(400, 'accessRestriction must be an object'));
// falsy values in cert and key unset the cert
if (data.key && typeof data.cert !== 'string') return next(new HttpError(400, 'cert must be a string'));
if (data.cert && typeof data.key !== 'string') return next(new HttpError(400, 'key must be a string'));
if (data.cert && !data.key) return next(new HttpError(400, 'key must be provided'));
if (!data.cert && data.key) return next(new HttpError(400, 'cert must be provided'));
if ('memoryLimit' in data && typeof data.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
if (data.altDomain && typeof data.altDomain !== 'string') return next(new HttpError(400, 'altDomain must be a string'));
if (data.xFrameOptions && typeof data.xFrameOptions !== 'string') return next(new HttpError(400, 'xFrameOptions must be a string'));
debug('Configuring app id:%s location:%s bindings:%j accessRestriction:%j memoryLimit:%s', req.params.id, data.location, data.portBindings, data.accessRestriction, data.memoryLimit);
debug('Configuring app id:%s data:%j', req.params.id, data);
apps.configure(req.params.id, data.location, data.portBindings || null, data.accessRestriction, data.cert || null, data.key || null, data.memoryLimit || 0, data.altDomain || null, auditSource(req), function (error) {
apps.configure(req.params.id, data, auditSource(req), function (error) {
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
@@ -180,20 +177,50 @@ function configureApp(req, res, next) {
}
function restoreApp(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
var data = req.body;
debug('Restore app id:%s', req.params.id);
apps.restore(req.params.id, auditSource(req), function (error) {
if (!('backupId' in req.body)) return next(new HttpError(400, 'backupId is required'));
if (data.backupId !== null && typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be string or null'));
apps.restore(req.params.id, data, auditSource(req), function (error) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, { }));
});
}
function cloneApp(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
var data = req.body;
debug('Clone app id:%s', req.params.id);
if (typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be a string'));
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'));
apps.clone(req.params.id, data, auditSource(req), function (error, result) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201, { id: result.id }));
});
}
function backupApp(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
@@ -209,10 +236,6 @@ function backupApp(req, res, next) {
});
}
/*
* Uninstalls an app
* @bodyparam {string} id The id of the app to be uninstalled
*/
function uninstallApp(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
@@ -260,15 +283,18 @@ function updateApp(req, res, next) {
var data = req.body;
if (!data) return next(new HttpError(400, 'Cannot parse data field'));
if (!data.manifest || typeof data.manifest !== 'object') return next(new HttpError(400, 'manifest is required'));
if (('portBindings' in data) && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
// atleast one
if ('manifest' in data && typeof data.manifest !== 'object') return next(new HttpError(400, 'manifest must be an object'));
if ('appStoreId' in data && typeof data.appStoreId !== 'string') return next(new HttpError(400, 'appStoreId must be a string'));
if (!data.manifest && !data.appStoreId) return next(new HttpError(400, 'appStoreId or manifest is required'));
if ('portBindings' in data && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
if ('icon' in data && typeof data.icon !== 'string') return next(new HttpError(400, 'icon is not a string'));
if ('force' in data && typeof data.force !== 'boolean') return next(new HttpError(400, 'force must be a boolean'));
debug('Update app id:%s to manifest:%j with portBindings:%j', req.params.id, data.manifest, data.portBindings);
apps.update(req.params.id, data.force || false, data.manifest, data.portBindings || null, data.icon, auditSource(req), function (error) {
apps.update(req.params.id, req.body, auditSource(req), function (error) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
@@ -338,15 +364,37 @@ function getLogs(req, res, next) {
});
}
function demuxStream(stream, stdin) {
var header = null;
stream.on('readable', function() {
header = header || stream.read(4);
while (header !== null) {
var length = header.readUInt32BE(0);
if (length === 0) {
header = null;
return stdin.end(); // EOF
}
var payload = stream.read(length);
if (payload === null) break;
stdin.write(payload);
header = stream.read(4);
}
});
}
function exec(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
debug('Execing into app id:%s', req.params.id);
debug('Execing into app id:%s and cmd:%s', req.params.id, req.query.cmd);
var cmd = null;
if (req.query.cmd) {
cmd = safe.JSON.parse(req.query.cmd);
if (!util.isArray(cmd) && cmd.length < 1) return next(new HttpError(400, 'cmd must be array with atleast size 1'));
if (!util.isArray(cmd) || cmd.length < 1) return next(new HttpError(400, 'cmd must be array with atleast size 1'));
}
var columns = req.query.columns ? parseInt(req.query.columns, 10) : null;
@@ -367,8 +415,16 @@ function exec(req, res, next) {
req.clearTimeout();
res.sendUpgradeHandshake();
// When tty is disabled, the duplexStream has 2 separate streams. When enabled, it has stdout/stderr merged.
duplexStream.pipe(res.socket);
res.socket.pipe(duplexStream);
if (tty) {
res.socket.pipe(duplexStream); // in tty mode, the client always waits for server to exit
} else {
demuxStream(res.socket, duplexStream);
res.socket.on('error', function () { duplexStream.end(); });
res.socket.on('end', function () { duplexStream.end(); });
}
});
}
+2 -2
View File
@@ -3,7 +3,7 @@
exports = module.exports = {
get: get,
create: create,
download: download
createDownloadUrl: createDownloadUrl
};
var assert = require('assert'),
@@ -43,7 +43,7 @@ function create(req, res, next) {
});
}
function download(req, res, next) {
function createDownloadUrl(req, res, next) {
assert.strictEqual(typeof req.params.backupId, 'string');
backups.getRestoreUrl(req.params.backupId, function (error, result) {
+53 -15
View File
@@ -4,16 +4,16 @@ exports = module.exports = {
add: add,
get: get,
del: del,
getAllByUserId: getAllByUserId,
getAll: getAll,
addClientToken: addClientToken,
getClientTokens: getClientTokens,
delClientTokens: delClientTokens
delClientTokens: delClientTokens,
delToken: delToken
};
var assert = require('assert'),
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,
validUrl = require('valid-url');
@@ -27,8 +27,9 @@ 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'));
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'));
clients.add(data.appId, clients.TYPE_EXTERNAL, data.redirectURI, data.scope, function (error, result) {
if (error && error.reason === ClientsError.INVALID_SCOPE) return next(new HttpError(400, error.message));
if (error && error.reason === ClientsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201, result));
});
@@ -38,7 +39,7 @@ function get(req, res, next) {
assert.strictEqual(typeof req.params.clientId, 'string');
clients.get(req.params.clientId, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new HttpError(404, 'No such client'));
if (error && error.reason === ClientsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, result));
});
@@ -47,26 +48,49 @@ function get(req, res, next) {
function del(req, res, next) {
assert.strictEqual(typeof req.params.clientId, 'string');
clients.del(req.params.clientId, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new HttpError(404, 'no such client'));
clients.get(req.params.clientId, function (error, result) {
if (error && error.reason === ClientsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204, result));
// we do not allow to use the REST API to delete addon clients
if (result.type !== clients.TYPE_EXTERNAL) return next(new HttpError(405, 'Deleting app addon clients is not allowed.'));
clients.del(req.params.clientId, function (error, result) {
if (error && error.reason === ClientsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === ClientsError.NOT_ALLOWED) return next(new HttpError(405, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204, result));
});
});
}
function getAllByUserId(req, res, next) {
clients.getAllWithDetailsByUserId(req.user.id, function (error, result) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return next(new HttpError(500, error));
function getAll(req, res, next) {
clients.getAll(function (error, result) {
if (error && error.reason !== ClientsError.NOT_FOUND) return next(new HttpError(500, error));
next(new HttpSuccess(200, { clients: result }));
});
}
function addClientToken(req, res, next) {
assert.strictEqual(typeof req.params.clientId, 'string');
assert.strictEqual(typeof req.user, 'object');
var expiresAt = req.query.expiresAt ? parseInt(req.query.expiresAt, 10) : Date.now() + 24 * 60 * 60 * 1000; // default 1 day;
if (isNaN(expiresAt) || expiresAt <= Date.now()) return next(new HttpError(400, 'expiresAt must be a timestamp in the future'));
clients.addClientTokenByUserId(req.params.clientId, req.user.id, expiresAt, function (error, result) {
if (error && error.reason === ClientsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201, { token: result }));
});
}
function getClientTokens(req, res, next) {
assert.strictEqual(typeof req.params.clientId, 'string');
assert.strictEqual(typeof req.user, 'object');
clients.getClientTokensByUserId(req.params.clientId, req.user.id, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new HttpError(404, 'no such client'));
if (error && error.reason === ClientsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { tokens: result }));
});
@@ -77,8 +101,22 @@ function delClientTokens(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
clients.delClientTokensByUserId(req.params.clientId, req.user.id, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new HttpError(404, 'no such client'));
if (error && error.reason === ClientsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204));
});
}
function delToken(req, res, next) {
assert.strictEqual(typeof req.params.clientId, 'string');
assert.strictEqual(typeof req.params.tokenId, 'string');
assert.strictEqual(typeof req.user, 'object');
clients.delToken(req.params.clientId, req.params.tokenId, function (error) {
if (error && error.reason === ClientsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === ClientsError.INVALID_TOKEN) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204));
});
}
+43 -5
View File
@@ -5,13 +5,16 @@ exports = module.exports = {
setupTokenAuth: setupTokenAuth,
getStatus: getStatus,
reboot: reboot,
migrate: migrate,
getProgress: getProgress,
getConfig: getConfig,
update: update,
feedback: feedback
feedback: feedback,
checkForUpdates: checkForUpdates
};
var assert = require('assert'),
async = require('async'),
cloudron = require('../cloudron.js'),
CloudronError = cloudron.CloudronError,
config = require('../config.js'),
@@ -20,7 +23,9 @@ var assert = require('assert'),
HttpSuccess = require('connect-lastmile').HttpSuccess,
progress = require('../progress.js'),
mailer = require('../mailer.js'),
superagent = require('superagent');
superagent = require('superagent'),
updateChecker = require('../updatechecker.js'),
_ = require('underscore');
function auditSource(req) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
@@ -55,9 +60,7 @@ function activate(req, res, next) {
cloudron.activate(username, password, email, displayName, ip, auditSource(req), 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_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
// only in caas case do we have to notify the api server about activation
@@ -112,6 +115,31 @@ function reboot(req, res, next) {
cloudron.reboot();
}
function migrate(req, res, next) {
if (config.provider() !== 'caas') return next(new HttpError(422, 'Cannot use migrate API with this provider'));
if ('size' in req.body && typeof req.body.size !== 'string') return next(new HttpError(400, 'size must be string'));
if ('region' in req.body && typeof req.body.region !== 'string') return next(new HttpError(400, 'region must be string'));
if ('domain' in req.body) {
if (typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be string'));
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider must be string'));
}
debug('Migration requested domain:%s size:%s region:%s', req.body.domain, req.body.size, req.body.region);
var options = _.pick(req.body, 'domain', 'size', 'region');
if (Object.keys(options).length === 0) return next(new HttpError(400, 'no migrate option provided'));
cloudron.migrate(req.body, function (error) { // pass req.body because 'domain' can have arbitrary options
if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
});
}
function getConfig(req, res, next) {
cloudron.getConfig(function (error, cloudronConfig) {
if (error) return next(new HttpError(500, error));
@@ -125,12 +153,22 @@ function update(req, res, next) {
cloudron.updateToLatest(auditSource(req), function (error) {
if (error && error.reason === CloudronError.ALREADY_UPTODATE) return next(new HttpError(422, error.message));
if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === CloudronError.SELF_UPGRADE_NOT_SUPPORTED) return next(new HttpError(412, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
});
}
function checkForUpdates(req, res, next) {
async.series([
updateChecker.checkAppUpdates,
updateChecker.checkBoxUpdates
], function () {
next(new HttpSuccess(200, { update: updateChecker.getUpdateInfo() }));
});
}
function feedback(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
+1 -1
View File
@@ -19,7 +19,7 @@ function auditSource(req) {
}
function enabled(req, res, next) {
developer.enabled(function (error, enabled) {
developer.isEnabled(function (error, enabled) {
if (enabled) return next();
next(new HttpError(412, 'Developer mode not enabled'));
});
+2 -2
View File
@@ -20,7 +20,7 @@ function create(req, res, next) {
if (typeof req.body.name !== 'string') return next(new HttpError(400, 'name must be string'));
groups.create(req.body.name, function (error, group) {
if (error && error.reason === GroupError.BAD_NAME) return next(new HttpError(400, error.message));
if (error && error.reason === GroupError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === GroupError.ALREADY_EXISTS) return next(new HttpError(409, 'Already exists'));
if (error) return next(new HttpError(500, error));
@@ -45,7 +45,7 @@ function get(req, res, next) {
}
function list(req, res, next) {
groups.getAll(function (error, result) {
groups.getAllWithMembers(function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { groups: result }));
+2 -1
View File
@@ -9,9 +9,10 @@ exports = module.exports = {
eventlog: require('./eventlog.js'),
graphs: require('./graphs.js'),
groups: require('./groups.js'),
mailboxes: require('./mailboxes.js'),
oauth2: require('./oauth2.js'),
profile: require('./profile.js'),
settings: require('./settings.js'),
sysadmin: require('./sysadmin.js'),
settings: require('./settings.js'),
user: require('./user.js')
};
+91
View File
@@ -0,0 +1,91 @@
'use strict';
exports = module.exports = {
list: list,
get: get,
remove: remove,
create: create,
setAliases: setAliases,
getAliases: getAliases
};
var assert = require('assert'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
mailboxes = require('../mailboxes.js'),
MailboxError = mailboxes.MailboxError,
util = require('util');
function create(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.name !== 'string') return next(new HttpError(400, 'name must be string'));
mailboxes.add(req.body.name, function (error, mailbox) {
if (error && error.reason === MailboxError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === MailboxError.ALREADY_EXISTS) return next(new HttpError(409, 'Already exists'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201, mailbox));
});
}
function get(req, res, next) {
assert.strictEqual(typeof req.params.mailboxId, 'string');
mailboxes.get(req.params.mailboxId, function (error, result) {
if (error && error.reason === MailboxError.NOT_FOUND) return next(new HttpError(404, 'No such mailbox'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, result));
});
}
function list(req, res, next) {
mailboxes.getAll(function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { mailboxes: result }));
});
}
function remove(req, res, next) {
assert.strictEqual(typeof req.params.mailboxId, 'string');
mailboxes.del(req.params.mailboxId, function (error) {
if (error && error.reason === MailboxError.NOT_FOUND) return next(new HttpError(404, 'Mailbox not found'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204));
});
}
function setAliases(req, res, next) {
assert.strictEqual(typeof req.params.mailboxId, 'string');
if (!util.isArray(req.body.aliases)) return next(new HttpError(400, 'aliases must be an array'));
for (var i = 0; i < req.body.aliases.length; i++) {
if (typeof req.body.aliases[i] !== 'string') return next(new HttpError(400, 'alias must be a string'));
}
mailboxes.setAliases(req.params.mailboxId, req.body.aliases, function (error) {
if (error && error.reason === MailboxError.NOT_FOUND) return next(new HttpError(404, 'No such mailbox'));
if (error && error.reason === MailboxError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === MailboxError.ALREADY_EXISTS) return next(new HttpError(409, 'One or more alias already exist'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
});
}
function getAliases(req, res, next) {
assert.strictEqual(typeof req.params.mailboxId, 'string');
mailboxes.getAliases(req.params.mailboxId, function (error, aliases) {
if (error && error.reason === MailboxError.NOT_FOUND) return next(new HttpError(404, 'No such mailbox'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { aliases: aliases }));
});
}
+55 -33
View File
@@ -4,9 +4,9 @@ var appdb = require('../appdb'),
apps = require('../apps'),
assert = require('assert'),
authcodedb = require('../authcodedb'),
clientdb = require('../clientdb'),
clients = require('../clients'),
ClientsError = clients.ClientsError,
config = require('../config.js'),
constants = require('../constants.js'),
DatabaseError = require('../databaseerror'),
debug = require('debug')('box:routes/oauth2'),
eventlog = require('../eventlog.js'),
@@ -21,7 +21,8 @@ var appdb = require('../appdb'),
url = require('url'),
user = require('../user.js'),
UserError = user.UserError,
util = require('util');
util = require('util'),
_ = require('underscore');
function auditSource(req, appId) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
@@ -41,7 +42,7 @@ gServer.serializeClient(function (client, callback) {
});
gServer.deserializeClient(function (id, callback) {
clientdb.get(id, callback);
clients.get(id, callback);
});
@@ -76,7 +77,7 @@ gServer.grant(oauth2orize.grant.token({ scopeSeparator: ',' }, function (client,
var token = tokendb.generateToken();
var expires = Date.now() + 24 * 60 * 60 * 1000; // 1 day
tokendb.add(token, tokendb.PREFIX_USER + user.id, client.id, expires, client.scope, function (error) {
tokendb.add(token, user.id, client.id, expires, client.scope, function (error) {
if (error) return callback(error);
debug('grant token: new access token for client %s token %s', client.id, token);
@@ -106,7 +107,7 @@ gServer.exchange(oauth2orize.exchange.code(function (client, code, redirectURI,
var token = tokendb.generateToken();
var expires = Date.now() + 24 * 60 * 60 * 1000; // 1 day
tokendb.add(token, tokendb.PREFIX_USER + authCode.userId, authCode.clientId, expires, client.scope, function (error) {
tokendb.add(token, authCode.userId, authCode.clientId, expires, client.scope, function (error) {
if (error) return callback(error);
debug('exchange: new access token for client %s token %s', client.id, token);
@@ -201,13 +202,13 @@ function loginForm(req, res) {
});
}
clientdb.get(u.query.client_id, function (error, result) {
clients.get(u.query.client_id, function (error, result) {
if (error) return sendError(req, res, 'Unknown OAuth client');
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');
case clients.TYPE_BUILT_IN: return render(result.appId, '/api/v1/cloudron/avatar');
case clients.TYPE_EXTERNAL: return render(result.appId, '/api/v1/cloudron/avatar');
case clients.TYPE_SIMPLE_AUTH: return sendError(req, res, 'Unknown OAuth client');
default: break;
}
@@ -306,16 +307,20 @@ function accountSetup(req, res, next) {
user.getByResetToken(req.body.resetToken, function (error, userObject) {
if (error) return sendError(req, res, 'Invalid Reset Token');
userObject.username = req.body.username;
userObject.displayName = req.body.displayName;
user.update(userObject.id, userObject.username, userObject.email, userObject.displayName, auditSource(req), function (error) {
var data = _.pick(req.body, 'username', 'displayName');
user.update(userObject.id, data, auditSource(req), function (error) {
if (error && error.reason === UserError.ALREADY_EXISTS) return renderAccountSetupSite(res, req, userObject, 'Username already exists');
if (error && error.reason === UserError.BAD_FIELD) return renderAccountSetupSite(res, req, userObject, error.message);
if (error && error.reason === UserError.NOT_FOUND) return renderAccountSetupSite(res, req, userObject, 'No such user');
if (error) return next(new HttpError(500, error));
userObject.username = req.body.username;
userObject.displayName = req.body.displayName;
// setPassword clears the resetToken
user.setPassword(userObject.id, req.body.password, function (error, result) {
if (error && error.reason === UserError.BAD_PASSWORD) return renderAccountSetupSite(res, req, userObject, 'Password invalid');
if (error && error.reason === UserError.BAD_FIELD) return renderAccountSetupSite(res, req, userObject, error.message);
if (error) return next(new HttpError(500, error));
res.redirect(util.format('%s?accessToken=%s&expiresAt=%s', config.adminOrigin(), result.token, result.expiresAt));
@@ -357,7 +362,7 @@ function passwordReset(req, res, next) {
// setPassword clears the resetToken
user.setPassword(userObject.id, req.body.password, function (error, result) {
if (error && error.reason === UserError.BAD_PASSWORD) return next(new HttpError(406, 'Password does not meet the requirements'));
if (error && error.reason === UserError.BAD_FIELD) return next(new HttpError(406, error.message));
if (error) return next(new HttpError(500, error));
res.redirect(util.format('%s?accessToken=%s&expiresAt=%s', config.adminOrigin(), result.token, result.expiresAt));
@@ -399,8 +404,8 @@ var authorization = [
gServer.authorization({}, function (clientId, redirectURI, callback) {
debug('authorization: client %s with callback to %s.', clientId, redirectURI);
clientdb.get(clientId, function (error, client) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
clients.get(clientId, function (error, client) {
if (error && error.reason === ClientsError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
// ignore the origin passed into form the client, but use the one from the clientdb
@@ -414,12 +419,12 @@ var authorization = [
// Handle our different types of oauth clients
var type = req.oauth2.client.type;
if (type === clientdb.TYPE_ADMIN) {
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource(req, 'admin'), { userId: req.oauth2.user.id });
if (type === clients.TYPE_EXTERNAL || type === clients.TYPE_BUILT_IN) {
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource(req, req.oauth2.client.appId), { userId: req.oauth2.user.id });
return next();
} else if (type === clients.TYPE_SIMPLE_AUTH) {
return sendError(req, res, 'Unknown OAuth client.');
}
if (type === clientdb.TYPE_EXTERNAL) return next();
if (type === clientdb.TYPE_SIMPLE_AUTH) return sendError(req, res, 'Unknown OAuth client.');
appdb.get(req.oauth2.client.appId, function (error, appObject) {
if (error) return sendErrorPageOrRedirect(req, res, 'Invalid request. Unknown app for this client_id.');
@@ -451,6 +456,31 @@ var token = [
gServer.errorHandler()
];
// tests if all requestedScopes are attached to the request
function validateRequestedScopes(req, requestedScopes) {
assert.strictEqual(typeof req, 'object');
assert(Array.isArray(requestedScopes));
if (!req.authInfo || !req.authInfo.scope) return new Error('No scope found');
var scopes = req.authInfo.scope.split(',');
// check for roles separately
if (requestedScopes.indexOf(clients.SCOPE_ROLE_SDK) !== -1 && scopes.indexOf(clients.SCOPE_ROLE_SDK) === -1) {
return new Error('Missing required scope role "' + clients.SCOPE_ROLE_SDK + '"');
}
if (scopes.indexOf('*') !== -1) return null;
for (var i = 0; i < requestedScopes.length; ++i) {
if (scopes.indexOf(requestedScopes[i]) === -1) {
debug('scope: missing scope "%s".', requestedScopes[i]);
return new Error('Missing required scope "' + requestedScopes[i] + '"');
}
}
return null;
}
// The scope middleware provides an auth middleware for routes.
//
@@ -470,17 +500,8 @@ function scope(requestedScope) {
return [
passport.authenticate(['bearer'], { session: false }),
function (req, res, next) {
if (!req.authInfo || !req.authInfo.scope) return next(new HttpError(401, 'No scope found'));
if (req.authInfo.scope === '*') return next();
var scopes = req.authInfo.scope.split(',');
for (var i = 0; i < requestedScopes.length; ++i) {
if (scopes.indexOf(requestedScopes[i]) === -1) {
debug('scope: missing scope "%s".', requestedScopes[i]);
return next(new HttpError(401, 'Missing required scope "' + requestedScopes[i] + '"'));
}
}
var error = validateRequestedScopes(req, requestedScopes);
if (error) return next(new HttpError(401, error.message));
next();
}
@@ -511,6 +532,7 @@ exports = module.exports = {
accountSetup: accountSetup,
authorization: authorization,
token: token,
validateRequestedScopes: validateRequestedScopes,
scope: scope,
csrf: csrf
};
+16 -33
View File
@@ -8,12 +8,11 @@ exports = module.exports = {
};
var assert = require('assert'),
groups = require('../groups.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
user = require('../user.js'),
tokendb = require('../tokendb.js'),
UserError = user.UserError;
UserError = user.UserError,
_ = require('underscore');
function auditSource(req) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
@@ -23,26 +22,14 @@ function auditSource(req) {
function get(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
var result = {};
result.id = req.user.id;
result.tokenType = req.user.tokenType;
if (req.user.tokenType === tokendb.TYPE_USER || req.user.tokenType === tokendb.TYPE_DEV) {
result.username = req.user.username;
result.email = req.user.email;
result.displayName = req.user.displayName;
result.showTutorial = req.user.showTutorial;
groups.isMember(groups.ADMIN_GROUP_ID, req.user.id, function (error, isAdmin) {
if (error) return next(new HttpError(500, error));
result.admin = isAdmin;
next(new HttpSuccess(200, result));
});
} else {
next(new HttpSuccess(200, result));
}
next(new HttpSuccess(200, {
id: req.user.id,
username: req.user.username,
email: req.user.email,
admin: req.user.admin,
displayName: req.user.displayName,
showTutorial: req.user.showTutorial
}));
}
function update(req, res, next) {
@@ -52,12 +39,11 @@ function update(req, res, next) {
if ('email' in req.body && typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string'));
if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be string'));
if (req.user.tokenType !== tokendb.TYPE_USER) return next(new HttpError(403, 'Token type not allowed'));
var data = _.pick(req.body, 'email', 'displayName');
user.update(req.user.id, req.user.username, req.body.email || req.user.email, req.body.displayName || req.user.displayName, auditSource(req), function (error) {
if (error && error.reason === UserError.BAD_USERNAME) return next(new HttpError(400, error.message));
if (error && error.reason === UserError.BAD_EMAIL) return next(new HttpError(400, error.message));
if (error && error.reason === UserError.ALREADY_EXISTS) return next(new HttpError(409, 'Already exists'));
user.update(req.user.id, data, auditSource(req), function (error) {
if (error && error.reason === UserError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === UserError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
if (error) return next(new HttpError(500, error));
@@ -69,13 +55,10 @@ function changePassword(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.user, 'object');
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'API call requires the users old password.'));
if (typeof req.body.newPassword !== 'string') return next(new HttpError(400, 'API call requires the users new password.'));
if (req.user.tokenType !== tokendb.TYPE_USER) return next(new HttpError(403, 'Token type not allowed'));
if (typeof req.body.newPassword !== 'string') return next(new HttpError(400, 'newPassword must be a string'));
user.setPassword(req.user.id, req.body.newPassword, function (error) {
if (error && error.reason === UserError.BAD_PASSWORD) return next(new HttpError(400, error.message));
if (error && error.reason === UserError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(403, 'Wrong password'));
if (error) return next(new HttpError(500, error));
+51 -3
View File
@@ -17,6 +17,10 @@ exports = module.exports = {
setBackupConfig: setBackupConfig,
getTimeZone: getTimeZone,
setTimeZone: setTimeZone,
getAppstoreConfig: getAppstoreConfig,
setAppstoreConfig: setAppstoreConfig,
setCertificate: setCertificate,
setAdminCertificate: setAdminCertificate
@@ -45,7 +49,7 @@ function setAutoupdatePattern(req, res, next) {
if (typeof req.body.pattern !== 'string') return next(new HttpError(400, 'pattern is required'));
settings.setAutoupdatePattern(req.body.pattern, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, 'Invalid pattern'));
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
@@ -58,15 +62,17 @@ function setCloudronName(req, res, next) {
if (typeof req.body.name !== 'string') return next(new HttpError(400, 'name is required'));
settings.setCloudronName(req.body.name, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, 'Invalid name'));
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
next(new HttpSuccess(202));
});
}
function getCloudronName(req, res, next) {
settings.getCloudronName(function (error, name) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { name: name }));
});
}
@@ -74,10 +80,24 @@ function getCloudronName(req, res, next) {
function getTimeZone(req, res, next) {
settings.getTimeZone(function (error, tz) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { timeZone: tz }));
});
}
function setTimeZone(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.timeZone !== 'string') return next(new HttpError(400, 'timeZone is required'));
settings.setTimeZone(req.body.timeZone, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
});
}
function setCloudronAvatar(req, res, next) {
assert.strictEqual(typeof req.files, 'object');
@@ -86,6 +106,7 @@ function setCloudronAvatar(req, res, next) {
settings.setCloudronAvatar(avatar, function (error) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
});
}
@@ -144,6 +165,33 @@ function setBackupConfig(req, res, next) {
});
}
function getAppstoreConfig(req, res, next) {
settings.getAppstoreConfig(function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, result));
});
}
function setAppstoreConfig(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.userId !== 'string') return next(new HttpError(400, 'userId is required'));
if (typeof req.body.token !== 'string') return next(new HttpError(400, 'token is required'));
var options = {
userId: req.body.userId,
token: req.body.token
};
settings.setAppstoreConfig(options, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
});
}
// default fallback cert
function setCertificate(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
+5 -7
View File
@@ -36,6 +36,7 @@ function update(req, res, next) {
cloudron.updateToLatest(auditSource, function (error) {
if (error && error.reason === CloudronError.ALREADY_UPTODATE) return next(new HttpError(422, error.message));
if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === CloudronError.SELF_UPGRADE_NOT_SUPPORTED) return next(new HttpError(412, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
@@ -45,12 +46,9 @@ function update(req, res, next) {
function retire(req, res, next) {
debug('triggering retire');
// 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.retire(function (error) {
if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
cloudron.retire('migrate', { }, function (error) {
if (error) console.error('Retire failed.', error);
});
next(new HttpSuccess(202, {}));
}
+247 -330
View File
@@ -1,7 +1,7 @@
'use strict';
/* jslint node:true */
/* global it:false */
/* global xit:false */
/* global describe:false */
/* global before:false */
/* global after:false */
@@ -12,7 +12,7 @@ var appdb = require('../../appdb.js'),
path = require('path'),
async = require('async'),
child_process = require('child_process'),
clientdb = require('../../clientdb.js'),
clients = require('../../clients.js'),
config = require('../../config.js'),
constants = require('../../constants.js'),
database = require('../../database.js'),
@@ -23,18 +23,18 @@ var appdb = require('../../appdb.js'),
http = require('http'),
https = require('https'),
js2xml = require('js2xmlparser'),
ldap = require('../../ldap.js'),
net = require('net'),
nock = require('nock'),
paths = require('../../paths.js'),
redis = require('redis'),
superagent = require('superagent'),
safe = require('safetydance'),
server = require('../../server.js'),
settings = require('../../settings.js'),
simpleauth = require('../../simpleauth.js'),
superagent = require('superagent'),
taskmanager = require('../../taskmanager.js'),
tokendb = require('../../tokendb.js'),
url = require('url'),
util = require('util'),
uuid = require('node-uuid'),
_ = require('underscore');
@@ -42,9 +42,9 @@ var SERVER_URL = 'http://localhost:' + config.get('port');
// Test image information
var TEST_IMAGE_REPO = 'cloudron/test';
var TEST_IMAGE_TAG = '11.0.0';
var TEST_IMAGE_TAG = '16.0.0';
var TEST_IMAGE = TEST_IMAGE_REPO + ':' + TEST_IMAGE_TAG;
var TEST_IMAGE_ID = child_process.execSync('docker inspect --format={{.Id}} ' + TEST_IMAGE).toString('utf8').trim();
// var TEST_IMAGE_ID = child_process.execSync('docker inspect --format={{.Id}} ' + TEST_IMAGE).toString('utf8').trim();
var APP_STORE_ID = 'test', APP_ID;
var APP_LOCATION = 'appslocation';
@@ -59,7 +59,7 @@ APP_MANIFEST_1.dockerImage = TEST_IMAGE;
APP_MANIFEST_1.singleUser = true;
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='admin@me.com';
var USER_1_ID = null, USERNAME_1 = 'user', PASSWORD_1 = 'Foobar?1338', EMAIL_1 ='user@me.com';
var USER_1_ID = null, USERNAME_1 = 'user', EMAIL_1 ='user@me.com';
var token = null; // authentication token
var token_1 = null;
@@ -104,52 +104,74 @@ function startDockerProxy(interceptor, callback) {
}).listen(5687, callback);
}
function checkAddons(appEntry, done) {
async.retry({ times: 15, interval: 3000 }, function (callback) {
// this was previously written with superagent but it was getting sporadic EPIPE
var req = http.get({ hostname: 'localhost', port: appEntry.httpPort, path: '/check_addons?username=' + USERNAME + '&password=' + PASSWORD });
req.on('error', callback);
req.on('response', function (res) {
if (res.statusCode !== 200) return callback('app returned non-200 status : ' + res.statusCode);
var d = '';
res.on('data', function (chunk) { d += chunk.toString('utf8'); });
res.on('end', function () {
var body = JSON.parse(d);
delete body.recvmail; // unclear why dovecot mail delivery won't work
delete body.stdenv; // cannot access APP_ORIGIN
for (var key in body) {
if (body[key] !== 'OK') return callback('Not done yet: ' + JSON.stringify(body));
}
callback();
});
});
req.end();
}, done);
}
function checkRedis(containerId, done) {
var redisIp, exportedRedisPort;
docker.getContainer(containerId).inspect(function (error, data) {
expect(error).to.not.be.ok();
expect(data).to.be.ok();
redisIp = safe.query(data, 'NetworkSettings.Networks.cloudron.IPAddress');
expect(redisIp).to.be.ok();
exportedRedisPort = safe.query(data, 'NetworkSettings.Ports.6379/tcp');
expect(exportedRedisPort).to.be(null);
done();
});
}
describe('Apps', function () {
this.timeout(50000);
this.timeout(100000);
var dockerProxy;
var imageDeleted = false;
var imageCreated = false;
before(function (done) {
console.log('Starting addons, this can take 10 seconds');
dockerProxy = startDockerProxy(function interceptor(req, res) {
if (req.method === 'POST' && req.url === '/images/create?fromImage=' + encodeURIComponent(TEST_IMAGE_REPO) + '&tag=' + TEST_IMAGE_TAG) {
imageCreated = true;
res.writeHead(200);
res.end();
return true;
} else if (req.method === 'DELETE' && req.url === '/images/' + TEST_IMAGE + '?force=false&noprune=false') {
imageDeleted = true;
res.writeHead(200);
res.end();
return true;
}
return false;
}, done);
});
after(function (done) {
// child_process.exec('docker rm -f mysql; docker rm -f postgresql; docker rm -f mongodb; docker rm -f mail', function (error) {
child_process.exec('docker ps | awk \'{print $1}\' | xargs docker rm -f', function () {
dockerProxy.close(done);
});
});
/*
Individual sub category setup and cleanup
*/
function setup(done) {
config._reset();
process.env.CREATE_INFRA = 1;
safe.fs.unlinkSync(paths.INFRA_VERSION_FILE);
child_process.execSync('docker ps -qa | xargs --no-run-if-empty docker rm -f');
async.series([
// first clear, then start server. otherwise, taskmanager spins up tasks for obsolete appIds
database.initialize,
database._clear,
server.start.bind(server),
ldap.start,
simpleauth.start,
function (callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
@@ -188,58 +210,104 @@ describe('Apps', function () {
token_1 = tokendb.generateToken();
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_1, tokendb.PREFIX_USER + USER_1_ID, 'test-client-id', Date.now() + 100000, '*', callback);
tokendb.add(token_1, USER_1_ID, 'test-client-id', Date.now() + 100000, '*', callback);
},
function (callback) {
dockerProxy = startDockerProxy(function interceptor(req, res) {
if (req.method === 'POST' && req.url === '/images/create?fromImage=' + encodeURIComponent(TEST_IMAGE_REPO) + '&tag=' + TEST_IMAGE_TAG) {
imageCreated = true;
res.writeHead(200);
res.end();
return true;
} else if (req.method === 'DELETE' && req.url === '/images/' + TEST_IMAGE + '?force=false&noprune=false') {
imageDeleted = true;
res.writeHead(200);
res.end();
return true;
}
return false;
}, callback);
},
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }),
settings.setTlsConfig.bind(null, { provider: 'caas' }),
settings.setBackupConfig.bind(null, { provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' })
], done);
}
], function (error) {
if (error) return done(error);
console.log('This test can take ~40 seconds to start as it waits for infra to be ready');
setTimeout(done, 40000);
});
});
after(function (done) {
delete process.env.CREATE_INFRA;
// child_process.execSync('docker ps -qa | xargs --no-run-if-empty docker rm -f');
dockerProxy.close(function () { });
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([
taskmanager.stopPendingTasks,
taskmanager.waitForPendingTasks,
server.stop,
ldap.stop,
simpleauth.stop,
config._reset,
], done);
}
});
describe('App API', function () {
this.timeout(50000);
before(setup);
after(function (done) {
APP_ID = null;
cleanup(done);
appdb._clear(done); // TODO: test proper uninstall (requires mock for aws)
});
it('app install fails - missing manifest', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, password: PASSWORD })
.send({ password: PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('manifest is required');
expect(res.body.message).to.eql('appStoreId or manifest is required');
done();
});
});
it('app install fails - missing appId', function (done) {
it('app install fails - null manifest', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, password: PASSWORD })
.send({ manifest: null, password: PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('appStoreId is required');
expect(res.body.message).to.eql('appStoreId or manifest is required');
done();
});
});
it('app install fails - invalid json', function (done) {
it('app install fails - bad manifest format', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: 'epic', password: PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('manifest must be an object');
done();
});
});
it('app install fails - empty appStoreId format', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: null, appStoreId: '', password: PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('appStoreId or manifest is required');
done();
});
});
it('app install fails - invalid json', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send('garbage')
@@ -252,7 +320,7 @@ describe('Apps', function () {
it('app install fails - invalid location', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: '!awesome', accessRestriction: null })
.send({ manifest: APP_MANIFEST, password: PASSWORD, location: '!awesome', accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('Hostname can only contain alphanumerics and hyphen');
@@ -263,7 +331,7 @@ describe('Apps', function () {
it('app install fails - invalid location type', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: 42, accessRestriction: null })
.send({ manifest: APP_MANIFEST, password: PASSWORD, location: 42, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('location is required');
@@ -274,7 +342,7 @@ describe('Apps', function () {
it('app install fails - reserved admin location', function (done) {
superagent.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: null })
.send({ manifest: APP_MANIFEST, password: PASSWORD, location: constants.ADMIN_LOCATION, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql(constants.ADMIN_LOCATION + ' is reserved');
@@ -285,7 +353,7 @@ describe('Apps', function () {
it('app install fails - reserved api location', function (done) {
superagent.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: null })
.send({ manifest: APP_MANIFEST, password: PASSWORD, location: constants.API_LOCATION, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql(constants.API_LOCATION + ' is reserved');
@@ -296,7 +364,7 @@ describe('Apps', function () {
it('app install fails - portBindings must be object', function (done) {
superagent.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: null })
.send({ manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: 23, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('portBindings must be an object');
@@ -307,7 +375,7 @@ describe('Apps', function () {
it('app install fails - accessRestriction is required', function (done) {
superagent.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({ manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: {} })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('accessRestriction is required');
@@ -318,7 +386,7 @@ describe('Apps', function () {
it('app install fails - accessRestriction type is wrong', function (done) {
superagent.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: '' })
.send({ manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: {}, accessRestriction: '' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('accessRestriction is required');
@@ -329,7 +397,7 @@ describe('Apps', function () {
it('app install fails - accessRestriction no users not allowed', function (done) {
superagent.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 })
.send({ manifest: APP_MANIFEST_1, password: PASSWORD, location: APP_LOCATION, portBindings: {}, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('accessRestriction must specify one user');
@@ -340,7 +408,7 @@ describe('Apps', function () {
it('app install fails - accessRestriction too many users not allowed', function (done) {
superagent.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' ] } })
.send({ manifest: APP_MANIFEST_1, password: PASSWORD, location: APP_LOCATION, portBindings: {}, accessRestriction: { users: [ 'one', 'two' ] } })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('accessRestriction must specify one user');
@@ -351,50 +419,64 @@ describe('Apps', function () {
it('app install fails for non admin', function (done) {
superagent.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: null })
.send({ manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
});
});
it('app install fails due to purchase failure', function (done) {
var fake = nock(config.apiServerOrigin()).post('/api/v1/apps/test/purchase?token=APPSTORE_TOKEN').reply(402, {});
it('app install fails because manifest download fails', function (done) {
var fake = nock(config.apiServerOrigin()).get('/api/v1/apps/test').reply(404, {});
superagent.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: null })
.send({ appStoreId: APP_STORE_ID, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: { users: [ 'someuser' ], groups: [] } })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(fake.isDone()).to.be.ok();
done();
});
});
it('app install fails due to purchase failure', function (done) {
var fake1 = nock(config.apiServerOrigin()).get('/api/v1/apps/test').reply(200, { manifest: APP_MANIFEST });
var fake2 = nock(config.apiServerOrigin()).post('/api/v1/apps/test/purchase?token=APPSTORE_TOKEN').reply(402, {});
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(402);
expect(fake.isDone()).to.be.ok();
expect(fake1.isDone()).to.be.ok();
expect(fake2.isDone()).to.be.ok();
done();
});
});
it('app install succeeds with purchase', function (done) {
var fake = nock(config.apiServerOrigin()).post('/api/v1/apps/test/purchase?token=APPSTORE_TOKEN').reply(201, {});
var fake1 = nock(config.apiServerOrigin()).get('/api/v1/apps/test').reply(200, { manifest: APP_MANIFEST });
var fake2 = nock(config.apiServerOrigin()).post('/api/v1/apps/test/purchase?token=APPSTORE_TOKEN').reply(201, {});
superagent.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: { users: [ 'someuser' ], groups: [] } })
.send({ appStoreId: APP_STORE_ID, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: { users: [ 'someuser' ], groups: [] } })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(res.body.id).to.be.a('string');
APP_ID = res.body.id;
expect(fake.isDone()).to.be.ok();
expect(fake1.isDone()).to.be.ok();
expect(fake2.isDone()).to.be.ok();
done();
});
});
it('app install fails because of conflicting location', function (done) {
var fake = nock(config.apiServerOrigin()).post('/api/v1/apps/test/purchase?token=APPSTORE_TOKEN').reply(201, {});
superagent.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: null })
.send({ manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
expect(fake.isDone()).to.be.ok();
done();
});
});
@@ -492,23 +574,23 @@ describe('Apps', function () {
});
it('app install succeeds already purchased', function (done) {
var fake = nock(config.apiServerOrigin()).post('/api/v1/apps/test/purchase?token=APPSTORE_TOKEN').reply(200, {});
var fake1 = nock(config.apiServerOrigin()).get('/api/v1/apps/test').reply(200, { manifest: APP_MANIFEST });
var fake2 = nock(config.apiServerOrigin()).post('/api/v1/apps/test/purchase?token=APPSTORE_TOKEN').reply(200, {});
superagent.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: null })
.send({ appStoreId: APP_STORE_ID, password: PASSWORD, location: APP_LOCATION_2, portBindings: null, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(res.body.id).to.be.a('string');
APP_ID = res.body.id;
expect(fake.isDone()).to.be.ok();
expect(fake1.isDone()).to.be.ok();
expect(fake2.isDone()).to.be.ok();
done();
});
});
it('app install succeeds without password but developer token', function (done) {
var fake = nock(config.apiServerOrigin()).post('/api/v1/apps/test/purchase?token=APPSTORE_TOKEN').reply(201, {});
settings.setDeveloperMode(true, function (error) {
expect(error).to.be(null);
@@ -517,7 +599,7 @@ describe('Apps', function () {
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(200);
expect(result.body.expiresAt).to.be.a('number');
expect(new Date(result.body.expiresAt).toString()).to.not.be('Invalid Date');
expect(result.body.token).to.be.a('string');
// overwrite non dev token
@@ -525,11 +607,10 @@ describe('Apps', function () {
superagent.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: null })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION+APP_LOCATION, portBindings: null, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(res.body.id).to.be.a('string');
expect(fake.isDone()).to.be.ok();
APP_ID = res.body.id;
done();
});
@@ -560,8 +641,6 @@ describe('Apps', function () {
imageCreated = false;
async.series([
setup,
function (callback) {
apiHockInstance
.get('/api/v1/apps/' + APP_STORE_ID + '/versions/' + APP_MANIFEST.version + '/icon')
@@ -590,7 +669,6 @@ describe('Apps', function () {
APP_ID = null;
async.series([
cleanup,
apiHockServer.close.bind(apiHockServer),
awsHockServer.close.bind(awsHockServer)
], done);
@@ -599,7 +677,8 @@ describe('Apps', function () {
var appResult = null /* the json response */, appEntry = null /* entry from database */;
it('can install test app', function (done) {
var fake = nock(config.apiServerOrigin()).post('/api/v1/apps/test/purchase?token=APPSTORE_TOKEN').reply(201, {});
var fake1 = nock(config.apiServerOrigin()).get('/api/v1/apps/test').reply(200, { manifest: APP_MANIFEST });
var fake2 = nock(config.apiServerOrigin()).post('/api/v1/apps/test/purchase?token=APPSTORE_TOKEN').reply(200, {});
var count = 0;
function checkInstallStatus() {
@@ -616,12 +695,13 @@ describe('Apps', function () {
superagent.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: null })
.send({ appStoreId: APP_STORE_ID, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(fake.isDone()).to.be.ok();
expect(fake1.isDone()).to.be.ok();
expect(fake2.isDone()).to.be.ok();
expect(res.body.id).to.be.a('string');
expect(res.body.id).to.be.eql(APP_ID);
APP_ID = res.body.id;
checkInstallStatus();
});
});
@@ -650,15 +730,9 @@ describe('Apps', function () {
expect(data.Config.Env).to.contain('CLOUDRON=1');
expect(data.Config.Env).to.contain('APP_ORIGIN=https://' + config.appFqdn(APP_LOCATION));
expect(data.Config.Env).to.contain('APP_DOMAIN=' + config.appFqdn(APP_LOCATION));
expect(data.Config.Hostname).to.be(APP_LOCATION);
clientdb.getByAppIdAndType(appResult.id, clientdb.TYPE_OAUTH, function (error, client) {
expect(error).to.not.be.ok();
expect(client.id.length).to.be(40); // cid- + 32 hex chars (128 bits) + 4 hyphens
expect(client.clientSecret.length).to.be(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);
done();
});
// Hostname must not be set of app fqdn or app location!
expect(data.Config.Hostname).to.not.contain(APP_LOCATION);
done();
});
});
@@ -704,144 +778,50 @@ describe('Apps', function () {
});
});
var redisIp, exportedRedisPort;
it('installation - redis addon created', function (done) {
docker.getContainer('redis-' + APP_ID).inspect(function (error, data) {
expect(error).to.not.be.ok();
expect(data).to.be.ok();
redisIp = safe.query(data, 'NetworkSettings.IPAddress');
expect(redisIp).to.be.ok();
exportedRedisPort = safe.query(data, 'NetworkSettings.Ports.6379/tcp[0].HostPort');
expect(exportedRedisPort).to.be.ok();
it('installation - app responnds to http request', function (done) {
superagent.get('http://localhost:' + appEntry.httpPort).end(function (err, res) {
expect(!err).to.be.ok();
expect(res.statusCode).to.equal(200);
expect(res.body.status).to.be('OK');
done();
});
});
it('installation - redis addon config', function (done) {
docker.getContainer(appEntry.containerId).inspect(function (error, data) {
var redisUrl = null;
data.Config.Env.forEach(function (env) { if (env.indexOf('REDIS_URL=') === 0) redisUrl = env.split('=')[1]; });
expect(redisUrl).to.be.ok();
var urlp = url.parse(redisUrl);
var password = urlp.auth.split(':')[1];
expect(data.Config.Env).to.contain('REDIS_PORT=6379');
expect(data.Config.Env).to.contain('REDIS_HOST=redis-' + APP_ID);
expect(data.Config.Env).to.contain('REDIS_PASSWORD=' + password);
expect(urlp.hostname).to.be('redis-' + APP_ID);
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) {
expect(err).to.not.be.ok();
expect(reply.toString()).to.be('value');
client.end();
done();
});
});
});
it('installation - mysql addon config', function (done) {
it('installation - oauth addon config', function (done) {
var appContainer = docker.getContainer(appEntry.containerId);
appContainer.inspect(function (error, data) {
var mysqlUrl = null;
data.Config.Env.forEach(function (env) { if (env.indexOf('MYSQL_URL=') === 0) mysqlUrl = env.split('=')[1]; });
expect(mysqlUrl).to.be.ok();
expect(error).to.not.be.ok();
var urlp = url.parse(mysqlUrl);
var username = urlp.auth.split(':')[0];
var password = urlp.auth.split(':')[1];
var dbname = urlp.path.substr(1);
expect(data.Config.Env).to.contain('MYSQL_PORT=3306');
expect(data.Config.Env).to.contain('MYSQL_HOST=mysql');
expect(data.Config.Env).to.contain('MYSQL_USERNAME=' + username);
expect(data.Config.Env).to.contain('MYSQL_PASSWORD=' + password);
expect(data.Config.Env).to.contain('MYSQL_DATABASE=' + dbname);
var cmd = util.format('mysql -h %s -u%s -p%s --database=%s -e "CREATE TABLE IF NOT EXISTS foo (id INT);"',
'mysql', username, password, dbname);
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); // "Warning: Using a password on the command line interface can be insecure."
clients.getByAppIdAndType(APP_ID, clients.TYPE_OAUTH, function (error, client) {
expect(error).to.not.be.ok();
expect(client.id.length).to.be(40); // cid- + 32 hex chars (128 bits) + 4 hyphens
expect(client.clientSecret.length).to.be(256); // 32 hex chars (8 * 256 bits)
expect(data.Config.Env).to.contain('OAUTH_CLIENT_ID=' + client.id);
expect(data.Config.Env).to.contain('OAUTH_CLIENT_SECRET=' + client.clientSecret);
done();
});
});
});
it('installation - postgresql addon config', function (done) {
var appContainer = docker.getContainer(appEntry.containerId);
appContainer.inspect(function (error, data) {
var postgresqlUrl = null;
data.Config.Env.forEach(function (env) { if (env.indexOf('POSTGRESQL_URL=') === 0) postgresqlUrl = env.split('=')[1]; });
expect(postgresqlUrl).to.be.ok();
var urlp = url.parse(postgresqlUrl);
var username = urlp.auth.split(':')[0];
var password = urlp.auth.split(':')[1];
var dbname = urlp.path.substr(1);
expect(data.Config.Env).to.contain('POSTGRESQL_PORT=5432');
expect(data.Config.Env).to.contain('POSTGRESQL_HOST=postgresql');
expect(data.Config.Env).to.contain('POSTGRESQL_USERNAME=' + username);
expect(data.Config.Env).to.contain('POSTGRESQL_PASSWORD=' + password);
expect(data.Config.Env).to.contain('POSTGRESQL_DATABASE=' + dbname);
var cmd = util.format('bash -c "PGPASSWORD=%s psql -q -h %s -U%s --dbname=%s -e \'CREATE TABLE IF NOT EXISTS foo (id INT);\'"',
password, 'postgresql', username, dbname);
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);
done();
});
it('installation - app can populate addons', function (done) {
superagent.get('http://localhost:' + appEntry.httpPort + '/populate_addons').end(function (err, res) {
expect(!err).to.be.ok();
expect(res.statusCode).to.equal(200);
for (var key in res.body) {
expect(res.body[key]).to.be('OK');
}
done();
});
});
it('installation - mongodb addon config', function (done) {
var appContainer = docker.getContainer(appEntry.containerId);
appContainer.inspect(function (error, data) {
var mongodbUrl = null;
data.Config.Env.forEach(function (env) { if (env.indexOf('MONGODB_URL=') === 0) mongodbUrl = env.split('=')[1]; });
expect(mongodbUrl).to.be.ok();
var urlp = url.parse(mongodbUrl);
var username = urlp.auth.split(':')[0];
var password = urlp.auth.split(':')[1];
var dbname = urlp.path.substr(1);
expect(data.Config.Env).to.contain('MONGODB_PORT=27017');
expect(data.Config.Env).to.contain('MONGODB_HOST=mongodb');
expect(data.Config.Env).to.contain('MONGODB_USERNAME=' + username);
expect(data.Config.Env).to.contain('MONGODB_PASSWORD=' + password);
expect(data.Config.Env).to.contain('MONGODB_DATABASE=' + dbname);
var cmd = util.format('mongo --quiet -u %s -p %s %s:%s/%s --eval "db.collection.insert({ item: 34 })"',
username, password, 'mongodb', 27017, dbname);
child_process.exec('docker exec ' + appContainer.id + ' ' + cmd, { timeout: 5000 }, function (error) {
expect(!error).to.be.ok();
done();
});
});
it('installation - app can check addons', function (done) {
this.timeout(120000);
console.log('This test can take a while as it waits for scheduler addon to tick 1');
checkAddons(appEntry, done);
});
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('installation - redis addon created', function (done) {
checkRedis('redis-' + APP_ID, done);
});
xit('logs - stdout and stderr', function (done) {
@@ -963,6 +943,12 @@ describe('Apps', function () {
checkStartState();
});
it('installation - app can check addons', function (done) {
this.timeout(120000);
console.log('This test can take a while as it waits for scheduler addon to tick 2');
checkAddons(appEntry, done);
});
it('can uninstall app', function (done) {
var count = 0;
function checkUninstallStatus() {
@@ -1046,8 +1032,6 @@ describe('Apps', function () {
APP_ID = uuid.v4();
async.series([
setup,
function (callback) {
config.set('fqdn', 'test.foobar.com');
callback();
@@ -1084,7 +1068,6 @@ describe('Apps', function () {
after(function (done) {
APP_ID = null;
async.series([
cleanup,
apiHockServer.close.bind(apiHockServer),
awsHockServer.close.bind(awsHockServer)
], done);
@@ -1093,7 +1076,8 @@ describe('Apps', function () {
var appResult = null, appEntry = null;
it('can install test app', function (done) {
var fake = nock(config.apiServerOrigin()).post('/api/v1/apps/test/purchase?token=APPSTORE_TOKEN').reply(201, {});
var fake1 = nock(config.apiServerOrigin()).get('/api/v1/apps/test').reply(200, { manifest: APP_MANIFEST });
var fake2 = nock(config.apiServerOrigin()).post('/api/v1/apps/test/purchase?token=APPSTORE_TOKEN').reply(201, {});
var count = 0;
function checkInstallStatus() {
@@ -1110,11 +1094,12 @@ describe('Apps', function () {
superagent.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: null })
.send({ appStoreId: APP_STORE_ID, password: PASSWORD, location: APP_LOCATION, portBindings: { ECHO_SERVER_PORT: 7171 }, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(fake.isDone()).to.be.ok();
expect(res.body.id).to.equal(APP_ID);
expect(fake1.isDone()).to.be.ok();
expect(fake2.isDone()).to.be.ok();
APP_ID = res.body.id;
checkInstallStatus();
});
});
@@ -1202,52 +1187,26 @@ describe('Apps', function () {
});
});
var redisIp, exportedRedisPort;
it('installation - redis addon created', function (done) {
docker.getContainer('redis-' + APP_ID).inspect(function (error, data) {
expect(error).to.not.be.ok();
expect(data).to.be.ok();
redisIp = safe.query(data, 'NetworkSettings.IPAddress');
expect(redisIp).to.be.ok();
exportedRedisPort = safe.query(data, 'NetworkSettings.Ports.6379/tcp[0].HostPort');
expect(exportedRedisPort).to.be.ok();
it('installation - app can populate addons', function (done) {
superagent.get('http://localhost:' + appEntry.httpPort + '/populate_addons').end(function (err, res) {
expect(!err).to.be.ok();
expect(res.statusCode).to.equal(200);
for (var key in res.body) {
expect(res.body[key]).to.be('OK');
}
done();
});
});
it('installation - redis addon config', function (done) {
docker.getContainer(appEntry.containerId).inspect(function (error, data) {
var redisUrl = null;
data.Config.Env.forEach(function (env) { if (env.indexOf('REDIS_URL=') === 0) redisUrl = env.split('=')[1]; });
expect(redisUrl).to.be.ok();
it('installation - app can check addons', function (done) {
this.timeout(120000);
console.log('This test can take a while as it waits for scheduler addon to tick 3');
checkAddons(appEntry, done);
});
var urlp = url.parse(redisUrl);
expect(urlp.hostname).to.be('redis-' + APP_ID);
var password = urlp.auth.split(':')[1];
expect(data.Config.Env).to.contain('REDIS_PORT=6379');
expect(data.Config.Env).to.contain('REDIS_HOST=redis-' + APP_ID);
expect(data.Config.Env).to.contain('REDIS_PASSWORD=' + password);
function checkRedis() {
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) {
expect(err).to.not.be.ok();
expect(reply.toString()).to.be('value');
client.end();
done();
});
}
setTimeout(checkRedis, 1000); // the bridge network takes time to come up?
});
it('installation - redis addon created', function (done) {
checkRedis('redis-' + APP_ID, done);
});
function checkConfigureStatus(count, done) {
@@ -1265,20 +1224,20 @@ describe('Apps', function () {
});
}
it('cannot reconfigure app with missing location', function (done) {
it('cannot reconfigure app with bad location', function (done) {
superagent.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: null })
.send({ password: PASSWORD, location: 1234, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('cannot reconfigure app with missing accessRestriction', function (done) {
it('cannot reconfigure app with bad accessRestriction', function (done) {
superagent.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({ password: PASSWORD, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
@@ -1288,7 +1247,7 @@ describe('Apps', function () {
it('cannot reconfigure app with only the cert, no key', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token })
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null, cert: validCert1 })
.send({ password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null, cert: validCert1 })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
@@ -1298,27 +1257,27 @@ describe('Apps', function () {
it('cannot reconfigure app with only the key, no cert', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token })
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null, key: validKey1 })
.send({ password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, key: validKey1 })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('cannot reconfigure app with cert not bein a string', function (done) {
it('cannot reconfigure app with cert not being a string', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token })
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null, cert: 1234, key: validKey1 })
.send({ password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null, cert: 1234, key: validKey1 })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('cannot reconfigure app with key not bein a string', function (done) {
it('cannot reconfigure app with key not being a string', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token })
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null, cert: validCert1, key: 1234 })
.send({ password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, cert: validCert1, key: 1234 })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
@@ -1328,7 +1287,7 @@ describe('Apps', function () {
it('non admin cannot reconfigure app', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token_1 })
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null })
.send({ password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
@@ -1338,7 +1297,7 @@ describe('Apps', function () {
it('can reconfigure app', function (done) {
superagent.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 })
.send({ password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 } })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
checkConfigureStatus(0, done);
@@ -1351,7 +1310,7 @@ describe('Apps', function () {
expect(!error).to.be.ok();
expect(app).to.be.an('object');
appEntry = app;
expect(appEntry.containerid).to.not.be(oldContainerId);
expect(appEntry.containerId).to.not.be(oldContainerId);
done();
});
});
@@ -1368,61 +1327,19 @@ describe('Apps', function () {
});
it('reconfiguration - redis addon recreated', function (done) {
docker.getContainer('redis-' + APP_ID).inspect(function (error, data) {
expect(error).to.not.be.ok();
expect(data).to.be.ok();
redisIp = safe.query(data, 'NetworkSettings.IPAddress');
expect(redisIp).to.be.ok();
exportedRedisPort = safe.query(data, 'NetworkSettings.Ports.6379/tcp[0].HostPort');
expect(exportedRedisPort).to.be.ok();
done();
});
checkRedis('redis-' + APP_ID, done);
});
it('redis addon works after reconfiguration', function (done) {
docker.getContainer(appEntry.containerId).inspect(function (error, data) {
var redisUrl = null;
data.Config.Env.forEach(function (env) { if (env.indexOf('REDIS_URL=') === 0) redisUrl = env.split('=')[1]; });
expect(redisUrl).to.be.ok();
var urlp = url.parse(redisUrl);
var password = urlp.auth.split(':')[1];
expect(urlp.hostname).to.be('redis-' + APP_ID);
expect(data.Config.Env).to.contain('REDIS_PORT=6379');
expect(data.Config.Env).to.contain('REDIS_HOST=redis-' + APP_ID);
expect(data.Config.Env).to.contain('REDIS_PASSWORD=' + 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) {
expect(err).to.not.be.ok();
expect(reply.toString()).to.be('value');
client.end();
done();
});
});
});
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('installation - app can check addons', function (done) {
this.timeout(120000);
console.log('This test can take a while as it waits for scheduler addon to tick 4');
checkAddons(appEntry, done);
});
it('can reconfigure app with custom certificate', function (done) {
superagent.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, cert: validCert1, key: validKey1 })
.send({ password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null, cert: validCert1, key: validKey1 })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
checkConfigureStatus(0, done);
+1 -1
View File
@@ -53,7 +53,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 */, null /* accessRestriction */, 0 /* memoryLimit */, null /* altDomain */, callback);
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, { }, callback);
},
function createSettings(callback) {
+71 -6
View File
@@ -8,7 +8,7 @@
var async = require('async'),
config = require('../../config.js'),
clientdb = require('../../clientdb.js'),
clients = require('../../clients.js'),
database = require('../../database.js'),
oauth2 = require('../oauth2.js'),
expect = require('expect.js'),
@@ -163,6 +163,26 @@ describe('OAuth Clients API', function () {
});
});
it('fails with invalid name', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ appId: '$"$%^45asdfasdfadf.adf.', redirectURI: 'http://foobar.com', scope: 'profile' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('succeeds with dash', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ appId: 'fo-1234-bar', redirectURI: 'http://foobar.com', scope: 'profile' })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
done();
});
});
it('succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
@@ -174,7 +194,7 @@ describe('OAuth Clients API', function () {
expect(result.body.redirectURI).to.be.a('string');
expect(result.body.clientSecret).to.be.a('string');
expect(result.body.scope).to.be.a('string');
expect(result.body.type).to.equal(clientdb.TYPE_EXTERNAL);
expect(result.body.type).to.equal(clients.TYPE_EXTERNAL);
done();
});
@@ -291,6 +311,14 @@ describe('OAuth Clients API', function () {
scope: 'profile'
};
var CLIENT_1 = {
id: '',
appId: 'someAppId-1',
redirectURI: 'http://some.callback1',
scope: 'profile',
type: clients.TYPE_OAUTH
};
before(function (done) {
async.series([
server.start.bind(null),
@@ -387,6 +415,44 @@ describe('OAuth Clients API', function () {
});
});
});
it('fails for cid-webadmin', function (done) {
superagent.del(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(405);
superagent.get(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
done();
});
});
});
it('fails for addon auth client', function (done) {
clients.add(CLIENT_1.appId, CLIENT_1.type, CLIENT_1.redirectURI, CLIENT_1.scope, function (error, result) {
expect(error).to.equal(null);
CLIENT_1.id = result.id;
superagent.del(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_1.id)
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(405);
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_1.id)
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
done();
});
});
});
});
});
});
});
@@ -489,8 +555,7 @@ describe('Clients', function () {
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.clients.length).to.eql(1);
expect(result.body.clients[0].tokenCount).to.eql(1);
expect(result.body.clients.length).to.eql(3);
done();
});
@@ -543,7 +608,7 @@ describe('Clients', function () {
expect(result.statusCode).to.equal(200);
expect(result.body.tokens.length).to.eql(1);
expect(result.body.tokens[0].identifier).to.eql('user-' + USER_0.id);
expect(result.body.tokens[0].identifier).to.eql(USER_0.id);
done();
});
@@ -596,7 +661,7 @@ describe('Clients', function () {
expect(result.statusCode).to.equal(200);
expect(result.body.tokens.length).to.eql(1);
expect(result.body.tokens[0].identifier).to.eql('user-' + USER_0.id);
expect(result.body.tokens[0].identifier).to.eql(USER_0.id);
superagent.del(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
.query({ access_token: token })
+178 -1
View File
@@ -1,6 +1,5 @@
'use strict';
/* jslint node:true */
/* global it:false */
/* global describe:false */
/* global before:false */
@@ -10,6 +9,7 @@ var async = require('async'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
locker = require('../../locker.js'),
nock = require('nock'),
os = require('os'),
superagent = require('superagent'),
@@ -26,6 +26,7 @@ function setup(done) {
nock.cleanAll();
config._reset();
config.set('version', '0.5.0');
config.set('fqdn', 'localhost');
server.start(done);
}
@@ -262,6 +263,182 @@ describe('Cloudron', function () {
});
describe('migrate', function () {
before(function (done) {
async.series([
setup,
function (callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
// stash token for further use
token = result.body.token;
callback();
});
},
function setupBackupConfig(callback) {
superagent.post(SERVER_URL + '/api/v1/settings/backup_config')
.send({ provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
callback();
});
}
], done);
});
after(function (done) {
locker.unlock(locker._operation); // migrate never unlocks
cleanup(done);
});
it('fails without token', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 'sfo'})
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails without password', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 'sfo'})
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('succeeds without size', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ region: 'sfo', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
done();
});
});
it('fails with wrong size type', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 4, region: 'sfo', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('succeeds without region', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
done();
});
});
it('fails with wrong region type', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 4, password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails when in wrong state', function (done) {
var scope2 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN')
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
var scope3 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/' + config.fqdn() + '/backupDone?token=APPSTORE_TOKEN', function (body) {
return body.boxVersion && body.restoreKey && !body.appId && !body.appVersion && body.appBackupIds.length === 0;
})
.reply(200, { id: 'someid' });
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(409, {});
injectShellMock();
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 'sfo', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
function checkAppstoreServerCalled() {
if (scope1.isDone() && scope2.isDone() && scope3.isDone()) {
restoreShellMock();
return done();
}
setTimeout(checkAppstoreServerCalled, 100);
}
checkAppstoreServerCalled();
});
});
it('succeeds', function (done) {
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=BACKUP_TOKEN')
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
injectShellMock();
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 'sfo', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
function checkAppstoreServerCalled() {
if (scope1.isDone() && scope2.isDone() && scope3.isDone()) {
restoreShellMock();
return done();
}
setTimeout(checkAppstoreServerCalled, 100);
}
checkAppstoreServerCalled();
});
});
});
describe('feedback', function () {
before(function (done) {
async.series([
+60 -4
View File
@@ -327,7 +327,7 @@ describe('Developer API', function () {
.send({ username: USERNAME, password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.expiresAt).to.be.a('number');
expect(new Date(result.body.expiresAt).toString()).to.not.be('Invalid Date');
expect(result.body.token).to.be.a('string');
done();
});
@@ -338,7 +338,7 @@ describe('Developer API', function () {
.send({ username: USERNAME.toUpperCase(), password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.expiresAt).to.be.a('number');
expect(new Date(result.body.expiresAt).toString()).to.not.be('Invalid Date');
expect(result.body.token).to.be.a('string');
done();
});
@@ -349,7 +349,7 @@ describe('Developer API', function () {
.send({ username: EMAIL, password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.expiresAt).to.be.a('number');
expect(new Date(result.body.expiresAt).toString()).to.not.be('Invalid Date');
expect(result.body.token).to.be.a('string');
done();
});
@@ -360,10 +360,66 @@ describe('Developer API', function () {
.send({ username: EMAIL.toUpperCase(), password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.expiresAt).to.be.a('number');
expect(new Date(result.body.expiresAt).toString()).to.not.be('Invalid Date');
expect(result.body.token).to.be.a('string');
done();
});
});
});
describe('sdk tokens are valid without password checks', function () {
var token_normal, token_sdk;
before(function (done) {
async.series([
setup,
settings.setDeveloperMode.bind(null, true),
function (callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
token_normal = result.body.token;
superagent.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: USERNAME, password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(new Date(result.body.expiresAt).toString()).to.not.be('Invalid Date');
expect(result.body.token).to.be.a('string');
token_sdk = result.body.token;
callback();
});
});
},
], done);
});
after(cleanup);
it('fails with non sdk token', function (done) {
superagent.post(SERVER_URL + '/api/v1/profile/password').query({ access_token: token_normal }).send({ newPassword: 'Some?$123' }).end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/profile/password').query({ access_token: token_sdk }).send({ newPassword: 'Some?$123' }).end(function (error, result) {
expect(result.statusCode).to.equal(204);
done();
});
});
});
});
+1 -1
View File
@@ -67,7 +67,7 @@ function setup(done) {
token_1 = tokendb.generateToken();
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_1, tokendb.PREFIX_USER + USER_1_ID, 'test-client-id', Date.now() + 100000, '*', callback);
tokendb.add(token_1, USER_1_ID, 'test-client-id', Date.now() + 100000, '*', callback);
}
], done);
+41 -9
View File
@@ -6,18 +6,15 @@
'use strict';
var appdb = require('../../appdb.js'),
async = require('async'),
var async = require('async'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
groups = require('../../groups.js'),
superagent = require('superagent'),
server = require('../../server.js'),
settings = require('../../settings.js'),
tokendb = require('../../tokendb.js'),
nock = require('nock'),
userdb = require('../../userdb.js');
nock = require('nock');
var SERVER_URL = 'http://localhost:' + config.get('port');
@@ -117,6 +114,9 @@ describe('Groups API', function () {
expect(res.body.groups).to.be.an(Array);
expect(res.body.groups.length).to.be(1);
expect(res.body.groups[0].name).to.eql('admin');
expect(res.body.groups[0].userIds).to.be.an(Array);
expect(res.body.groups[0].userIds.length).to.be(1);
expect(res.body.groups[0].userIds[0]).to.be(userId);
done();
});
});
@@ -224,7 +224,7 @@ describe('Groups API', function () {
});
it('cannot add user to invalid group', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + userId + '/set_groups')
superagent.put(SERVER_URL + '/api/v1/users/' + userId + '/groups')
.query({ access_token: token })
.send({ groupIds: [ 'admin', 'something' ]})
.end(function (error, result) {
@@ -234,7 +234,7 @@ describe('Groups API', function () {
});
it('can add user to valid group', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + userId + '/set_groups')
superagent.put(SERVER_URL + '/api/v1/users/' + userId + '/groups')
.query({ access_token: token })
.send({ groupIds: [ 'admin', 'group0', 'group1' ]})
.end(function (error, result) {
@@ -243,8 +243,8 @@ describe('Groups API', function () {
});
});
it('can remove last user from admin', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + userId + '/set_groups')
it('cannot remove self from admin', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + userId + '/groups')
.query({ access_token: token })
.send({ groupIds: [ 'group0', 'group1' ]})
.end(function (error, result) {
@@ -252,5 +252,37 @@ describe('Groups API', function () {
done();
});
});
it('can add another user to admin', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + userId_1 + '/groups')
.query({ access_token: token })
.send({ groupIds: [ 'admin' ]})
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
done();
});
});
it('lists members of admin group', function (done) {
superagent.get(SERVER_URL + '/api/v1/groups/admin')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.userIds.length).to.be(2);
expect(result.body.userIds[0]).to.be(userId);
expect(result.body.userIds[1]).to.be(userId_1);
done();
});
});
it('remove activation user from admin', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + userId + '/groups')
.query({ access_token: token_1 })
.send({ groupIds: [ 'group0', 'group1' ]})
.end(function (error, result) {
expect(result.statusCode).to.equal(204); // user_1 is still admin, so we can remove the other person
done();
});
});
});
});
+211
View File
@@ -0,0 +1,211 @@
/* jslint node:true */
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
'use strict';
var async = require('async'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
superagent = require('superagent'),
server = require('../../server.js'),
nock = require('nock'),
userdb = require('../../userdb.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
var MAILBOX_ID = 'mailbox';
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null;
var server;
function setup(done) {
config.set('fqdn', 'foobar.com');
async.series([
server.start.bind(server),
userdb._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, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
// stash token for further use
token = result.body.token;
callback();
});
}
], done);
}
function cleanup(done) {
database._clear(function (error) {
expect(!error).to.be.ok();
server.stop(done);
});
}
describe('Mailbox API', function () {
this.timeout(10000);
before(setup);
after(cleanup);
it('cannot create a mailbox without name param', function (done) {
superagent.post(SERVER_URL + '/api/v1/mailboxes')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('cannot create a mailbox without token', function (done) {
superagent.post(SERVER_URL + '/api/v1/mailboxes')
.send({ name: MAILBOX_ID })
.end(function (err, res) {
expect(res.statusCode).to.equal(401);
done();
});
});
it('cannot create invalid mailbox', function (done) {
superagent.post(SERVER_URL + '/api/v1/mailboxes')
.query({ access_token: token })
.send({ name: 'no-reply' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('can create mailbox', function (done) {
superagent.post(SERVER_URL + '/api/v1/mailboxes')
.query({ access_token: token })
.send({ name: MAILBOX_ID })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
});
it('can get mailbox', function (done) {
superagent.get(SERVER_URL + '/api/v1/mailboxes/' + MAILBOX_ID)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.name).to.equal(MAILBOX_ID);
expect(res.body.creationTime).to.be.ok();
done();
});
});
it('cannot set with invalid alias', function (done) {
superagent.put(SERVER_URL + '/api/v1/mailboxes/' + MAILBOX_ID + '/aliases')
.query({ access_token: token })
.send({ aliases: [ 'a' ]})
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('cannot set with invalid type', function (done) {
superagent.put(SERVER_URL + '/api/v1/mailboxes/' + MAILBOX_ID + '/aliases')
.query({ access_token: token })
.send({ aliases: [ 'apple', 34 ]})
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('can set aliases of mailbox', function (done) {
superagent.put(SERVER_URL + '/api/v1/mailboxes/' + MAILBOX_ID + '/aliases')
.query({ access_token: token })
.send({ aliases: [ 'alias1', 'alias2' ]})
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
done();
});
});
it('can list mailboxes', function (done) {
superagent.get(SERVER_URL + '/api/v1/mailboxes')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.mailboxes).to.be.an(Array);
expect(res.body.mailboxes[0].name).to.be(MAILBOX_ID);
done();
});
});
it('can get aliases', function (done) {
superagent.get(SERVER_URL + '/api/v1/mailboxes/' + MAILBOX_ID + '/aliases')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.aliases).to.be.an(Array);
expect(res.body.aliases[0]).to.be('alias1');
expect(res.body.aliases[1]).to.be('alias2');
done();
});
});
it('can add another mailbox', function (done) {
superagent.post(SERVER_URL + '/api/v1/mailboxes')
.query({ access_token: token })
.send({ name: MAILBOX_ID + '2' })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
});
it('cannot alias existing mailbox', function (done) {
superagent.put(SERVER_URL + '/api/v1/mailboxes/' + MAILBOX_ID + '/aliases')
.query({ access_token: token })
.send({ aliases: [ MAILBOX_ID + '2' ]})
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
done();
});
});
it('can delete mailbox', function (done) {
superagent.del(SERVER_URL + '/api/v1/mailboxes/' + MAILBOX_ID)
.query({ access_token: token })
.send({ name: MAILBOX_ID })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
});
it('cannot delete random mailbox', function (done) {
superagent.del(SERVER_URL + '/api/v1/mailboxes/' + MAILBOX_ID)
.query({ access_token: token })
.send({ name: MAILBOX_ID })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
});
});
});
+15 -14
View File
@@ -18,6 +18,7 @@ var expect = require('expect.js'),
querystring = require('querystring'),
database = require('../../database.js'),
clientdb = require('../../clientdb.js'),
clients = require('../../clients.js'),
userdb = require('../../userdb.js'),
user = require('../../user.js'),
appdb = require('../../appdb.js'),
@@ -197,7 +198,7 @@ describe('OAuth2', function () {
var CLIENT_0 = {
id: 'cid-client0',
appId: 'appid-app0',
type: clientdb.TYPE_OAUTH,
type: clients.TYPE_OAUTH,
clientSecret: 'secret0',
redirectURI: 'http://redirect0',
scope: 'profile'
@@ -207,7 +208,7 @@ describe('OAuth2', function () {
var CLIENT_1 = {
id: 'cid-client1',
appId: 'appid-app1',
type: clientdb.TYPE_OAUTH,
type: clients.TYPE_OAUTH,
clientSecret: 'secret1',
redirectURI: 'http://redirect1',
scope: 'profile'
@@ -217,7 +218,7 @@ describe('OAuth2', function () {
var CLIENT_2 = {
id: 'cid-client2',
appId: APP_0.id,
type: clientdb.TYPE_OAUTH,
type: clients.TYPE_OAUTH,
clientSecret: 'secret2',
redirectURI: 'http://redirect2',
scope: 'profile'
@@ -227,7 +228,7 @@ describe('OAuth2', function () {
var CLIENT_3 = {
id: 'cid-client3',
appId: APP_0.id,
type: clientdb.TYPE_OAUTH,
type: clients.TYPE_OAUTH,
clientSecret: 'secret3',
redirectURI: 'http://redirect1',
scope: 'profile'
@@ -237,7 +238,7 @@ describe('OAuth2', function () {
var CLIENT_4 = {
id: 'cid-client4',
appId: 'appid-app4',
type: clientdb.TYPE_PROXY,
type: clients.TYPE_PROXY,
clientSecret: 'secret4',
redirectURI: 'http://redirect4',
scope: 'profile'
@@ -247,7 +248,7 @@ describe('OAuth2', function () {
var CLIENT_5 = {
id: 'cid-client5',
appId: APP_0.id,
type: clientdb.TYPE_PROXY,
type: clients.TYPE_PROXY,
clientSecret: 'secret5',
redirectURI: 'http://redirect5',
scope: 'profile'
@@ -257,7 +258,7 @@ describe('OAuth2', function () {
var CLIENT_6 = {
id: 'cid-client6',
appId: APP_1.id,
type: clientdb.TYPE_OAUTH,
type: clients.TYPE_OAUTH,
clientSecret: 'secret6',
redirectURI: 'http://redirect6',
scope: 'profile'
@@ -267,7 +268,7 @@ describe('OAuth2', function () {
var CLIENT_7 = {
id: 'cid-client7',
appId: APP_2.id,
type: clientdb.TYPE_OAUTH,
type: clients.TYPE_OAUTH,
clientSecret: 'secret7',
redirectURI: 'http://redirect7',
scope: 'profile'
@@ -277,7 +278,7 @@ describe('OAuth2', function () {
var CLIENT_8 = {
id: 'cid-client8',
appId: APP_2.id,
type: clientdb.TYPE_SIMPLE_AUTH,
type: clients.TYPE_SIMPLE_AUTH,
clientSecret: 'secret8',
redirectURI: 'http://redirect8',
scope: 'profile'
@@ -287,7 +288,7 @@ describe('OAuth2', function () {
var CLIENT_9 = {
id: 'cid-client9',
appId: APP_3.id,
type: clientdb.TYPE_OAUTH,
type: clients.TYPE_OAUTH,
clientSecret: 'secret9',
redirectURI: 'http://redirect9',
scope: 'profile'
@@ -313,10 +314,10 @@ describe('OAuth2', function () {
clientdb.add.bind(null, CLIENT_7.id, CLIENT_7.appId, CLIENT_7.type, CLIENT_7.clientSecret, CLIENT_7.redirectURI, CLIENT_7.scope),
clientdb.add.bind(null, CLIENT_8.id, CLIENT_8.appId, CLIENT_8.type, CLIENT_8.clientSecret, CLIENT_8.redirectURI, CLIENT_8.scope),
clientdb.add.bind(null, CLIENT_9.id, CLIENT_9.appId, CLIENT_9.type, CLIENT_9.clientSecret, CLIENT_9.redirectURI, CLIENT_9.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.memoryLimit, APP_0.altDomain),
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.portBindings, APP_1.accessRestriction, APP_1.memoryLimit, APP_1.altDomain),
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.portBindings, APP_2.accessRestriction, APP_2.memoryLimit, APP_2.altDomain),
appdb.add.bind(null, APP_3.id, APP_3.appStoreId, APP_3.manifest, APP_3.location, APP_3.portBindings, APP_3.accessRestriction, APP_3.memoryLimit, APP_3.altDomain),
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0),
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.portBindings, APP_1),
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.portBindings, APP_2),
appdb.add.bind(null, APP_3.id, APP_3.appStoreId, APP_3.manifest, APP_3.location, APP_3.portBindings, APP_3),
function (callback) {
user.create(USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, null /* source */, function (error, userObject) {
expect(error).to.not.be.ok();
+14 -14
View File
@@ -115,7 +115,7 @@ describe('Profile API', function () {
var token = tokendb.generateToken();
var expires = Date.now() - 2000; // 1 sec
tokendb.add(token, tokendb.PREFIX_USER + user_0.id, null, expires, '*', function (error) {
tokendb.add(token, user_0.id, null, expires, '*', function (error) {
expect(error).to.not.be.ok();
superagent.get(SERVER_URL + '/api/v1/profile').query({ access_token: token }).end(function (error, result) {
@@ -153,7 +153,7 @@ describe('Profile API', function () {
after(cleanup);
it('change email fails due to missing token', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile')
superagent.post(SERVER_URL + '/api/v1/profile')
.send({ email: EMAIL_0_NEW })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
@@ -162,7 +162,7 @@ describe('Profile API', function () {
});
it('change email fails due to invalid email', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile')
superagent.post(SERVER_URL + '/api/v1/profile')
.query({ access_token: token_0 })
.send({ email: 'foo@bar' })
.end(function (error, result) {
@@ -172,7 +172,7 @@ describe('Profile API', function () {
});
it('change user succeeds without email nor displayName', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile')
superagent.post(SERVER_URL + '/api/v1/profile')
.query({ access_token: token_0 })
.send({})
.end(function (error, result) {
@@ -182,7 +182,7 @@ describe('Profile API', function () {
});
it('change email succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile')
superagent.post(SERVER_URL + '/api/v1/profile')
.query({ access_token: token_0 })
.send({ email: EMAIL_0_NEW })
.end(function (error, result) {
@@ -203,7 +203,7 @@ describe('Profile API', function () {
});
it('change displayName succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile')
superagent.post(SERVER_URL + '/api/v1/profile')
.query({ access_token: token_0 })
.send({ displayName: DISPLAY_NAME_0_NEW })
.end(function (error, result) {
@@ -229,7 +229,7 @@ describe('Profile API', function () {
after(cleanup);
it('fails due to missing current password', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile/password')
superagent.post(SERVER_URL + '/api/v1/profile/password')
.query({ access_token: token_0 })
.send({ newPassword: 'some wrong password' })
.end(function (err, res) {
@@ -239,7 +239,7 @@ describe('Profile API', function () {
});
it('fails due to missing new password', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile/password')
superagent.post(SERVER_URL + '/api/v1/profile/password')
.query({ access_token: token_0 })
.send({ password: PASSWORD })
.end(function (err, res) {
@@ -249,7 +249,7 @@ describe('Profile API', function () {
});
it('fails due to wrong password', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile/password')
superagent.post(SERVER_URL + '/api/v1/profile/password')
.query({ access_token: token_0 })
.send({ password: 'some wrong password', newPassword: 'MOre#$%34' })
.end(function (err, res) {
@@ -259,7 +259,7 @@ describe('Profile API', function () {
});
it('fails due to invalid password', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile/password')
superagent.post(SERVER_URL + '/api/v1/profile/password')
.query({ access_token: token_0 })
.send({ password: PASSWORD, newPassword: 'five' })
.end(function (err, res) {
@@ -269,7 +269,7 @@ describe('Profile API', function () {
});
it('succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile/password')
superagent.post(SERVER_URL + '/api/v1/profile/password')
.query({ access_token: token_0 })
.send({ password: PASSWORD, newPassword: 'MOre#$%34' })
.end(function (err, res) {
@@ -284,7 +284,7 @@ describe('Profile API', function () {
after(cleanup);
it('fails due to missing showTutorial', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile/tutorial')
superagent.post(SERVER_URL + '/api/v1/profile/tutorial')
.query({ access_token: token_0 })
.send({})
.end(function (err, res) {
@@ -294,7 +294,7 @@ describe('Profile API', function () {
});
it('fails due to wrong showTutorial type', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile/tutorial')
superagent.post(SERVER_URL + '/api/v1/profile/tutorial')
.query({ access_token: token_0 })
.send({ showTutorial: 'true' })
.end(function (err, res) {
@@ -304,7 +304,7 @@ describe('Profile API', function () {
});
it('succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile/tutorial')
superagent.post(SERVER_URL + '/api/v1/profile/tutorial')
.query({ access_token: token_0 })
.send({ showTutorial: false })
.end(function (err, res) {
+88
View File
@@ -0,0 +1,88 @@
/* jslint node:true */
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
'use strict';
var async = require('async'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
net = require('net'),
nock = require('nock'),
superagent = require('superagent'),
server = require('../../server.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null;
var server;
function setup(done) {
config.setVersion('1.2.3');
async.series([
server.start.bind(server),
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, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
// stash token for further use
token = result.body.token;
callback();
});
}
], done);
}
function cleanup(done) {
database._clear(function (error) {
expect(!error).to.be.ok();
server.stop(done);
});
}
describe('REST API', function () {
before(setup);
after(cleanup);
it('does not crash with invalid JSON', function (done) {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.set('content-type', 'application/json')
.send("some invalid non-strict json")
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
expect(result.body.message).to.be('Bad JSON');
done();
});
});
it('does not crash with invalid string', function (done) {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.set('content-type', 'application/x-www-form-urlencoded')
.send("some string")
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
});
+2 -2
View File
@@ -56,7 +56,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 */, null /* accessRestriction */, 0 /* memoryLimit */, null /* altDomain */, callback);
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, { }, callback);
}
], done);
}
@@ -175,7 +175,7 @@ describe('Settings API', function () {
.query({ access_token: token })
.send({ name: name })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.statusCode).to.equal(202);
done();
});
});
+13 -12
View File
@@ -6,9 +6,10 @@
'use strict';
var clientdb = require('../../clientdb.js'),
appdb = require('../../appdb.js'),
var appdb = require('../../appdb.js'),
async = require('async'),
clientdb = require('../../clientdb.js'),
clients = require('../../clients.js'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
@@ -70,7 +71,7 @@ describe('SimpleAuth API', function () {
var CLIENT_0 = {
id: 'someclientid',
appId: 'someappid',
type: clientdb.TYPE_SIMPLE_AUTH,
type: clients.TYPE_SIMPLE_AUTH,
clientSecret: 'someclientsecret',
redirectURI: '',
scope: 'user,profile'
@@ -79,7 +80,7 @@ describe('SimpleAuth API', function () {
var CLIENT_1 = {
id: 'someclientid1',
appId: APP_0.id,
type: clientdb.TYPE_SIMPLE_AUTH,
type: clients.TYPE_SIMPLE_AUTH,
clientSecret: 'someclientsecret1',
redirectURI: '',
scope: 'user,profile'
@@ -88,7 +89,7 @@ describe('SimpleAuth API', function () {
var CLIENT_2 = {
id: 'someclientid2',
appId: APP_1.id,
type: clientdb.TYPE_SIMPLE_AUTH,
type: clients.TYPE_SIMPLE_AUTH,
clientSecret: 'someclientsecret2',
redirectURI: '',
scope: 'user,profile'
@@ -97,7 +98,7 @@ describe('SimpleAuth API', function () {
var CLIENT_3 = {
id: 'someclientid3',
appId: APP_2.id,
type: clientdb.TYPE_SIMPLE_AUTH,
type: clients.TYPE_SIMPLE_AUTH,
clientSecret: 'someclientsecret3',
redirectURI: '',
scope: 'user,profile'
@@ -106,7 +107,7 @@ describe('SimpleAuth API', function () {
var CLIENT_4 = {
id: 'someclientid4',
appId: APP_2.id,
type: clientdb.TYPE_OAUTH,
type: clients.TYPE_OAUTH,
clientSecret: 'someclientsecret4',
redirectURI: '',
scope: 'user,profile'
@@ -115,7 +116,7 @@ describe('SimpleAuth API', function () {
var CLIENT_5 = {
id: 'someclientid5',
appId: APP_3.id,
type: clientdb.TYPE_SIMPLE_AUTH,
type: clients.TYPE_SIMPLE_AUTH,
clientSecret: 'someclientsecret5',
redirectURI: '',
scope: 'user,profile'
@@ -159,10 +160,10 @@ describe('SimpleAuth API', function () {
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),
clientdb.add.bind(null, CLIENT_5.id, CLIENT_5.appId, CLIENT_5.type, CLIENT_5.clientSecret, CLIENT_5.redirectURI, CLIENT_5.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.memoryLimit, APP_0.altDomain),
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.portBindings, APP_1.accessRestriction, APP_1.memoryLimit, APP_1.altDomain),
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.portBindings, APP_2.accessRestriction, APP_2.memoryLimit, APP_2.altDomain),
appdb.add.bind(null, APP_3.id, APP_3.appStoreId, APP_3.manifest, APP_3.location, APP_3.portBindings, APP_3.accessRestriction, APP_3.memoryLimit, APP_3.altDomain)
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0),
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.portBindings, APP_1),
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.portBindings, APP_2),
appdb.add.bind(null, APP_3.id, APP_3.appStoreId, APP_3.manifest, APP_3.location, APP_3.portBindings, APP_3)
], done);
});
-72
View File
@@ -1,72 +0,0 @@
#!/bin/bash
set -eu -o pipefail
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
source ${SOURCE_DIR}/src/INFRA_VERSION
readonly mysqldatadir="/tmp/mysqldata-$(date +%s)"
readonly postgresqldatadir="/tmp/postgresqldata-$(date +%s)"
readonly mongodbdatadir="/tmp/mongodbdata-$(date +%s)"
root_password=secret
start_postgresql() {
postgresql_vars="POSTGRESQL_ROOT_PASSWORD=${root_password}; POSTGRESQL_ROOT_HOST=172.17.0.0/255.255.0.0"
rm -rf /tmp/postgresql_vars.sh
echo "${postgresql_vars}" > /tmp/postgresql_vars.sh
docker rm -f postgresql 2>/dev/null 1>&2 || true
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"
rm -rf /tmp/mysql_vars.sh
echo "${mysql_vars}" > /tmp/mysql_vars.sh
docker rm -f mysql 2>/dev/null 1>&2 || true
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}"
rm -rf /tmp/mongodb_vars.sh
echo "${mongodb_vars}" > /tmp/mongodb_vars.sh
docker rm -f mongodb 2>/dev/null 1>&2 || true
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() {
docker rm -f mail 2>/dev/null 1>&2 || true
docker run -dP --name=mail -e MAIL_SERVER_NAME="server.local" -e MAIL_DOMAIN="server.local" \
--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 "."
sleep 1
done
echo ""
+1 -1
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 */, null /* accessRestriction */, 0 /* memoryLimit */, null /* altDomain */, callback);
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, { }, callback);
},
function createSettings(callback) {
+60 -13
View File
@@ -1,4 +1,3 @@
/* jslint node:true */
/* global it:false */
/* global describe:false */
/* global before:false */
@@ -21,7 +20,7 @@ var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME_0 = 'superaDmIn', PASSWORD = 'Foobar?1337', EMAIL_0 = 'silLY@me.com', EMAIL_0_NEW = 'stupID@me.com', DISPLAY_NAME_0_NEW = 'New Name';
var USERNAME_1 = 'userTheFirst', EMAIL_1 = 'taO@zen.mac';
var USERNAME_2 = 'userTheSecond', EMAIL_2 = 'USER@foo.bar', EMAIL_2_NEW = 'happy@ME.com';
var USERNAME_3 = 'userTheThird', EMAIL_3 = 'user3@FOO.bar';
var USERNAME_3 = 'ut', EMAIL_3 = 'user3@FOO.bar';
function setup(done) {
server.start(function (error) {
@@ -161,7 +160,7 @@ describe('User API', function () {
var token = tokendb.generateToken();
var expires = Date.now() + 2000; // 1 sec
tokendb.add(token, tokendb.PREFIX_USER + user_0.id, null, expires, '*', function (error) {
tokendb.add(token, user_0.id, null, expires, '*', function (error) {
expect(error).to.not.be.ok();
setTimeout(function () {
@@ -261,7 +260,7 @@ describe('User API', function () {
checkMails(2, function () {
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_1, tokendb.PREFIX_USER + user_1.id, 'test-client-id', Date.now() + 10000, '*', done);
tokendb.add(token_1, user_1.id, 'test-client-id', Date.now() + 10000, '*', done);
});
});
});
@@ -293,7 +292,7 @@ describe('User API', function () {
});
it('set second user as admin succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + user_1.id + '/set_groups')
superagent.put(SERVER_URL + '/api/v1/users/' + user_1.id + '/groups')
.query({ access_token: token })
.send({ groupIds: [ groups.ADMIN_GROUP_ID ] })
.end(function (err, res) {
@@ -310,8 +309,24 @@ describe('User API', function () {
});
});
it('list groupIds when listing users', function (done) {
superagent.get(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.end(function (error, res) {
expect(error).to.be(null);
expect(res.statusCode).to.equal(200);
expect(res.body.users).to.be.an('array');
res.body.users.forEach(function (user) {
expect(user.admin).to.be(true);
expect(user.groupIds).to.eql([ groups.ADMIN_GROUP_ID ]);
});
done();
});
});
it('remove itself from admins fails', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + user_0.id + '/set_groups')
superagent.put(SERVER_URL + '/api/v1/users/' + user_0.id + '/groups')
.query({ access_token: token })
.send({ groupIds: [ 'somegroupid' ] })
.end(function (err, res) {
@@ -321,7 +336,7 @@ describe('User API', function () {
});
it('remove second user from admins succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + user_1.id + '/set_groups')
superagent.put(SERVER_URL + '/api/v1/users/' + user_1.id + '/groups')
.query({ access_token: token })
.send({ groupIds: [ 'somegroupid' ] })
.end(function (err, res) {
@@ -368,6 +383,26 @@ describe('User API', function () {
});
});
it('create user reserved name fails', function (done) {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: 'no-reply' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('create user with short name fails', function (done) {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: 'n' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('create second and third user', function (done) {
mailer._clearMailQueue();
@@ -441,12 +476,24 @@ describe('User API', function () {
expect(user.email).to.be.ok();
expect(user.password).to.not.be.ok();
expect(user.salt).to.not.be.ok();
expect(user.groupIds).to.be.an(Array);
expect(user.admin).to.be.a('boolean');
});
done();
});
});
it('remove random user fails', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/randomid')
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
});
});
it('user removes himself is not allowed', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/' + user_0.id)
.query({ access_token: token })
@@ -508,7 +555,7 @@ describe('User API', function () {
// Change email
it('change email fails due to missing token', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + user_0.id)
superagent.post(SERVER_URL + '/api/v1/users/' + user_0.id)
.send({ email: EMAIL_0_NEW })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
@@ -517,7 +564,7 @@ describe('User API', function () {
});
it('change email fails due to invalid email', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + user_0.id)
superagent.post(SERVER_URL + '/api/v1/users/' + user_0.id)
.query({ access_token: token })
.send({ email: 'foo@bar' })
.end(function (error, result) {
@@ -527,7 +574,7 @@ describe('User API', function () {
});
it('change user succeeds without email nor displayName', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + user_0.id)
superagent.post(SERVER_URL + '/api/v1/users/' + user_0.id)
.query({ access_token: token })
.send({})
.end(function (error, result) {
@@ -537,7 +584,7 @@ describe('User API', function () {
});
it('change email succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + user_2.id)
superagent.post(SERVER_URL + '/api/v1/users/' + user_2.id)
.query({ access_token: token })
.send({ email: EMAIL_2_NEW })
.end(function (error, result) {
@@ -558,7 +605,7 @@ describe('User API', function () {
});
it('change email as admin for other user succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + user_2.id)
superagent.post(SERVER_URL + '/api/v1/users/' + user_2.id)
.query({ access_token: token })
.send({ email: EMAIL_2 })
.end(function (error, result) {
@@ -579,7 +626,7 @@ describe('User API', function () {
});
it('change displayName succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + user_0.id)
superagent.post(SERVER_URL + '/api/v1/users/' + user_0.id)
.query({ access_token: token })
.send({ displayName: DISPLAY_NAME_0_NEW })
.end(function (error, result) {
+32 -39
View File
@@ -13,13 +13,16 @@ exports = module.exports = {
};
var assert = require('assert'),
clients = require('../clients.js'),
generatePassword = require('../password.js').generate,
groups = require('../groups.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
oauth2 = require('./oauth2.js'),
user = require('../user.js'),
tokendb = require('../tokendb.js'),
UserError = user.UserError;
UserError = user.UserError,
_ = require('underscore');
function auditSource(req) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
@@ -41,11 +44,8 @@ function create(req, res, next) {
var displayName = req.body.displayName || '';
user.create(username, password, email, displayName, auditSource(req), { invitor: req.user, sendInvite: sendInvite }, function (error, user) {
if (error && error.reason === UserError.BAD_USERNAME) return next(new HttpError(400, 'Invalid username'));
if (error && error.reason === UserError.BAD_EMAIL) return next(new HttpError(400, 'Invalid email'));
if (error && error.reason === UserError.BAD_PASSWORD) return next(new HttpError(400, 'Invalid password'));
if (error && error.reason === UserError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === UserError.ALREADY_EXISTS) return next(new HttpError(409, 'Already exists'));
if (error && error.reason === UserError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
var userInfo = {
@@ -54,6 +54,7 @@ function create(req, res, next) {
displayName: user.displayName,
email: user.email,
admin: user.admin,
groupIds: [ ],
resetToken: user.resetToken
};
@@ -68,30 +69,29 @@ function update(req, res, next) {
if ('email' in req.body && typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string'));
if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be string'));
if ('username' in req.body && typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be a string'));
if (req.user.tokenType !== tokendb.TYPE_USER) return next(new HttpError(403, 'Token type not allowed'));
if (req.user.id !== req.params.userId && !req.user.admin) return next(new HttpError(403, 'Not allowed'));
user.get(req.params.userId, function (error, result) {
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'No such user'));
user.update(req.params.userId, req.body, auditSource(req), function (error) {
if (error && error.reason === UserError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === UserError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
if (error) return next(new HttpError(500, error));
user.update(req.params.userId, result.username, req.body.email || result.email, req.body.displayName || result.displayName, auditSource(req), function (error) {
if (error && error.reason === UserError.BAD_USERNAME) return next(new HttpError(400, error.message));
if (error && error.reason === UserError.BAD_EMAIL) return next(new HttpError(400, error.message));
if (error && error.reason === UserError.ALREADY_EXISTS) return next(new HttpError(409, 'Already exists'));
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204));
});
next(new HttpSuccess(204));
});
}
function list(req, res, next) {
user.list(function (error, result) {
user.list(function (error, results) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { users: result }));
var users = results.map(function (result) {
return _.pick(result, 'id', 'username', 'email', 'displayName', 'groupIds', 'admin');
});
next(new HttpSuccess(200, { users: users }));
});
}
@@ -105,17 +105,14 @@ function get(req, res, next) {
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'No such user'));
if (error) return next(new HttpError(500, error));
groups.isMember(groups.ADMIN_GROUP_ID, req.params.userId, function (error, isAdmin) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, {
id: result.id,
username: result.username,
email: result.email,
admin: isAdmin,
displayName: result.displayName
}));
});
next(new HttpSuccess(200, {
id: result.id,
username: result.username,
displayName: result.displayName,
email: result.email,
admin: result.admin,
groupIds: result.groupIds
}));
});
}
@@ -129,24 +126,20 @@ function remove(req, res, next) {
if (req.user.id === req.params.userId) return next(new HttpError(403, 'Not allowed to remove yourself.'));
user.get(req.params.userId, function (error, userObject) {
user.remove(req.params.userId, auditSource(req), function (error) {
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'No such user'));
if (error) return next(new HttpError(500, error));
user.remove(userObject, auditSource(req), function (error) {
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'No such user'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204));
});
next(new HttpSuccess(204));
});
}
function verifyPassword(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
// developers are allowed through without password
if (req.user.tokenType === tokendb.TYPE_DEV) return next();
// using an 'sdk' token we skip password checks
var error = oauth2.validateRequestedScopes(req, [ clients.SCOPE_ROLE_SDK ]);
if (!error) return next();
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'API call requires user password'));
+8 -5
View File
@@ -12,8 +12,8 @@ if [[ $# == 1 && "$1" == "--check" ]]; then
exit 0
fi
if [ $# -lt 8 ]; then
echo "Usage: backupapp.sh <appid> <s3 config url> <s3 data url> <access key id> <access key> <session token> <region> <password>"
if [ $# -lt 7 ]; then
echo "Usage: backupapp.sh <appid> <s3 config url> <s3 data url> <access key id> <access key> <region> <password> [session token]"
exit 1
fi
@@ -25,9 +25,12 @@ readonly s3_config_url="$2"
readonly s3_data_url="$3"
export AWS_ACCESS_KEY_ID="$4"
export AWS_SECRET_ACCESS_KEY="$5"
export AWS_SESSION_TOKEN="$6"
export AWS_DEFAULT_REGION="$7"
readonly password="$8"
export AWS_DEFAULT_REGION="$6"
readonly password="$7"
if [ $# -gt 7 ]; then
export AWS_SESSION_TOKEN="$8"
fi
readonly now=$(date "+%Y-%m-%dT%H:%M:%S")
readonly app_data_dir="${DATA_DIR}/${app_id}"
+9 -5
View File
@@ -12,8 +12,8 @@ if [[ $# == 1 && "$1" == "--check" ]]; then
exit 0
fi
if [ $# -lt 6 ]; then
echo "Usage: backupbox.sh <s3 url> <access key id> <access key> <session token> <region> <password>"
if [ $# -lt 5 ]; then
echo "Usage: backupbox.sh <s3 url> <access key id> <access key> <region> <password> [session token]"
exit 1
fi
@@ -21,9 +21,13 @@ fi
s3_url="$1"
export AWS_ACCESS_KEY_ID="$2"
export AWS_SECRET_ACCESS_KEY="$3"
export AWS_SESSION_TOKEN="$4"
export AWS_DEFAULT_REGION="$5"
password="$6"
export AWS_DEFAULT_REGION="$4"
password="$5"
if [ $# -gt 5 ]; then
export AWS_SESSION_TOKEN="$6"
fi
now=$(date "+%Y-%m-%dT%H:%M:%S")
BOX_DATA_DIR="${HOME}/data/box"
box_snapshot_dir="${HOME}/data/snapshots/box-${now}"
+1 -3
View File
@@ -1,7 +1,5 @@
#!/bin/bash
# This script is called once at the end of a cloudrons lifetime
set -eu -o pipefail
if [[ ${EUID} -ne 0 ]]; then
@@ -22,7 +20,7 @@ if [[ "${BOX_ENV}" != "cloudron" ]]; then
exit 0
fi
"${BOX_SRC_DIR}/setup/splashpage.sh" --retire --data "$1" # show splash
"${BOX_SRC_DIR}/setup/splashpage.sh" --retire-reason "$1" --retire-info "$2" --data "$3" # show splash
echo "Stopping apps"
systemctl stop docker # stop the apps
-148
View File
@@ -1,148 +0,0 @@
#!/bin/bash
set -eu -o pipefail
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root." > /dev/stderr
exit 1
fi
if [[ $# == 1 && "$1" == "--check" ]]; then
echo "OK"
exit 0
fi
readonly DATA_DIR="/home/yellowtent/data"
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${script_dir}/../INFRA_VERSION" # this injects INFRA_VERSION
readonly fqdn="$1"
readonly mail_fqdn="$2"
readonly mail_tls_cert="$3"
readonly mail_tls_key="$4"
# removing containers ensures containers are launched with latest config updates
# restore code in appatask does not delete old containers
infra_version="none"
[[ -f "${DATA_DIR}/INFRA_VERSION" ]] && infra_version=$(cat "${DATA_DIR}/INFRA_VERSION")
if [[ "${infra_version}" == "${INFRA_VERSION}" ]]; then
echo "Infrastructure is upto date"
exit 0
fi
echo "Upgrading infrastructure from ${infra_version} to ${INFRA_VERSION}"
# TODO: be nice and stop addons cleanly (example, shutdown commands)
existing_containers=$(docker ps -qa)
echo "Remove containers: ${existing_containers}"
if [[ -n "${existing_containers}" ]]; then
echo "${existing_containers}" | xargs docker rm -f
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_SMTP_PORT is 2500 in addons.js. used in mailer.js as well)
# MAIL_SERVER_NAME is the hostname of the mailserver i.e server uses these certs
# MAIL_DOMAIN is the domain for which this server is relaying mails
mail_container_id=$(docker run --restart=always -d --name="mail" \
-m 75m \
--memory-swap 150m \
-h "${fqdn}" \
-e "MAIL_DOMAIN=${fqdn}" \
-e "MAIL_SERVER_NAME=${mail_fqdn}" \
-v "${DATA_DIR}/box/mail:/app/data" \
-v "${mail_tls_key}:/etc/tls_key.pem:ro" \
-v "${mail_tls_cert}:/etc/tls_cert.pem:ro" \
--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)
docker0_ip=$(/sbin/ifconfig docker0 | grep "inet addr" | awk -F: '{print $2}' | awk '{print $1}')
cat > "${DATA_DIR}/addons/mysql_vars.sh" <<EOF
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 256m \
--memory-swap 512m \
-h "${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)
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 "${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)
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 "${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, upgraded and restored cloudons), download app backups
echo "Marking installed apps for restore"
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", oldConfigJson = NULL WHERE installationState = "installed"' box
fi
echo -n "${INFRA_VERSION}" > "${DATA_DIR}/INFRA_VERSION"

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