Compare commits

..

641 Commits

Author SHA1 Message Date
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
Girish Ramakrishnan 8b2e4ce700 do another infra bump for existing cloudrons 2016-05-10 16:53:13 -07:00
Girish Ramakrishnan 776f184dbc wait randomly instead 2016-05-10 16:51:20 -07:00
Girish Ramakrishnan a54466f8c2 Fix apptask error because of multiple collectd restarts
Everyone gets in a rush to restart collectd and apptask update/restore
fails during infra updates
2016-05-10 09:52:55 -07:00
Girish Ramakrishnan f36641b443 better error message 2016-05-10 09:50:57 -07:00
Girish Ramakrishnan 36eb107b83 0.13.4 changes 2016-05-10 09:16:16 -07:00
Girish Ramakrishnan fa16ae9a0c Use mail container 0.12.0 that contains the restart fix 2016-05-10 09:05:06 -07:00
Girish Ramakrishnan 517b36b3f0 fix typo 2016-05-09 23:58:35 -07:00
Girish Ramakrishnan 83a28afc8f addons are started by box code now 2016-05-09 23:56:02 -07:00
Girish Ramakrishnan e76c7de259 bump test app version 2016-05-09 23:40:59 -07:00
Girish Ramakrishnan fcda4a771c play around with some text 2016-05-09 18:47:11 -07:00
Girish Ramakrishnan 76d8f16e22 0.13.3 changes 2016-05-08 01:17:03 -07:00
Girish Ramakrishnan 1b3cd1f373 remove tls since server does not offer it anymore 2016-05-08 01:15:24 -07:00
Girish Ramakrishnan 62b020e96d add note 2016-05-07 02:34:52 -07:00
Girish Ramakrishnan bc78f4a6d8 fix user to match adminEmail 2016-05-07 01:32:14 -07:00
Girish Ramakrishnan a8e458e935 Load certs into etc 2016-05-06 21:46:22 -07:00
Johannes Zellner e4747ef50c Rework the tutorial 2016-05-06 21:32:34 +02:00
Johannes Zellner 0d6637de27 Avoid circular dependencies with apps and certificates 2016-05-06 18:44:37 +02:00
Johannes Zellner 28e513a434 Fix eventlog tests 2016-05-06 18:05:28 +02:00
Girish Ramakrishnan e73174685b add note on settings route 2016-05-06 08:42:27 -07:00
Johannes Zellner 3af95508f5 eventlog getAllPaged is now getByQueryPaged 2016-05-06 17:27:52 +02:00
Johannes Zellner 4fa8ab596b Show busy state in eventlogs 2016-05-06 17:23:39 +02:00
Johannes Zellner 54c9bb7409 Add filter bar for event log view 2016-05-06 17:18:47 +02:00
Johannes Zellner 4c7dc5056d Add more query options for eventlog api 2016-05-06 16:49:17 +02:00
Johannes Zellner e986a67d39 Fixup all the unit tests 2016-05-06 15:16:22 +02:00
Johannes Zellner da8de173a6 Remove appdb.getBySubdomain() 2016-05-06 14:52:33 +02:00
Johannes Zellner cbc906f8d1 Remove apps.getBySubdomain() 2016-05-06 14:52:06 +02:00
Johannes Zellner c7958f8e1d Remove unused /api/v1/subdomains/:subdomain 2016-05-06 14:51:02 +02:00
Johannes Zellner b88ee8143a Rename welcome -> tutorial 2016-05-06 14:41:20 +02:00
Johannes Zellner e413f7ba9b Handle tutorial walkthrough 2016-05-06 14:38:17 +02:00
Johannes Zellner 7e1055ae44 Show tutorial for admins 2016-05-06 14:23:21 +02:00
Johannes Zellner c61ce40362 Add /api/v1/profile/tutorial route tests 2016-05-06 14:06:54 +02:00
Johannes Zellner e48156dceb postprocess showTutorial to ensure we deal with a boolean 2016-05-06 14:05:47 +02:00
Johannes Zellner f3811e3df9 Remove all unused vars in profile test 2016-05-06 14:03:24 +02:00
Johannes Zellner 0d40b1b80d Fix the test wording in profile password change tests 2016-05-06 14:02:21 +02:00
Johannes Zellner 8b92c8f7ae Remove unused checkMails() in profile tests 2016-05-06 13:57:46 +02:00
Johannes Zellner d41eb81b3d Add new profile/ route to set the showTutorial field 2016-05-06 13:56:40 +02:00
Johannes Zellner 3adf91afed Add setShowTutorial() api to users.js 2016-05-06 13:56:26 +02:00
Johannes Zellner 18f05de8ae use users.showTutorial field in userdb 2016-05-06 13:56:05 +02:00
Johannes Zellner b0f4396389 Add showTutorial field to users table 2016-05-06 13:55:03 +02:00
Johannes Zellner bf99475dbd Introduce basic welcome tutorial flow 2016-05-06 13:06:12 +02:00
Girish Ramakrishnan d50fa70f47 pass -out 2016-05-05 21:26:13 -07:00
Girish Ramakrishnan 0e655cadb0 generate dkim keys before dns setup
Two things require DKIM keys
1. the mail addon
2. the DNS TXT record
2016-05-05 21:15:10 -07:00
Girish Ramakrishnan 496e1c3dc1 fix path to INFRA_VERSION 2016-05-05 18:37:17 -07:00
Girish Ramakrishnan 325252699e set MAIL_FROM more smartly 2016-05-05 18:33:22 -07:00
Girish Ramakrishnan 2d43e22285 fix typo 2016-05-05 15:26:32 -07:00
Girish Ramakrishnan 9e673c3890 supply a bogus username/password 2016-05-05 15:24:52 -07:00
Girish Ramakrishnan c3c18e8a4b reserve more ports 2016-05-05 15:00:07 -07:00
Girish Ramakrishnan cb1bd58cb9 do not export submission port just yet 2016-05-05 14:53:07 -07:00
Girish Ramakrishnan 0bdff14c9f r -> ro 2016-05-05 13:54:57 -07:00
Girish Ramakrishnan c4ae9526af look for fallback cert in nginx cert dir 2016-05-05 13:52:08 -07:00
Girish Ramakrishnan 8d79ac9ae0 provide tls cert and key to mail server
haraka requires tls certs for:
1. supporting AUTH
2. port 587 support (MSA)

currently, we just reuse the cert for the admin domain. Otherwise,
we have to setup dns etc to get a new cert. While doable, its' not
necessary right now.
2016-05-05 13:18:17 -07:00
Girish Ramakrishnan 7d4ed5bafc Revert "x"
This reverts commit a1554b9cc1642037e9fbd0d261722c908f499aab.

committed by mistake
2016-05-05 12:57:04 -07:00
Johannes Zellner 85db8f398b Ensure we require an admin for settings routes
The cloudron name and avatar already have public routes.
2016-05-05 12:48:21 +02:00
Girish Ramakrishnan 636b71ce6f Add MAIL_FROM env 2016-05-05 00:28:08 -07:00
Girish Ramakrishnan b46008f0b1 add sendmail ou bind
this will be used by haraka to authenticate the apps
2016-05-05 00:26:43 -07:00
Girish Ramakrishnan 9d9bd42cd2 x 2016-05-05 00:09:16 -07:00
Girish Ramakrishnan 0f6a2a42f2 use latest mysql image 2016-05-04 23:09:30 -07:00
Girish Ramakrishnan fc8bf82993 Add getters for fallback and admin cert 2016-05-04 17:37:21 -07:00
Girish Ramakrishnan e56192913d reserved smtp and imap locations 2016-05-04 15:54:21 -07:00
Girish Ramakrishnan 2d08ce441f rename arg_fqdn to fqdn 2016-05-04 15:54:21 -07:00
Girish Ramakrishnan 87497c2047 pass fqdn as arg 2016-05-04 15:54:21 -07:00
Girish Ramakrishnan 32a0bf6fd2 add --check arg to setup_infra.sh 2016-05-04 15:54:21 -07:00
Girish Ramakrishnan 291e625785 skip infra setup for tests (the test does it itself) 2016-05-04 15:54:21 -07:00
Girish Ramakrishnan 6bcfd33e10 boot2docker is dead 2016-05-04 15:54:21 -07:00
Girish Ramakrishnan b4c15b1719 Let the box code initialize the infrastructure
This is done because:
1. The box code can install certs for addons (like mail addon) when
   required.

2. The box code initialize/teardown addons on demand. This is not planned
   currently.
2016-05-04 15:54:21 -07:00
Girish Ramakrishnan 920626192c fix error message 2016-05-04 09:28:50 -07:00
Johannes Zellner 778371b818 Only send out mails if the admin group has changed 2016-05-04 13:55:14 +02:00
Girish Ramakrishnan d2a1cea1e8 0.13.2 changes 2016-05-03 23:48:45 -07:00
Girish Ramakrishnan 5683cefe89 provide auditSource when autoupdating app 2016-05-03 23:36:27 -07:00
Girish Ramakrishnan 126d64ffa8 move backup event in backupBoxAndApps
the updater uses this code route
2016-05-03 18:36:52 -07:00
Girish Ramakrishnan 7262eb208f add route to get the timezone 2016-05-03 12:10:21 -07:00
Girish Ramakrishnan f57f8f5e58 fix typo 2016-05-03 12:09:58 -07:00
Girish Ramakrishnan 00ad1308aa handle empty time_zone 2016-05-03 12:09:22 -07:00
Girish Ramakrishnan 2cc37a9c31 0.13.1 changes 2016-05-03 11:50:52 -07:00
Girish Ramakrishnan 13b0093f20 telize has been discontinued. use freegeoip 2016-05-03 11:50:31 -07:00
Girish Ramakrishnan 5617baea50 eventlog tests 2016-05-02 14:54:25 -07:00
Girish Ramakrishnan da79e4f229 only admin can view activity logs 2016-05-02 14:54:20 -07:00
Girish Ramakrishnan 6c1bd522e6 add pagination 2016-05-02 11:50:15 -07:00
Girish Ramakrishnan 7fca9decc1 fixes to eventlog 2016-05-02 11:07:55 -07:00
Girish Ramakrishnan 7cf08b6a4d add start event
this signals that an update worked
2016-05-02 10:01:23 -07:00
Girish Ramakrishnan ffedbdfa13 various minor fixes to eventlog 2016-05-02 10:01:23 -07:00
Johannes Zellner 43e207a301 Merge branch 'v0.12.7-hotfix' 2016-05-02 17:34:56 +02:00
Johannes Zellner b855dee4cb Fix crash by providing required eventsource 2016-05-02 13:43:12 +02:00
Girish Ramakrishnan b322f6805f move authType into source 2016-05-01 21:53:44 -07:00
Girish Ramakrishnan ccc119ddec add appLocation to user login 2016-05-01 21:47:35 -07:00
Girish Ramakrishnan 994cbaa22a add event log in model code 2016-05-01 21:38:20 -07:00
Girish Ramakrishnan d7a34bbf68 remove profile action 2016-05-01 20:14:21 -07:00
Girish Ramakrishnan 1f31fe6f8f make user.remove and user.update add eventlog 2016-05-01 20:11:11 -07:00
Girish Ramakrishnan 37bdd2672b make user.create take auditSource 2016-05-01 20:01:34 -07:00
Girish Ramakrishnan 63702f836a move developer eventlogs to model code 2016-05-01 13:46:33 -07:00
Girish Ramakrishnan 5898428a6c use req.connection.remoteAddress 2016-05-01 13:29:11 -07:00
Girish Ramakrishnan f4a6c64956 make cloudron.activate take an auditSource 2016-05-01 13:27:57 -07:00
Girish Ramakrishnan f9d4d3014d remove reboot (user cannot initiate anyway) 2016-05-01 13:23:31 -07:00
Girish Ramakrishnan 8254337552 make cloudron.updateToLatest take an auditSource 2016-05-01 13:17:35 -07:00
Girish Ramakrishnan d811115f21 update schema.sql 2016-05-01 13:17:27 -07:00
Girish Ramakrishnan fec388b648 move backup eventlog to model 2016-05-01 13:17:23 -07:00
Girish Ramakrishnan a969e323a6 what if cron was a username 2016-05-01 11:48:29 -07:00
Girish Ramakrishnan 09584ac29c fix failing test 2016-04-30 23:26:45 -07:00
Girish Ramakrishnan 7967610f3f add user login to event log 2016-04-30 23:18:14 -07:00
Girish Ramakrishnan 727332fe66 save the userId as well 2016-04-30 23:04:54 -07:00
Girish Ramakrishnan 09595d1c43 make tests pass 2016-04-30 22:35:46 -07:00
Girish Ramakrishnan c4ad6c803f add certificate renew event 2016-04-30 22:27:33 -07:00
Girish Ramakrishnan fd1a00d280 better text for action 2016-04-30 20:25:20 -07:00
Girish Ramakrishnan 5c2a650681 add activity view 2016-04-30 20:25:20 -07:00
Girish Ramakrishnan 43051cea3b add app update event 2016-04-30 20:25:20 -07:00
Girish Ramakrishnan 3d50a251ee store email in USER_ADD event 2016-04-30 20:25:20 -07:00
Girish Ramakrishnan 219df8babd pick up correct ip 2016-04-30 19:08:24 -07:00
Girish Ramakrishnan d3d9706a70 make response plural 2016-04-30 18:57:43 -07:00
Girish Ramakrishnan 8a3ad6c964 store the activation username 2016-04-30 14:32:35 -07:00
Girish Ramakrishnan 90719cd4d9 do not store entire manifest 2016-04-30 14:28:59 -07:00
Girish Ramakrishnan 30445ddab9 more changes 2016-04-30 14:14:02 -07:00
Girish Ramakrishnan 157fbc89b8 add eventlog route 2016-04-30 14:08:44 -07:00
Girish Ramakrishnan 71219c6af7 add eventlog hooks 2016-04-30 14:05:19 -07:00
Girish Ramakrishnan 934abafbd4 make actions seem like commands 2016-04-30 13:34:06 -07:00
Girish Ramakrishnan bc6e896507 add eventlog test 2016-04-30 13:02:57 -07:00
Girish Ramakrishnan ca8731c282 add source to events table 2016-04-30 12:56:23 -07:00
Girish Ramakrishnan c511019d79 remove jslint hint 2016-04-30 11:53:46 -07:00
Girish Ramakrishnan 992c4ee847 add some event enums 2016-04-30 11:49:51 -07:00
Girish Ramakrishnan 8c427553ba add eventlogdb tests 2016-04-30 10:16:27 -07:00
Girish Ramakrishnan c1df22f079 use JSON type (nice for querying) 2016-04-30 10:15:33 -07:00
Girish Ramakrishnan 7673ecde2f Add eventlog model file 2016-04-29 23:58:29 -07:00
Girish Ramakrishnan a9d0cf66fd Add eventlogdb 2016-04-29 23:58:24 -07:00
Girish Ramakrishnan db89784af8 add eventlog table 2016-04-29 23:58:20 -07:00
Girish Ramakrishnan 4143f903ad 0.13.0 changes 2016-04-29 20:50:55 -07:00
Girish Ramakrishnan 12820db4a5 fix typo in mysql config 2016-04-29 20:20:52 -07:00
Girish Ramakrishnan 8837cc5a3c update nginx version 2016-04-29 19:38:06 -07:00
Girish Ramakrishnan f2545e3def bump systemd to 229 2016-04-29 19:18:31 -07:00
Girish Ramakrishnan 7a72bf3f78 select mysql 5.7 2016-04-29 19:12:20 -07:00
Girish Ramakrishnan 87351f04ef use 16.04
among other things this will bump the mysql version bringing us
json type
2016-04-29 19:11:00 -07:00
Girish Ramakrishnan 4a04e0b52f use recommendation from raymii.org 2016-04-28 09:59:03 -07:00
136 changed files with 11150 additions and 4768 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
}
+69
View File
@@ -489,3 +489,72 @@
[0.12.7]
- Fix changing password
[0.13.0]
- Upgrade to ubuntu 16.04
- Add event log
[0.13.1]
- Make activity log viewable to admins
- Fix geoip lookup
[0.13.2]
- Fix crash in app auto updater
- Fix crash with empty timezone
[0.13.3]
- Enable auth in email addon
- Add search for activity log
- Add tutorial for first time users
[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
@@ -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}/../setup/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}"
+144
View File
@@ -0,0 +1,144 @@
#!/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="testing"
security_group="sg-b9a473d1"
instance_type="t2.micro"
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}"
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\"}]}"
echo "Copy image to other regions"
aws ec2 copy-image --region us-west-2 --profile ${aws_credentials} --source-image-id ${image_id} --source-region ${region} --name ${snapshot_name}
aws ec2 copy-image --region ap-southeast-1 --profile ${aws_credentials} --source-image-id ${image_id} --source-region ${region} --name ${snapshot_name}
echo "Done."
echo ""
echo "--------------------------------------------------"
echo "New image id is: ${image_id}"
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
@@ -30,7 +30,7 @@ function create_droplet() {
local box_name="$2"
local image_region="sfo1"
local ubuntu_image_slug="ubuntu-15-10-x64"
local ubuntu_image_slug="ubuntu-16-04-x64"
local box_size="512mb"
local data="{\"name\":\"${box_name}\",\"size\":\"${box_size}\",\"region\":\"${image_region}\",\"image\":\"${ubuntu_image_slug}\",\"ssh_keys\":[ \"${ssh_key_id}\" ],\"backups\":false}"
@@ -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() {
+33 -73
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"
@@ -17,19 +16,7 @@ function die {
exit 1
}
[[ "$(systemd --version 2>&1)" == *"systemd 225"* ]] || die "Expecting systemd to be 225"
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
[[ "$(systemd --version 2>&1)" == *"systemd 229"* ]] || die "Expecting systemd to be 229"
echo "==== Create User ${USER} ===="
if ! id "${USER}"; then
@@ -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,34 +139,26 @@ 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
[[ "$(nginx -v 2>&1)" == *"nginx/1.9."* ]] || die "Expecting nginx version to be 1.9.x"
[[ "$(nginx -v 2>&1)" == *"nginx/1.10."* ]] || die "Expecting nginx version to be 1.10.x"
echo "==== Install build-essential ===="
apt-get -y install build-essential rcconf
@@ -191,8 +166,8 @@ apt-get -y install build-essential rcconf
echo "==== Install mysql ===="
debconf-set-selections <<< 'mysql-server mysql-server/root_password password password'
debconf-set-selections <<< 'mysql-server mysql-server/root_password_again password password'
apt-get -y install mysql-server
[[ "$(mysqld --version 2>&1)" == *"5.6."* ]] || die "Expecting nginx version to be 5.6.x"
apt-get -y install mysql-server-5.7
[[ "$(mysqld --version 2>&1)" == *"5.7."* ]] || die "Expecting mysql version to be 5.7.x"
echo "==== Install pwgen and swaks awscli ===="
apt-get -y install pwgen swaks awscli
@@ -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
+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
];
}
@@ -0,0 +1,24 @@
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
exports.up = function(db, callback) {
var cmd = "CREATE TABLE eventlog(" +
"id VARCHAR(128) NOT NULL," +
"source JSON," +
"creationTime TIMESTAMP," +
"action VARCHAR(128) NOT NULL," +
"data JSON," +
"PRIMARY KEY (id))";
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE eventlog', 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 users ADD COLUMN showTutorial BOOLEAN DEFAULT 0', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP COLUMN showTutorial', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -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);
});
};
+28 -8
View File
@@ -19,6 +19,7 @@ CREATE TABLE IF NOT EXISTS users(
modifiedAt VARCHAR(512) NOT NULL,
admin INTEGER NOT NULL,
displayName VARCHAR(512) DEFAULT '',
showTutorial BOOLEAN DEFAULT 0,
PRIMARY KEY(id));
CREATE TABLE IF NOT EXISTS groups(
@@ -37,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(
@@ -53,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),
@@ -61,16 +62,15 @@ CREATE TABLE IF NOT EXISTS apps(
httpPort INTEGER, // this is the nginx proxy port and not manifest.httpPort
location VARCHAR(128) NOT NULL UNIQUE,
dnsRecordId VARCHAR(512),
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),
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));
@@ -85,7 +85,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(
@@ -100,7 +100,7 @@ CREATE TABLE IF NOT EXISTS appAddonConfigs(
FOREIGN KEY(appId) REFERENCES apps(id));
CREATE TABLE IF NOT EXISTS backups(
id VARCHAR(128) NOT NULL,
filename VARCHAR(128) NOT NULL,
creationTime TIMESTAMP,
version VARCHAR(128) NOT NULL, /* app version or box version */
type VARCHAR(16) NOT NULL, /* 'box' or 'app' */
@@ -108,3 +108,23 @@ CREATE TABLE IF NOT EXISTS backups(
state VARCHAR(16) NOT NULL,
PRIMARY KEY (filename));
CREATE TABLE IF NOT EXISTS eventlog(
id VARCHAR(128) NOT NULL,
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"
-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=27
# 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.10.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
+1 -1
View File
@@ -4,4 +4,4 @@
# http://bugs.mysql.com/bug.php?id=68514
[mysqld]
performance_schema=OFF
max_connection=50
max_connections=50
+6 -5
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}/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,13 +22,16 @@ 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}" == "true" || "${existing_infra}" != "${current_infra}" ]]; then
echo "Showing progress bar on all subdomains in retired mode or infra update. retire: ${arg_retire} 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"
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"
fi
+11 -12
View File
@@ -39,7 +39,7 @@ set_progress "10" "Ensuring directories"
[[ "${is_update}" == "false" ]] && btrfs subvolume create "${DATA_DIR}/box"
mkdir -p "${DATA_DIR}/box/appicons"
mkdir -p "${DATA_DIR}/box/certs"
mkdir -p "${DATA_DIR}/box/mail"
mkdir -p "${DATA_DIR}/box/mail/dkim/${arg_fqdn}"
mkdir -p "${DATA_DIR}/box/acme" # acme keys
mkdir -p "${DATA_DIR}/graphite"
@@ -120,11 +120,9 @@ 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 "40" "Setting up infra"
${script_dir}/start/setup_infra.sh "${arg_fqdn}"
set_progress "65" "Creating cloudron.conf"
sudo -u yellowtent -H bash <<EOF
set -eu
@@ -138,7 +136,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",
@@ -191,18 +188,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
+6 -1
View File
@@ -18,11 +18,15 @@ server {
# https://bettercrypto.org/static/applied-crypto-hardening.pdf
# https://mozilla.github.io/server-side-tls/ssl-config-generator/
# https://cipherli.st/
# https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
ssl_prefer_server_ciphers on;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # don't use SSLv3 ref: POODLE
ssl_ciphers 'AES128+EECDH:AES128+EDH';
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 SAMEORIGIN;
proxy_http_version 1.1;
proxy_intercept_errors on;
proxy_read_timeout 3500;
@@ -58,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;
-133
View File
@@ -1,133 +0,0 @@
#!/bin/bash
set -eu -o pipefail
readonly DATA_DIR="/home/yellowtent/data"
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${script_dir}/../INFRA_VERSION" # this injects INFRA_VERSION
arg_fqdn="$1"
# 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 "${arg_fqdn}" \
-e "MAIL_SERVER_NAME=${arg_fqdn}" \
-e "MAIL_DOMAIN=${arg_fqdn}" \
-v "${DATA_DIR}/box/mail:/app/data" \
--read-only -v /tmp -v /run \
"${MAIL_IMAGE}")
echo "Mail container id: ${mail_container_id}"
if docker images "${MAIL_REPO}" | tail -n +2 | awk '{ print $1 ":" $2 }' | grep -v "${MAIL_IMAGE}" | xargs --no-run-if-empty docker rmi; then
echo "Removed old mail images"
fi
# mysql
mysql_addon_root_password=$(pwgen -1 -s)
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 "${arg_fqdn}" \
-v "${DATA_DIR}/mysql:/var/lib/mysql" \
-v "${DATA_DIR}/addons/mysql_vars.sh:/etc/mysql/mysql_vars.sh:ro" \
--read-only -v /tmp -v /run \
"${MYSQL_IMAGE}")
echo "MySQL container id: ${mysql_container_id}"
if docker images "${MYSQL_REPO}" | tail -n +2 | awk '{ print $1 ":" $2 }' | grep -v "${MYSQL_IMAGE}" | xargs --no-run-if-empty docker rmi; then
echo "Removed old mysql images"
fi
# postgresql
postgresql_addon_root_password=$(pwgen -1 -s)
cat > "${DATA_DIR}/addons/postgresql_vars.sh" <<EOF
readonly POSTGRESQL_ROOT_PASSWORD='${postgresql_addon_root_password}'
EOF
postgresql_container_id=$(docker run --restart=always -d --name="postgresql" \
-m 100m \
--memory-swap 200m \
-h "${arg_fqdn}" \
-v "${DATA_DIR}/postgresql:/var/lib/postgresql" \
-v "${DATA_DIR}/addons/postgresql_vars.sh:/etc/postgresql/postgresql_vars.sh:ro" \
--read-only -v /tmp -v /run \
"${POSTGRESQL_IMAGE}")
echo "PostgreSQL container id: ${postgresql_container_id}"
if docker images "${POSTGRESQL_REPO}" | tail -n +2 | awk '{ print $1 ":" $2 }' | grep -v "${POSTGRESQL_IMAGE}" | xargs --no-run-if-empty docker rmi; then
echo "Removed old postgresql images"
fi
# mongodb
mongodb_addon_root_password=$(pwgen -1 -s)
cat > "${DATA_DIR}/addons/mongodb_vars.sh" <<EOF
readonly MONGODB_ROOT_PASSWORD='${mongodb_addon_root_password}'
EOF
mongodb_container_id=$(docker run --restart=always -d --name="mongodb" \
-m 100m \
--memory-swap 200m \
-h "${arg_fqdn}" \
-v "${DATA_DIR}/mongodb:/var/lib/mongodb" \
-v "${DATA_DIR}/addons/mongodb_vars.sh:/etc/mongodb_vars.sh:ro" \
--read-only -v /tmp -v /run \
"${MONGODB_IMAGE}")
echo "Mongodb container id: ${mongodb_container_id}"
if docker images "${MONGODB_REPO}" | tail -n +2 | awk '{ print $1 ":" $2 }' | grep -v "${MONGODB_IMAGE}" | xargs --no-run-if-empty docker rmi; then
echo "Removed old mongodb images"
fi
# redis
if docker images "${REDIS_REPO}" | tail -n +2 | awk '{ print $1 ":" $2 }' | grep -v "${REDIS_IMAGE}" | xargs --no-run-if-empty docker rmi; then
echo "Removed old redis images"
fi
# only touch apps in installed state. any other state is just resumed by the taskmanager
if [[ "${infra_version}" == "none" ]]; then
# if no existing infra was found (for new, 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"
+142 -140
View File
@@ -7,7 +7,6 @@ exports = module.exports = {
restoreAddons: restoreAddons,
getEnvironment: getEnvironment,
getLinksSync: getLinksSync,
getBindsSync: getBindsSync,
getContainerNamesSync: getContainerNamesSync,
@@ -19,28 +18,34 @@ exports = module.exports = {
var appdb = require('./appdb.js'),
assert = require('assert'),
async = require('async'),
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,
@@ -77,6 +82,12 @@ var KNOWN_ADDONS = {
backup: backupPostgreSql,
restore: restorePostgreSql
},
recvmail: {
setup: setupRecvMail,
teardown: teardownRecvMail,
backup: NOOP,
restore: setupRecvMail
},
redis: {
setup: setupRedis,
teardown: teardownRedis,
@@ -197,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');
@@ -265,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()
];
@@ -298,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);
});
@@ -311,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);
@@ -344,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');
@@ -386,19 +399,17 @@ function setupSendMail(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var username = app.location ? app.location + '-app' : 'no-reply'; // use no-reply for bare domains
var from = (app.location ? app.location : app.manifest.title.replace(/[^a-zA-Z0-9]/, '')) + '.app';
var env = [
'MAIL_SMTP_SERVER=mail',
'MAIL_SMTP_PORT=2500', // if you change this, change the mail container
'MAIL_SMTP_USERNAME=' + username,
'MAIL_SMTP_PASSWORD=' + hat(256), // this is ignored
'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) {
@@ -408,7 +419,55 @@ function teardownSendMail(app, options, callback) {
debugApp(app, 'Tearing down sendmail');
appdb.unsetAddonConfig(app.id, 'sendmail', callback);
var from = (app.location ? app.location : app.manifest.title.replace(/[^a-zA-Z0-9]/, '')) + '.app';
var cmd = [ '/addons/mail/service.sh', 'remove-send', from ];
debugApp(app, 'Tearing down sendmail');
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]/, '')) + '.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]/, '')) + '.app';
var cmd = [ '/addons/mail/service.sh', 'remove-recv', to ];
debugApp(app, 'Tearing down recvmail');
docker.execContainer('mail', cmd, { }, function (error) {
if (error) return callback(error);
appdb.unsetAddonConfig(app.id, 'recvmail', callback);
});
}
function setupMySql(app, options, callback) {
@@ -601,38 +660,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');
@@ -649,57 +676,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 setup/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);
});
}
+17 -33
View File
@@ -4,7 +4,6 @@
exports = module.exports = {
get: get,
getBySubdomain: getBySubdomain,
getByHttpPort: getByHttpPort,
getByContainerId: getByContainerId,
add: add,
@@ -27,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
@@ -59,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' ].join(',');
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'environmentVariable', 'appId' ].join(',');
@@ -70,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;
@@ -114,22 +110,6 @@ function get(id, callback) {
});
}
function getBySubdomain(subdomain, callback) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
+ ' FROM apps LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId WHERE location = ? GROUP BY apps.id', [ subdomain ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
postProcess(result[0]);
callback(null, result[0]);
});
}
function getByHttpPort(httpPort, callback) {
assert.strictEqual(typeof httpPort, 'number');
assert.strictEqual(typeof callback, 'function');
@@ -177,27 +157,31 @@ 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 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, lastBackupId) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain, lastBackupId ]
});
Object.keys(portBindings).forEach(function (env) {
@@ -301,9 +285,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]));
@@ -362,14 +343,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'));
}
+1 -1
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;
+413 -236
View File
@@ -6,7 +6,6 @@ exports = module.exports = {
hasAccessTo: hasAccessTo,
get: get,
getBySubdomain: getBySubdomain,
getByIpAddress: getByIpAddress,
getAll: getAll,
getAllByUser: getAllByUser,
@@ -16,6 +15,7 @@ exports = module.exports = {
uninstall: uninstall,
restore: restore,
clone: clone,
update: update,
@@ -31,7 +31,12 @@ exports = module.exports = {
checkManifestConstraints: checkManifestConstraints,
autoupdateApps: autoupdateApps,
updateApps: updateApps,
restoreInstalledApps: restoreInstalledApps,
configureInstalledApps: configureInstalledApps,
getAppConfig: getAppConfig,
// exported for testing
_validateHostname: validateHostname,
@@ -44,12 +49,14 @@ 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'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:apps'),
docker = require('./docker.js'),
eventlog = require('./eventlog.js'),
fs = require('fs'),
groups = require('./groups.js'),
manifestFormat = require('cloudron-manifestformat'),
@@ -62,6 +69,7 @@ var addons = require('./addons.js'),
superagent = require('superagent'),
taskmanager = require('./taskmanager.js'),
util = require('util'),
uuid = require('node-uuid'),
validator = require('validator');
// http://dustinsenos.com/articles/customErrorsInNode
@@ -103,16 +111,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 ];
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;
}
@@ -126,17 +134,23 @@ function validatePortBindings(portBindings, tcpPorts) {
25, /* smtp */
53, /* dns */
80, /* http */
143, /* imap */
443, /* https */
465, /* smtps */
587, /* submission */
919, /* ssh */
993, /* imaps */
2003, /* graphite (lo) */
2004, /* graphite (lo) */
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) */
];
@@ -146,8 +160,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]));
}
@@ -170,19 +184,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;
}
@@ -197,8 +212,8 @@ 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;
}
@@ -225,6 +240,17 @@ 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,
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;
@@ -267,21 +293,6 @@ function get(appId, callback) {
});
}
function getBySubdomain(subdomain, callback) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof callback, 'function');
appdb.getBySubdomain(subdomain, 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));
app.iconUrl = getIconUrlSync(app);
app.fqdn = app.altDomain || config.appFqdn(app.location);
callback(null, app);
});
}
function getByIpAddress(ip, callback) {
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -353,218 +364,263 @@ function purchase(appStoreId, callback) {
});
}
function install(appId, appStoreId, manifest, location, portBindings, accessRestriction, icon, cert, key, memoryLimit, altDomain, 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;
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);
// 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));
}
}
taskmanager.restartAppTask(appId);
error = certificates.validateCertificate(cert, key, config.appFqdn(location));
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
callback(null);
var appId = uuid.v4();
debug('Will install app with id : ' + appId);
purchase(appStoreId, function (error) {
if (error) return callback(error);
var data = {
accessRestriction: accessRestriction,
memoryLimit: memoryLimit,
altDomain: altDomain
};
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, 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;
}
// 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));
taskmanager.restartAppTask(appId);
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId });
callback(null);
});
});
}
function update(appId, force, manifest, portBindings, icon, 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 = '';
}
callback(null);
// 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;
}
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);
});
});
});
}
@@ -614,8 +670,10 @@ function getLogs(appId, lines, follow, callback) {
});
}
function restore(appId, 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');
debug('Will restore app with id:%s', appId);
@@ -624,47 +682,106 @@ function restore(appId, 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'));
callback(null);
// re-validate because this new box version may not accept old configs
error = checkManifestConstraints(restoreConfig.manifest);
if (error) return callback(error);
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 uninstall(appId, callback) {
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,
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 });
});
});
});
});
}
function uninstall(appId, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug('Will uninstall app with id:%s', appId);
@@ -674,6 +791,8 @@ function uninstall(appId, callback) {
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));
eventlog.add(eventlog.ACTION_APP_UNINSTALL, auditSource, { appId: appId });
taskmanager.startAppTask(appId, callback);
});
});
@@ -712,14 +831,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;
@@ -743,10 +864,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
};
@@ -756,9 +881,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) {
@@ -771,23 +905,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;
}
@@ -806,7 +942,12 @@ function autoupdateApps(updateInfo, callback) { // updateInfo is { appId -> { ma
return iteratorDone();
}
update(appId, false /* force */, updateInfo[appId].manifest, app.portBindings, null /* icon */, 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);
@@ -853,3 +994,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);
});
}
+22 -16
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();
@@ -642,10 +639,13 @@ function update(app, callback) {
async.series([
updateApp.bind(null, app, { installationProgress: '30, Backup app' }),
backups.backupApp.bind(null, app, app.oldConfig.manifest.addons)
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 -35
View File
@@ -10,17 +10,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 +31,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 +66,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 +84,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 +100,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);
});
});
}));
+76 -45
View File
@@ -7,6 +7,7 @@ exports = module.exports = {
getByAppIdPaged: getByAppIdPaged,
getRestoreUrl: getRestoreUrl,
getRestoreConfig: getRestoreConfig,
ensureBackup: ensureBackup,
@@ -27,6 +28,7 @@ var addons = require('./addons.js'),
config = require('./config.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:backups'),
eventlog = require('./eventlog.js'),
locker = require('./locker.js'),
path = require('path'),
paths = require('./paths.js'),
@@ -35,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');
@@ -134,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) {
@@ -160,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');
@@ -184,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));
@@ -224,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));
@@ -247,10 +279,10 @@ function backupBoxWithAppBackupIds(appBackupIds, callback) {
// function backupBox(callback) {
// apps.getAll(function (error, allApps) {
// if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
//
//
// var appBackupIds = allApps.map(function (app) { return app.lastBackupId; });
// appBackupIds = appBackupIds.filter(function (id) { return id !== null; }); // remove apps that were never backed up
//
//
// backupBoxWithAppBackupIds(appBackupIds, callback);
// });
// }
@@ -266,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);
@@ -279,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);
@@ -309,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));
@@ -323,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) {
@@ -336,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);
@@ -358,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);
@@ -367,9 +393,13 @@ function backupApp(app, addonsToBackup, callback) {
}
// this function expects you to have a lock
function backupBoxAndApps(callback) {
function backupBoxAndApps(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
callback = callback || NOOP_CALLBACK;
eventlog.add(eventlog.ACTION_BACKUP_START, auditSource, { });
apps.getAll(function (error, allApps) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
@@ -381,7 +411,7 @@ function backupBoxAndApps(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);
@@ -402,24 +432,25 @@ function backupBoxAndApps(callback) {
backupBoxWithAppBackupIds(backupIds, function (error, filename) {
progress.set(progress.BACKUP, 100, error ? error.message : '');
eventlog.add(eventlog.ACTION_BACKUP_FINISH, auditSource, { errorMessage: error ? error.message : null, filename: filename });
callback(error, filename);
});
});
});
}
function backup(callback) {
function backup(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
var error = locker.lock(locker.OP_FULL_BACKUP);
if (error) return callback(new BackupsError(BackupsError.BAD_STATE, error.message));
// ensure tools can 'wait' on progress
progress.set(progress.BACKUP, 0, 'Starting');
progress.set(progress.BACKUP, 0, 'Starting'); // ensure tools can 'wait' on progress
// start the backup operation in the background
backupBoxAndApps(function (error) {
if (error) console.error('backup failed.', error);
backupBoxAndApps(auditSource, function (error) { // start the backup operation in the background
if (error) debug('backup failed.', error);
locker.unlock(locker.OP_FULL_BACKUP);
});
@@ -427,8 +458,8 @@ function backup(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) {
@@ -441,7 +472,7 @@ function ensureBackup(callback) {
return callback(null);
}
backup(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'),
+59 -24
View File
@@ -1,5 +1,19 @@
'use strict';
exports = module.exports = {
installAdminCertificate: installAdminCertificate,
renewAll: renewAll,
setFallbackCertificate: setFallbackCertificate,
setAdminCertificate: setAdminCertificate,
CertificatesError: CertificatesError,
validateCertificate: validateCertificate,
ensureCertificate: ensureCertificate,
getAdminCertificatePath: getAdminCertificatePath,
// exported for testing
_getApi: getApi
};
var acme = require('./cert/acme.js'),
apps = require('./apps.js'),
assert = require('assert'),
@@ -9,6 +23,7 @@ var acme = require('./cert/acme.js'),
config = require('./config.js'),
constants = require('./constants.js'),
debug = require('debug')('box:src/certificates'),
eventlog = require('./eventlog.js'),
fs = require('fs'),
mailer = require('./mailer.js'),
nginx = require('./nginx.js'),
@@ -22,16 +37,6 @@ var acme = require('./cert/acme.js'),
waitForDns = require('./waitfordns.js'),
x509 = require('x509');
exports = module.exports = {
installAdminCertificate: installAdminCertificate,
autoRenew: autoRenew,
setFallbackCertificate: setFallbackCertificate,
setAdminCertificate: setAdminCertificate,
CertificatesError: CertificatesError,
validateCertificate: validateCertificate,
ensureCertificate: ensureCertificate
};
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function CertificatesError(reason, errorOrMessage) {
@@ -55,6 +60,7 @@ function CertificatesError(reason, errorOrMessage) {
util.inherits(CertificatesError, Error);
CertificatesError.INTERNAL_ERROR = 'Internal Error';
CertificatesError.INVALID_CERT = 'Invalid certificate';
CertificatesError.NOT_FOUND = 'Not Found';
function getApi(app, callback) {
assert.strictEqual(typeof app, 'object');
@@ -63,10 +69,15 @@ 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.
@@ -91,8 +102,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
@@ -120,9 +131,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);
@@ -136,7 +149,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;
}
@@ -145,7 +158,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);
@@ -153,26 +166,28 @@ 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');
mailer.certificateRenewed(domain, error ? error.message : '');
var errorMessage = error ? error.message : '';
eventlog.add(eventlog.ACTION_CERTIFICATE_RENEWAL, auditSource, { domain: domain, errorMessage: errorMessage });
mailer.certificateRenewed(domain, 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);
// 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
@@ -254,6 +269,14 @@ function setFallbackCertificate(cert, key, callback) {
});
}
function getFallbackCertificatePath(callback) {
assert.strictEqual(typeof callback, 'function');
// any user fallback cert is always copied over to nginx cert dir
callback(null, path.join(paths.NGINX_CERT_DIR, 'host.cert'), path.join(paths.NGINX_CERT_DIR, 'host.key'));
}
// FIXME: setting admin cert needs to restart the mail container because it uses admin cert
function setAdminCertificate(cert, key, callback) {
assert.strictEqual(typeof cert, 'string');
assert.strictEqual(typeof key, 'string');
@@ -273,6 +296,18 @@ function setAdminCertificate(cert, key, callback) {
nginx.configureAdmin(certFilePath, keyFilePath, callback);
}
function getAdminCertificatePath(callback) {
assert.strictEqual(typeof callback, 'function');
var vhost = config.adminFqdn();
var certFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.cert');
var keyFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.key');
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, certFilePath, keyFilePath);
getFallbackCertificatePath(callback);
}
function ensureCertificate(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
+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);
});
}
+114 -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,10 @@ function ClientsError(reason, errorOrMessage) {
util.inherits(ClientsError, Error);
ClientsError.INVALID_SCOPE = 'Invalid scope';
ClientsError.INVALID_CLIENT = 'Invalid client';
ClientsError.INVALID_TOKEN = 'Invalid token';
ClientsError.NOT_FOUND = 'Not found';
ClientsError.INTERNAL_ERROR = 'Internal Error';
ClientsError.NOT_ALLOWED = 'Not allowed to remove this client';
function validateScope(scope) {
assert.strictEqual(typeof scope, 'string');
@@ -58,16 +77,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 +99,14 @@ 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);
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 +129,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 +140,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 +166,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 +187,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 +223,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 +236,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);
});
});
}
+67 -43
View File
@@ -12,7 +12,6 @@ exports = module.exports = {
sendHeartbeat: sendHeartbeat,
updateToLatest: updateToLatest,
update: update,
reboot: reboot,
retire: retire,
@@ -31,10 +30,12 @@ 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'),
fs = require('fs'),
locker = require('./locker.js'),
mailer = require('./mailer.js'),
@@ -52,9 +53,8 @@ var apps = require('./apps.js'),
updateChecker = require('./updatechecker.js'),
user = require('./user.js'),
UserError = user.UserError,
userdb = require('./userdb.js'),
util = require('util'),
uuid = require('node-uuid');
user = require('./user.js'),
util = require('util');
var REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'),
INSTALLER_UPDATE_URL = 'http://127.0.0.1:2020/api/v1/installer/update',
@@ -90,17 +90,16 @@ 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');
ensureDkimKeySync();
exports.events.on(exports.EVENT_CONFIGURED, addDnsRecords);
exports.events.on(exports.EVENT_FIRST_RUN, installAppBundle);
@@ -190,55 +189,62 @@ function setTimeZone(ip, callback) {
debug('setTimeZone ip:%s', ip);
superagent.get('http://www.telize.com/geoip/' + ip).end(function (error, result) {
// https://github.com/bluesmoon/node-geoip
// https://github.com/runk/node-maxmind
// { url: 'http://freegeoip.net/json/%s', jpath: 'time_zone' },
// { url: 'http://ip-api.com/json/%s', jpath: 'timezone' },
// { url: 'http://geoip.nekudo.com/api/%s', jpath: 'time_zone }
superagent.get('http://freegeoip.net/json/' + ip).end(function (error, result) {
if ((error && !error.response) || result.statusCode !== 200) {
debug('Failed to get geo location: %s', error.message);
return callback(null);
}
if (!result.body.timezone) {
if (!result.body.time_zone || typeof result.body.time_zone !== 'string') {
debug('No timezone in geoip response : %j', result.body);
return callback(null);
}
debug('Setting timezone to ', result.body.timezone);
debug('Setting timezone to ', result.body.time_zone);
settings.setTimeZone(result.body.timezone, callback);
settings.setTimeZone(result.body.time_zone, callback);
});
}
function activate(username, password, email, displayName, ip, callback) {
function activate(username, password, email, displayName, ip, auditSource, callback) {
assert.strictEqual(typeof username, 'string');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof email, 'string');
assert.strictEqual(typeof displayName, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug('activating user:%s email:%s', username, email);
setTimeZone(ip, function () { }); // TODO: get this from user. note that timezone is detected based on the browser location and not the cloudron region
user.createOwner(username, password, email, displayName, function (error, userObject) {
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
process.nextTick(function () { exports.events.emit(exports.EVENT_ACTIVATED); });
eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, { });
callback(null, { token: token, expires: expires });
});
});
@@ -248,7 +254,7 @@ function activate(username, password, email, displayName, ip, callback) {
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) {
@@ -352,6 +358,21 @@ function sendHeartbeat() {
});
}
function ensureDkimKeySync() {
var dkimPrivateKeyFile = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn() + '/private');
var dkimPublicKeyFile = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn() + '/public');
if (fs.existsSync(dkimPrivateKeyFile) && fs.existsSync(dkimPublicKeyFile)) {
debug('DKIM keys already present');
return;
}
debug('Generating new DKIM keys');
safe.child_process.execSync('openssl genrsa -out ' + dkimPrivateKeyFile + ' 1024');
safe.child_process.execSync('openssl rsa -in ' + dkimPrivateKeyFile + ' -out ' + dkimPublicKeyFile + ' -pubout -outform PEM');
}
function readDkimPublicKeySync() {
var dkimPublicKeyFile = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn() + '/public');
var publicKey = safe.fs.readFileSync(dkimPublicKeyFile, 'utf8');
@@ -412,21 +433,24 @@ function addDnsRecords() {
var DMARC_REPORT_EMAIL = 'dmarc-report@cloudron.io';
var dkimKey = readDkimPublicKeySync();
if (!dkimKey) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, new Error('internal error failed to read dkim public key')));
if (!dkimKey) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, new Error('Failed to read dkim public key')));
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
var nakedDomainRecord = { subdomain: '', type: 'A', values: [ ip ] };
@@ -435,6 +459,7 @@ function addDnsRecords() {
records.push(webadminRecord);
records.push(dkimRecord);
records.push(dmarcRecord);
records.push(mxRecord);
}
debug('addDnsRecords: %j', records);
@@ -470,8 +495,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);
@@ -479,6 +505,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');
@@ -510,13 +538,16 @@ function update(boxUpdateInfo, callback) {
}
function updateToLatest(callback) {
function updateToLatest(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
var boxUpdateInfo = updateChecker.getUpdateInfo().box;
if (!boxUpdateInfo) return callback(new CloudronError(CloudronError.ALREADY_UPTODATE, 'No update available'));
update(boxUpdateInfo, callback);
if (boxUpdateInfo.upgrade && config.provider() !== 'caas') return callback(new CloudronError(CloudronError.SELF_UPGRADE_NOT_SUPPORTED));
update(boxUpdateInfo, auditSource, callback);
}
function doShortCircuitUpdate(boxUpdateInfo, callback) {
@@ -539,7 +570,7 @@ function doUpgrade(boxUpdateInfo, callback) {
progress.set(progress.UPDATE, 5, 'Backing up for upgrade');
backups.backupBoxAndApps(function (error) {
backups.backupBoxAndApps({ userId: null, username: 'upgrader' }, function (error) {
if (error) return upgradeError(error);
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/upgrade')
@@ -568,7 +599,7 @@ function doUpdate(boxUpdateInfo, callback) {
progress.set(progress.UPDATE, 5, 'Backing up for update');
backups.backupBoxAndApps(function (error) {
backups.backupBoxAndApps({ userId: null, username: 'updater' }, function (error) {
if (error) return updateError(error);
// NOTE: the args here are tied to the installer revision, box code and appstore provisioning logic
@@ -626,23 +657,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 */, iteratorCallback);
});
apps.install(data, { userId: null, username: 'autoinstaller' }, iteratorCallback);
}, function (error) {
if (error) debug('autoInstallApps: ', error);
+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);
}
+5
View File
@@ -4,6 +4,11 @@
exports = module.exports = {
ADMIN_LOCATION: 'my',
API_LOCATION: 'api', // this is unused but reserved for future use (#403)
SMTP_LOCATION: 'smtp',
IMAP_LOCATION: 'imap',
MAIL_LOCATION: 'my', // not a typo! should be same as admin location until we figure out certificates
POSTMAN_LOCATION: 'postman', // used in dovecot bounces
ADMIN_NAME: 'Settings',
ADMIN_CLIENT_ID: 'webadmin', // oauth client id
+5 -4
View File
@@ -30,6 +30,7 @@ var gAutoupdaterJob = null,
gCheckDiskSpaceJob = null;
var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
var AUDIT_SOURCE = { userId: null, username: 'cron' };
// cron format
// Seconds: 0-59
@@ -66,7 +67,7 @@ function recreateJobs(unusedTimeZone, callback) {
if (gBackupJob) gBackupJob.stop();
gBackupJob = new CronJob({
cronTime: '00 00 */4 * * *', // every 4 hours
onTick: backups.ensureBackup,
onTick: backups.ensureBackup.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
start: true,
timeZone: allSettings[settings.TIME_ZONE_KEY]
});
@@ -122,7 +123,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 +155,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');
}
+3 -3
View File
@@ -1,5 +1,3 @@
/* jslint node: true */
'use strict';
exports = module.exports = {
@@ -121,7 +119,9 @@ function clear(callback) {
require('./tokendb.js')._clear,
require('./groupdb.js')._clear,
require('./userdb.js')._clear,
require('./settingsdb.js')._clear
require('./settingsdb.js')._clear,
require('./eventlogdb.js')._clear,
require('./mailboxdb.js')._clear
], callback);
}
+17 -7
View File
@@ -5,7 +5,7 @@
exports = module.exports = {
DeveloperError: DeveloperError,
enabled: enabled,
isEnabled: isEnabled,
setEnabled: setEnabled,
issueDeveloperToken: issueDeveloperToken,
getNonApprovedApps: getNonApprovedApps
@@ -13,7 +13,9 @@ 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'),
settings = require('./settings.js'),
superagent = require('superagent'),
@@ -41,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) {
@@ -50,27 +52,35 @@ function enabled(callback) {
});
}
function setEnabled(enabled, callback) {
function setEnabled(enabled, auditSource, callback) {
assert.strictEqual(typeof enabled, 'boolean');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
settings.setDeveloperMode(enabled, function (error) {
if (error) return callback(new DeveloperError(DeveloperError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_CLI_MODE, auditSource, { enabled: enabled });
callback(null);
});
}
function issueDeveloperToken(user, callback) {
function issueDeveloperToken(user, auditSource, callback) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
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));
callback(null, { token: token, expiresAt: expiresAt });
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource, { authType: 'cli', userId: user.id, username: user.username });
callback(null, { token: token, expiresAt: new Date(expiresAt).toISOString() });
});
}
@@ -80,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, []);
}
+9 -12
View File
@@ -133,7 +133,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 +172,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 +209,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 +372,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) {
+102
View File
@@ -0,0 +1,102 @@
'use strict';
exports = module.exports = {
EventLogError: EventLogError,
add: add,
get: get,
getAllPaged: getAllPaged,
// 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',
ACTION_APP_UNINSTALL: 'app.uninstall',
ACTION_APP_UPDATE: 'app.update',
ACTION_BACKUP_FINISH: 'backup.finish',
ACTION_BACKUP_START: 'backup.start',
ACTION_CERTIFICATE_RENEWAL: 'certificate.renew',
ACTION_CLI_MODE: 'settings.climode',
ACTION_START: 'cloudron.start',
ACTION_UPDATE: 'cloudron.update',
ACTION_USER_ADD: 'user.add',
ACTION_USER_LOGIN: 'user.login',
ACTION_USER_REMOVE: 'user.remove',
ACTION_USER_UPDATE: 'user.update'
};
var assert = require('assert'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:eventlog'),
eventlogdb = require('./eventlogdb.js'),
util = require('util'),
uuid = require('node-uuid');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function EventLogError(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(EventLogError, Error);
EventLogError.INTERNAL_ERROR = 'Internal error';
EventLogError.NOT_FOUND = 'Not Found';
function add(action, source, data, callback) {
assert.strictEqual(typeof action, 'string');
assert.strictEqual(typeof source, 'object');
assert.strictEqual(typeof data, 'object');
assert(!callback || typeof callback === 'function');
callback = callback || NOOP_CALLBACK;
var id = uuid.v4();
eventlogdb.add(id, action, source, data, function (error) {
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
callback(null, { id: id });
});
}
function get(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
eventlogdb.get(id, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new EventLogError(EventLogError.NOT_FOUND, 'No such event'));
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
callback(null, result);
});
}
function getAllPaged(action, search, page, perPage, callback) {
assert(typeof action === 'string' || action === null);
assert(typeof search === 'string' || search === null);
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
eventlogdb.getAllPaged(action, search, page, perPage, function (error, boxes) {
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
callback(null, boxes);
});
}
+104
View File
@@ -0,0 +1,104 @@
'use strict';
exports = module.exports = {
get: get,
getAllPaged: getAllPaged,
add: add,
count: count,
_clear: clear
};
var assert = require('assert'),
database = require('./database.js'),
DatabaseError = require('./databaseerror'),
mysql = require('mysql'),
safe = require('safetydance');
var EVENTLOGS_FIELDS = [ 'id', 'action', 'source', 'data', 'creationTime' ].join(',');
// until mysql module supports automatic type coercion
function postProcess(eventLog) {
eventLog.source = safe.JSON.parse(eventLog.source);
eventLog.data = safe.JSON.parse(eventLog.data);
return eventLog;
}
function get(eventId, callback) {
assert.strictEqual(typeof eventId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + EVENTLOGS_FIELDS + ' FROM eventlog WHERE id = ?', [ eventId ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null, postProcess(result[0]));
});
}
function getAllPaged(action, search, page, perPage, callback) {
assert(typeof action === 'string' || action === null);
assert(typeof search === 'string' || search === null);
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
var data = [];
var query = 'SELECT ' + EVENTLOGS_FIELDS + ' FROM eventlog';
if (action || search) query += ' WHERE';
if (search) query += ' data LIKE ' + mysql.escape('%' + search + '%');
if (action && search) query += ' AND ';
if (action) {
query += ' action=?';
data.push(action);
}
query += ' ORDER BY creationTime DESC LIMIT ?,?';
data.push((page-1)*perPage);
data.push(perPage);
database.query(query, data, function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results.forEach(postProcess);
callback(null, results);
});
}
function add(id, action, source, data, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof action, 'string');
assert.strictEqual(typeof source, 'object');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO eventlog (id, action, source, data) VALUES (?, ?, ?, ?)', [ id, action, JSON.stringify(source), JSON.stringify(data) ], function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error));
if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}
function count(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT COUNT(*) AS total FROM eventlog', function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
return callback(null, result[0].total);
});
}
function clear(callback) {
database.query('DELETE FROM eventlog', 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');
+21
View File
@@ -0,0 +1,21 @@
'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 = {
'version': 37,
'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.14.0' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:0.9.0' }
}
};
+213 -133
View File
@@ -9,9 +9,12 @@ var assert = require('assert'),
apps = require('./apps.js'),
config = require('./config.js'),
debug = require('debug')('box:ldap'),
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;
@@ -34,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();
});
}
@@ -44,146 +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 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()));
res.end();
});
});
});
});
gServer.listen(config.get('ldapPort'), callback);
gServer.listen(config.get('ldapPort'), '0.0.0.0', callback);
}
function stop(callback) {
+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);
});
}
+201
View File
@@ -0,0 +1,201 @@
'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();
// the alias is setup for the FQDN. otherwise, the mail alias plugin matches name@*
aliases = aliases.map(function (a) { return a + '@' + config.fqdn(); });
var cmd = [ '/addons/mail/service.sh', 'set-alias', name + '@' + config.fqdn() ].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);
});
}
+20 -15
View File
@@ -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,15 +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: 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) {
@@ -218,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' }),
@@ -244,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)
@@ -267,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' }),
@@ -302,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' })
@@ -320,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' })
@@ -338,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' })
@@ -356,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' })
@@ -370,7 +375,7 @@ function outOfDiskSpace(message) {
assert.strictEqual(typeof message, 'string');
var mailOptions = {
from: config.adminEmail(),
from: platform.mailConfig().from,
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' })
@@ -384,7 +389,7 @@ function certificateRenewed(domain, message) {
assert.strictEqual(typeof message, 'string');
var mailOptions = {
from: config.adminEmail(),
from: platform.mailConfig().from,
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' })
@@ -400,7 +405,7 @@ function unexpectedExit(program, context) {
assert.strictEqual(typeof context, 'string');
var mailOptions = {
from: config.adminEmail(),
from: platform.mailConfig().from,
to: 'admin@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' })
@@ -422,7 +427,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
@@ -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'),
+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')
};
+272
View File
@@ -0,0 +1,272 @@
'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');
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' };
}
if (infra.version === existingInfra.version) {
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,
createDockerNetwork,
startAddons,
removeOldImages,
existingInfra.version === 'none' ? apps.restoreInstalledApps : apps.configureInstalledApps,
loadAddonVars,
mailboxes.setupAliases,
fs.writeFile.bind(fs, paths.INFRA_VERSION_FILE, JSON.stringify(infra))
], callback);
// 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);
}
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(callback) {
// TODO: be nice and stop addons cleanly (example, shutdown commands)
debug('stopping existing containers');
shell.execSync('stopContainersSync', 'docker ps -qa | xargs --no-run-if-empty docker rm -f');
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);
callback();
});
}
function startAddons(callback) {
assert.strictEqual(typeof callback, 'function');
async.series([
startGraphite,
startMysql,
startPostgresql,
startMongodb,
startMail
], 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
};
}
+111 -68
View File
@@ -1,10 +1,7 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
getApp: getApp,
getAppBySubdomain: getAppBySubdomain,
getApps: getApps,
getAppIcon: getAppIcon,
installApp: installApp,
@@ -19,7 +16,9 @@ exports = module.exports = {
stopApp: stopApp,
startApp: startApp,
exec: exec
exec: exec,
cloneApp: cloneApp
};
var apps = require('../apps.js'),
@@ -31,8 +30,12 @@ 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;
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
}
function removeInternalAppFields(app) {
return {
@@ -65,17 +68,6 @@ function getApp(req, res, next) {
});
}
function getAppBySubdomain(req, res, next) {
assert.strictEqual(typeof req.params.subdomain, 'string');
apps.getBySubdomain(req.params.subdomain, function (error, app) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such subdomain'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, removeInternalAppFields(app)));
});
}
function getApps(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
@@ -99,82 +91,74 @@ 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();
debug('Installing app id:%s data:%j', data);
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);
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, 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'));
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, 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.'));
@@ -189,20 +173,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, 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');
@@ -218,16 +232,12 @@ 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');
debug('Uninstalling app id:%s', req.params.id);
apps.uninstall(req.params.id, function (error) {
apps.uninstall(req.params.id, auditSource(req), function (error) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error) return next(new HttpError(500, error));
@@ -269,15 +279,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, 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));
@@ -347,15 +360,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;
@@ -376,8 +411,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(); });
}
});
}
+8 -3
View File
@@ -3,7 +3,7 @@
exports = module.exports = {
get: get,
create: create,
download: download
createDownloadUrl: createDownloadUrl
};
var assert = require('assert'),
@@ -12,6 +12,11 @@ var assert = require('assert'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
function auditSource(req) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
}
function get(req, res, next) {
var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number'));
@@ -30,7 +35,7 @@ function get(req, res, next) {
function create(req, res, next) {
// 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
backups.backup(function (error) {
backups.backup(auditSource(req), function (error) {
if (error && error.reason === BackupsError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
@@ -38,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) {
+52 -17
View File
@@ -1,21 +1,19 @@
/* jslint node:true */
'use strict';
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');
@@ -29,8 +27,8 @@ 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) return next(new HttpError(500, error));
next(new HttpSuccess(201, result));
});
@@ -40,7 +38,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));
});
@@ -49,26 +47,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 }));
});
@@ -79,8 +100,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));
});
}
+26 -12
View File
@@ -1,5 +1,3 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
@@ -10,19 +8,27 @@ exports = module.exports = {
getProgress: getProgress,
getConfig: getConfig,
update: update,
feedback: feedback
feedback: feedback,
checkForUpdates: checkForUpdates
};
var assert = require('assert'),
async = require('async'),
cloudron = require('../cloudron.js'),
config = require('../config.js'),
progress = require('../progress.js'),
mailer = require('../mailer.js'),
CloudronError = cloudron.CloudronError,
config = require('../config.js'),
debug = require('debug')('box:routes/cloudron'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
superagent = require('superagent');
progress = require('../progress.js'),
mailer = require('../mailer.js'),
superagent = require('superagent'),
updateChecker = require('../updatechecker.js');
function auditSource(req) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
}
/**
* Creating an admin user and activate the cloudron.
@@ -50,11 +56,9 @@ function activate(req, res, next) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
debug('activate: username:%s ip:%s', username, ip);
cloudron.activate(username, password, email, displayName, ip, function (error, info) {
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
@@ -119,15 +123,25 @@ function getConfig(req, res, next) {
function update(req, res, next) {
// this only initiates the update, progress can be checked via the progress route
cloudron.updateToLatest(function (error) {
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');
+9 -5
View File
@@ -1,5 +1,3 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
@@ -15,8 +13,13 @@ var developer = require('../developer.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
function auditSource(req) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
}
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'));
});
@@ -25,8 +28,9 @@ function enabled(req, res, next) {
function setEnabled(req, res, next) {
if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled must be boolean'));
developer.setEnabled(req.body.enabled, function (error) {
developer.setEnabled(req.body.enabled, auditSource(req), function (error) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, {}));
});
}
@@ -40,7 +44,7 @@ function login(req, res, next) {
if (error) return next(new HttpError(500, error));
if (!user) return next(new HttpError(401, 'Invalid credentials'));
developer.issueDeveloperToken(user, function (error, result) {
developer.issueDeveloperToken(user, auditSource(req), function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { token: result.token, expiresAt: result.expiresAt }));
+26
View File
@@ -0,0 +1,26 @@
'use strict';
exports = module.exports = {
get: get
};
var eventlog = require('../eventlog.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
function get(req, res, next) {
var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number'));
var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
if (req.query.action && typeof req.query.action !== 'string') return next(new HttpError(400, 'action must be a string'));
if (req.query.search && typeof req.query.search !== 'string') return next(new HttpError(400, 'search must be a string'));
eventlog.getAllPaged(req.query.action || null, req.query.search || null, page, perPage, function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { eventlogs: result }));
});
}
+2 -4
View File
@@ -1,5 +1,3 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
@@ -22,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));
@@ -47,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 }));
+3 -1
View File
@@ -6,11 +6,13 @@ exports = module.exports = {
clients: require('./clients.js'),
cloudron: require('./cloudron.js'),
developer: require('./developer.js'),
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 }));
});
}
+68 -38
View File
@@ -1,33 +1,37 @@
/* jslint node:true */
'use strict';
var assert = require('assert'),
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'),
hat = require('hat'),
HttpError = require('connect-lastmile').HttpError,
middleware = require('../middleware/index.js'),
oauth2orize = require('oauth2orize'),
passport = require('passport'),
querystring = require('querystring'),
util = require('util'),
session = require('connect-ensure-login'),
tokendb = require('../tokendb'),
appdb = require('../appdb'),
url = require('url'),
user = require('../user.js'),
UserError = user.UserError,
hat = require('hat');
util = require('util'),
_ = require('underscore');
function auditSource(req, appId) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
return { authType: 'oauth', ip: ip, appId: appId };
}
// create OAuth 2.0 server
var gServer = oauth2orize.createServer();
// Register serialialization and deserialization functions.
//
// The client id is stored in the session and can thus be retrieved for each
@@ -38,7 +42,7 @@ gServer.serializeClient(function (client, callback) {
});
gServer.deserializeClient(function (id, callback) {
clientdb.get(id, callback);
clients.get(id, callback);
});
@@ -73,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);
@@ -103,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);
@@ -198,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;
}
@@ -303,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, 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));
@@ -354,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));
@@ -396,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
@@ -411,9 +419,12 @@ var authorization = [
// Handle our different types of oauth clients
var type = req.oauth2.client.type;
if (type === clientdb.TYPE_ADMIN) return next();
if (type === clientdb.TYPE_EXTERNAL) return next();
if (type === clientdb.TYPE_SIMPLE_AUTH) return sendError(req, res, 'Unknown OAuth client.');
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.');
}
appdb.get(req.oauth2.client.appId, function (error, appObject) {
if (error) return sendErrorPageOrRedirect(req, res, 'Invalid request. Unknown app for this client_id.');
@@ -422,6 +433,8 @@ var authorization = [
if (error) return sendError(req, res, 'Internal error');
if (!access) return sendErrorPageOrRedirect(req, res, 'No access to this app.');
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource(req, appObject.id), { userId: req.oauth2.user.id });
next();
});
});
@@ -443,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.
//
@@ -462,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();
}
@@ -503,6 +532,7 @@ exports = module.exports = {
accountSetup: accountSetup,
authorization: authorization,
token: token,
validateRequestedScopes: validateRequestedScopes,
scope: scope,
csrf: csrf
};
+40 -33
View File
@@ -1,11 +1,10 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
get: get,
update: update,
changePassword: changePassword
changePassword: changePassword,
setShowTutorial: setShowTutorial
};
var assert = require('assert'),
@@ -13,31 +12,29 @@ var assert = require('assert'),
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;
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
}
function get(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
var result = {};
result.id = req.user.id;
result.tokenType = req.user.tokenType;
groups.isMember(groups.ADMIN_GROUP_ID, req.user.id, function (error, isAdmin) {
if (error) return next(new HttpError(500, error));
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;
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: isAdmin,
displayName: req.user.displayName,
showTutorial: req.user.showTutorial
}));
});
}
function update(req, res, next) {
@@ -47,12 +44,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, 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));
@@ -64,13 +60,24 @@ 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));
next(new HttpSuccess(204));
});
}
function setShowTutorial(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.showTutorial !== 'boolean') return next(new HttpError(400, 'showTutorial must be a boolean.'));
user.setShowTutorial(req.user.id, req.body.showTutorial, function (error) {
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(403, 'Wrong password'));
if (error) return next(new HttpError(500, error));
+29 -4
View File
@@ -1,5 +1,3 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
@@ -18,6 +16,9 @@ exports = module.exports = {
getBackupConfig: getBackupConfig,
setBackupConfig: setBackupConfig,
getTimeZone: getTimeZone,
setTimeZone: setTimeZone,
setCertificate: setCertificate,
setAdminCertificate: setAdminCertificate
};
@@ -45,7 +46,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,8 +59,9 @@ 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));
});
}
@@ -67,10 +69,32 @@ function setCloudronName(req, res, next) {
function getCloudronName(req, res, next) {
settings.getCloudronName(function (error, name) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { name: name }));
});
}
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');
@@ -79,6 +103,7 @@ function setCloudronAvatar(req, res, next) {
settings.setCloudronAvatar(avatar, function (error) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
});
}
+5 -2
View File
@@ -19,7 +19,8 @@ function backup(req, res, next) {
// 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
backups.backup(function (error) {
var auditSource = { userId: null, username: 'sysadmin' };
backups.backup(auditSource, function (error) {
if (error && error.reason === BackupsError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
@@ -31,9 +32,11 @@ function update(req, res, next) {
debug('triggering update');
// this only initiates the update, progress can be checked via the progress route
cloudron.updateToLatest(function (error) {
var auditSource = { userId: null, username: 'sysadmin' };
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, {}));
File diff suppressed because it is too large Load Diff
+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) {
+51 -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'),
@@ -174,7 +174,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 +291,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 +395,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 +535,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 +588,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 +641,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 })
+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();
});
});
});
});
+152
View File
@@ -0,0 +1,152 @@
/* 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'),
nock = require('nock'),
superagent = require('superagent'),
server = require('../../server.js'),
tokendb = require('../../tokendb.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null;
var USER_1_ID = null, token_1;
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();
});
},
function (callback) {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: 'nonadmin', email: 'notadmin@server.test', invite: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
USER_1_ID = res.body.id;
callback(null);
});
},
function (callback) {
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, USER_1_ID, 'test-client-id', Date.now() + 100000, '*', callback);
}
], done);
}
function cleanup(done) {
database._clear(function (error) {
expect(!error).to.be.ok();
server.stop(done);
});
}
describe('Eventlog API', function () {
before(setup);
after(cleanup);
describe('get', function () {
it('fails due to wrong token', function (done) {
superagent.get(SERVER_URL + '/api/v1/eventlog')
.query({ access_token: token.toUpperCase() })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails for non-admin', function (done) {
superagent.get(SERVER_URL + '/api/v1/eventlog')
.query({ access_token: token_1, page: 1, per_page: 10 })
.end(function (error, result) {
expect(result.statusCode).to.equal(403);
done();
});
});
it('succeeds for admin', function (done) {
superagent.get(SERVER_URL + '/api/v1/eventlog')
.query({ access_token: token, page: 1, per_page: 10 })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.eventlogs.length >= 2).to.be.ok(); // activate, user.add
done();
});
});
it('succeeds with action', function (done) {
superagent.get(SERVER_URL + '/api/v1/eventlog')
.query({ access_token: token, page: 1, per_page: 10, action: 'cloudron.activate' })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.eventlogs.length).to.equal(1);
done();
});
});
it('succeeds with search', function (done) {
superagent.get(SERVER_URL + '/api/v1/eventlog')
.query({ access_token: token, page: 1, per_page: 10, search: EMAIL })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.eventlogs.length).to.equal(1);
done();
});
});
it('succeeds with search', function (done) {
superagent.get(SERVER_URL + '/api/v1/eventlog')
.query({ access_token: token, page: 1, per_page: 10, search: EMAIL, action: 'cloudron.activate' })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.eventlogs.length).to.equal(0);
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();
});
});
});
+20 -17
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'),
@@ -145,7 +146,8 @@ describe('OAuth2', function () {
createdAt: (new Date()).toUTCString(),
modifiedAt: (new Date()).toUTCString(),
resetToken: hat(256),
displayName: ''
displayName: '',
showTutorial: false
};
var APP_0 = {
@@ -196,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'
@@ -206,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'
@@ -216,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'
@@ -226,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'
@@ -236,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'
@@ -246,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'
@@ -256,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'
@@ -266,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'
@@ -276,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'
@@ -286,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'
@@ -312,12 +314,12 @@ 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, function (error, userObject) {
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();
// update the global objects to reflect the new user id
@@ -1301,7 +1303,8 @@ describe('Password', function () {
createdAt: (new Date()).toUTCString(),
modifiedAt: (new Date()).toUTCString(),
resetToken: hat(256),
displayName: ''
displayName: '',
showTutorial: false
};
// make csrf always succeed for testing
+64 -35
View File
@@ -10,28 +10,20 @@ var config = require('../../config.js'),
database = require('../../database.js'),
tokendb = require('../../tokendb.js'),
expect = require('expect.js'),
groups = require('../../groups.js'),
mailer = require('../../mailer.js'),
superagent = require('superagent'),
nock = require('nock'),
server = require('../../server.js'),
userdb = require('../../userdb.js');
server = require('../../server.js');
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';
describe('Profile API', function () {
this.timeout(5000);
var user_0, user_1, user_2, user_3 = null;
var user_0 = null;
var token_0;
var token_1 = tokendb.generateToken();
var token_2 = tokendb.generateToken();
var token_3;
function setup(done) {
server.start(function (error) {
@@ -74,15 +66,6 @@ describe('Profile API', function () {
});
}
function checkMails(number, done) {
// mails are enqueued async
setTimeout(function () {
expect(mailer._getMailQueue().length).to.equal(number);
mailer._clearMailQueue();
done();
}, 500);
}
describe('get profile', function () {
before(setup);
after(cleanup);
@@ -117,6 +100,10 @@ describe('Profile API', function () {
expect(result.body.username).to.equal(USERNAME_0.toLowerCase());
expect(result.body.email).to.equal(EMAIL_0.toLowerCase());
expect(result.body.admin).to.be.ok();
expect(result.body.showTutorial).to.be.ok();
expect(result.body.displayName).to.be.a('string');
expect(result.body.password).to.not.be.ok();
expect(result.body.salt).to.not.be.ok();
user_0 = result.body;
@@ -128,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) {
@@ -152,6 +139,7 @@ describe('Profile API', function () {
expect(result.body.username).to.equal(USERNAME_0.toLowerCase());
expect(result.body.email).to.equal(EMAIL_0.toLowerCase());
expect(result.body.admin).to.be.ok();
expect(result.body.showTutorial).to.be.ok();
expect(result.body.displayName).to.be.a('string');
expect(result.body.password).to.not.be.ok();
expect(result.body.salt).to.not.be.ok();
@@ -165,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);
@@ -174,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) {
@@ -184,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) {
@@ -194,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) {
@@ -215,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) {
@@ -240,8 +228,8 @@ describe('Profile API', function () {
before(setup);
after(cleanup);
it('change password fails due to missing current password', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile/password')
it('fails due to missing current password', function (done) {
superagent.post(SERVER_URL + '/api/v1/profile/password')
.query({ access_token: token_0 })
.send({ newPassword: 'some wrong password' })
.end(function (err, res) {
@@ -250,8 +238,8 @@ describe('Profile API', function () {
});
});
it('change password fails due to missing new password', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile/password')
it('fails due to missing new password', function (done) {
superagent.post(SERVER_URL + '/api/v1/profile/password')
.query({ access_token: token_0 })
.send({ password: PASSWORD })
.end(function (err, res) {
@@ -260,8 +248,8 @@ describe('Profile API', function () {
});
});
it('change password fails due to wrong password', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile/password')
it('fails due to wrong password', function (done) {
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) {
@@ -270,8 +258,8 @@ describe('Profile API', function () {
});
});
it('change password fails due to invalid password', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile/password')
it('fails due to invalid password', function (done) {
superagent.post(SERVER_URL + '/api/v1/profile/password')
.query({ access_token: token_0 })
.send({ password: PASSWORD, newPassword: 'five' })
.end(function (err, res) {
@@ -280,8 +268,8 @@ describe('Profile API', function () {
});
});
it('change password succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/profile/password')
it('succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/profile/password')
.query({ access_token: token_0 })
.send({ password: PASSWORD, newPassword: 'MOre#$%34' })
.end(function (err, res) {
@@ -290,4 +278,45 @@ describe('Profile API', function () {
});
});
});
describe('showTutorial change', function () {
before(setup);
after(cleanup);
it('fails due to missing showTutorial', function (done) {
superagent.post(SERVER_URL + '/api/v1/profile/tutorial')
.query({ access_token: token_0 })
.send({})
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('fails due to wrong showTutorial type', function (done) {
superagent.post(SERVER_URL + '/api/v1/profile/tutorial')
.query({ access_token: token_0 })
.send({ showTutorial: 'true' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/profile/tutorial')
.query({ access_token: token_0 })
.send({ showTutorial: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
superagent.get(SERVER_URL + '/api/v1/profile/').query({ access_token: token_0 }).end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.showTutorial).to.not.be.ok();
done();
});
});
});
});
});
+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();
});
});
});
+13 -1
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);
}
@@ -360,5 +360,17 @@ describe('Settings API', function () {
done();
});
});
describe('time_zone', function () {
it('succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/settings/time_zone')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.timeZone).to.be('America/Los_Angeles');
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);
});
-87
View File
@@ -1,87 +0,0 @@
#!/bin/bash
set -eu -o pipefail
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
source ${SOURCE_DIR}/setup/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"
if which boot2docker >/dev/null 2>&1; then
boot2docker ssh "sudo rm -rf /tmp/postgresql_vars.sh"
boot2docker ssh "echo \"${postgresql_vars}\" > /tmp/postgresql_vars.sh"
else
rm -rf /tmp/postgresql_vars.sh
echo "${postgresql_vars}" > /tmp/postgresql_vars.sh
fi
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"
if which boot2docker >/dev/null 2>&1; then
boot2docker ssh "sudo rm -rf /tmp/mysql_vars.sh"
boot2docker ssh "echo \"${mysql_vars}\" > /tmp/mysql_vars.sh"
else
rm -rf /tmp/mysql_vars.sh
echo "${mysql_vars}" > /tmp/mysql_vars.sh
fi
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}"
if which boot2docker >/dev/null 2>&1; then
boot2docker ssh "sudo rm -rf /tmp/mongodb_vars.sh"
boot2docker ssh "echo \"${mongodb_vars}\" > /tmp/mongodb_vars.sh"
else
rm -rf /tmp/mongodb_vars.sh
echo "${mongodb_vars}" > /tmp/mongodb_vars.sh
fi
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) {
+38 -42
View File
@@ -1,5 +1,3 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
@@ -15,13 +13,21 @@ 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;
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
}
function create(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
@@ -37,12 +43,9 @@ function create(req, res, next) {
var username = req.body.username || '';
var displayName = req.body.displayName || '';
user.create(username, password, email, displayName, { 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'));
user.create(username, password, email, displayName, auditSource(req), { invitor: req.user, sendInvite: sendInvite }, function (error, user) {
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 = {
@@ -51,6 +54,7 @@ function create(req, res, next) {
displayName: user.displayName,
email: user.email,
admin: user.admin,
groupIds: [ ],
resetToken: user.resetToken
};
@@ -65,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, 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 }));
});
}
@@ -102,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
}));
});
}
@@ -126,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, 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}"
+11 -1
View File
@@ -13,6 +13,16 @@ if [[ $# == 1 && "$1" == "--check" ]]; then
fi
if [[ "${BOX_ENV}" == "cloudron" ]]; then
/etc/init.d/collectd restart
# when restoring the cloudron with many apps, the apptasks rush in to restart
# collectd which makes systemd/collectd very unhappy and puts the collectd in
# inactive state
for i in {1..10}; do
echo "Restarting collectd"
if systemctl restart collectd; then
exit 0
fi
echo "Failed to reload collectd. Maybe some other apptask is restarting it"
sleep $((RANDOM%30))
done
fi
+57 -35
View File
@@ -14,12 +14,14 @@ var assert = require('assert'),
cron = require('./cron.js'),
config = require('./config.js'),
database = require('./database.js'),
eventlog = require('./eventlog.js'),
express = require('express'),
http = require('http'),
mailer = require('./mailer.js'),
middleware = require('./middleware'),
passport = require('passport'),
path = require('path'),
platform = require('./platform.js'),
routes = require('./routes/index.js'),
taskmanager = require('./taskmanager.js');
@@ -30,7 +32,7 @@ function initializeExpressSync() {
var app = express();
var httpServer = http.createServer(app);
var QUERY_LIMIT = '10mb', // max size for json and urlencoded queries
var QUERY_LIMIT = '1mb', // max size for json and urlencoded queries (see also client_max_body_size in nginx)
FIELD_LIMIT = 2 * 1024 * 1024; // max fields that can appear in multipart
var REQUEST_TIMEOUT = 10000; // timeout for all requests (see also setTimeout on the httpServer)
@@ -41,6 +43,7 @@ function initializeExpressSync() {
app.set('views', path.join(__dirname, 'oauth2views'));
app.set('view options', { layout: true, debug: false });
app.set('view engine', 'ejs');
app.set('json spaces', 2); // pretty json
if (process.env.BOX_ENV !== 'test') app.use(middleware.morgan('Box :method :url :status :response-time ms - :res[content-length]', { immediate: false }));
@@ -60,13 +63,13 @@ function initializeExpressSync() {
.use(middleware.lastMile());
// NOTE: these limits have to be in sync with nginx limits
var FILE_SIZE_LIMIT = '1mb', // max file size that can be uploaded
var FILE_SIZE_LIMIT = '1mb', // max file size that can be uploaded (see also client_max_body_size in nginx)
FILE_TIMEOUT = 60 * 1000; // increased timeout for file uploads (1 min)
var multipart = middleware.multipart({ maxFieldsSize: FIELD_LIMIT, limit: FILE_SIZE_LIMIT, timeout: FILE_TIMEOUT });
// scope middleware implicitly also adds bearer token verification
var rootScope = routes.oauth2.scope(clients.SCOPE_ROOT);
var cloudronScope = routes.oauth2.scope(clients.SCOPE_CLOUDRON);
var profileScope = routes.oauth2.scope(clients.SCOPE_PROFILE);
var usersScope = routes.oauth2.scope(clients.SCOPE_USERS);
var appsScope = routes.oauth2.scope(clients.SCOPE_APPS);
@@ -88,27 +91,29 @@ function initializeExpressSync() {
router.post('/api/v1/developer/login', routes.developer.enabled, routes.developer.login);
router.get ('/api/v1/developer/apps', developerScope, routes.developer.enabled, routes.developer.apps);
// private routes
router.get ('/api/v1/cloudron/config', rootScope, routes.cloudron.getConfig);
router.post('/api/v1/cloudron/update', rootScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.cloudron.update);
router.post('/api/v1/cloudron/reboot', rootScope, routes.cloudron.reboot);
router.get ('/api/v1/cloudron/graphs', rootScope, routes.graphs.getGraphs);
// cloudron routes
router.get ('/api/v1/cloudron/config', cloudronScope, routes.cloudron.getConfig);
router.post('/api/v1/cloudron/update', cloudronScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.cloudron.update);
router.post('/api/v1/cloudron/check_for_updates', cloudronScope, routes.user.requireAdmin, routes.cloudron.checkForUpdates);
router.post('/api/v1/cloudron/reboot', cloudronScope, routes.cloudron.reboot);
router.get ('/api/v1/cloudron/graphs', cloudronScope, routes.graphs.getGraphs);
// feedback
router.post('/api/v1/cloudron/feedback', usersScope, routes.cloudron.feedback);
// profile api, working off the user behind the provided token
router.get ('/api/v1/profile', profileScope, routes.profile.get);
router.put ('/api/v1/profile', profileScope, routes.profile.update);
router.put ('/api/v1/profile/password', profileScope, routes.user.verifyPassword, routes.profile.changePassword);
router.post('/api/v1/profile', profileScope, routes.profile.update);
router.post('/api/v1/profile/password', profileScope, routes.user.verifyPassword, routes.profile.changePassword);
router.post('/api/v1/profile/tutorial', profileScope, routes.profile.setShowTutorial);
// user routes only for admins
// user routes
router.get ('/api/v1/users', usersScope, routes.user.requireAdmin, routes.user.list);
router.post('/api/v1/users', usersScope, routes.user.requireAdmin, routes.user.create);
router.get ('/api/v1/users/:userId', usersScope, routes.user.requireAdmin, routes.user.get);
router.del ('/api/v1/users/:userId', usersScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.user.remove);
router.put ('/api/v1/users/:userId', usersScope, routes.user.requireAdmin, routes.user.update);
router.put ('/api/v1/users/:userId/set_groups', usersScope, routes.user.requireAdmin, routes.user.setGroups);
router.post('/api/v1/users/:userId', usersScope, routes.user.requireAdmin, routes.user.update);
router.put ('/api/v1/users/:userId/groups', usersScope, routes.user.requireAdmin, routes.user.setGroups);
router.post('/api/v1/users/:userId/invite', usersScope, routes.user.requireAdmin, routes.user.sendInvite);
// Group management
@@ -117,6 +122,14 @@ function initializeExpressSync() {
router.get ('/api/v1/groups/:groupId', usersScope, routes.user.requireAdmin, routes.groups.get);
router.del ('/api/v1/groups/:groupId', usersScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.groups.remove);
// Mailbox management
router.get ('/api/v1/mailboxes', usersScope, routes.user.requireAdmin, routes.mailboxes.list);
router.post('/api/v1/mailboxes', usersScope, routes.user.requireAdmin, routes.mailboxes.create);
router.get ('/api/v1/mailboxes/:mailboxId', usersScope, routes.user.requireAdmin, routes.mailboxes.get);
router.del ('/api/v1/mailboxes/:mailboxId', usersScope, routes.user.requireAdmin, routes.mailboxes.remove);
router.put ('/api/v1/mailboxes/:mailboxId/aliases', usersScope, routes.user.requireAdmin, routes.mailboxes.setAliases);
router.get ('/api/v1/mailboxes/:mailboxId/aliases', usersScope, routes.user.requireAdmin, routes.mailboxes.getAliases);
// form based login routes used by oauth2 frame
router.get ('/api/v1/session/login', csrf, routes.oauth2.loginForm);
router.post('/api/v1/session/login', csrf, routes.oauth2.login);
@@ -133,13 +146,15 @@ function initializeExpressSync() {
// oauth2 routes
router.get ('/api/v1/oauth/dialog/authorize', routes.oauth2.authorization);
router.post('/api/v1/oauth/token', routes.oauth2.token);
router.get ('/api/v1/oauth/clients', settingsScope, routes.clients.getAllByUserId);
router.get ('/api/v1/oauth/clients', settingsScope, routes.clients.getAll);
router.post('/api/v1/oauth/clients', routes.developer.enabled, settingsScope, routes.clients.add);
router.get ('/api/v1/oauth/clients/:clientId', routes.developer.enabled, settingsScope, routes.clients.get);
router.post('/api/v1/oauth/clients/:clientId', routes.developer.enabled, settingsScope, routes.clients.add);
router.del ('/api/v1/oauth/clients/:clientId', routes.developer.enabled, settingsScope, routes.clients.del);
router.get ('/api/v1/oauth/clients/:clientId/tokens', settingsScope, routes.clients.getClientTokens);
router.post('/api/v1/oauth/clients/:clientId/tokens', routes.developer.enabled, settingsScope, routes.clients.addClientToken);
router.del ('/api/v1/oauth/clients/:clientId/tokens', settingsScope, routes.clients.delClientTokens);
router.del ('/api/v1/oauth/clients/:clientId/tokens/:tokenId', settingsScope, routes.clients.delToken);
// app routes
router.get ('/api/v1/apps', appsScope, routes.apps.getApps);
@@ -158,30 +173,34 @@ function initializeExpressSync() {
router.get ('/api/v1/apps/:id/logstream', appsScope, routes.user.requireAdmin, routes.apps.getLogStream);
router.get ('/api/v1/apps/:id/logs', appsScope, routes.user.requireAdmin, routes.apps.getLogs);
router.get ('/api/v1/apps/:id/exec', routes.developer.enabled, appsScope, routes.user.requireAdmin, routes.apps.exec);
router.post('/api/v1/apps/:id/clone', appsScope, routes.user.requireAdmin, routes.apps.cloneApp);
// subdomain routes
router.get ('/api/v1/subdomains/:subdomain', routes.apps.getAppBySubdomain);
// settings routes (these are for the settings tab - avatar & name have public routes for normal users. see above)
router.get ('/api/v1/settings/autoupdate_pattern', settingsScope, routes.user.requireAdmin, routes.settings.getAutoupdatePattern);
router.post('/api/v1/settings/autoupdate_pattern', settingsScope, routes.user.requireAdmin, routes.settings.setAutoupdatePattern);
router.get ('/api/v1/settings/cloudron_name', settingsScope, routes.user.requireAdmin, routes.settings.getCloudronName);
router.post('/api/v1/settings/cloudron_name', settingsScope, routes.user.requireAdmin, routes.settings.setCloudronName);
router.get ('/api/v1/settings/cloudron_avatar', settingsScope, routes.user.requireAdmin, routes.settings.getCloudronAvatar);
router.post('/api/v1/settings/cloudron_avatar', settingsScope, routes.user.requireAdmin, multipart, routes.settings.setCloudronAvatar);
router.get ('/api/v1/settings/dns_config', settingsScope, routes.user.requireAdmin, routes.settings.getDnsConfig);
router.post('/api/v1/settings/dns_config', settingsScope, routes.user.requireAdmin, routes.settings.setDnsConfig);
router.get ('/api/v1/settings/backup_config', settingsScope, routes.user.requireAdmin, routes.settings.getBackupConfig);
router.post('/api/v1/settings/backup_config', settingsScope, routes.user.requireAdmin, routes.settings.setBackupConfig);
router.post('/api/v1/settings/certificate', settingsScope, routes.user.requireAdmin, routes.settings.setCertificate);
router.post('/api/v1/settings/admin_certificate', settingsScope, routes.user.requireAdmin, routes.settings.setAdminCertificate);
router.get ('/api/v1/settings/time_zone', settingsScope, routes.user.requireAdmin, routes.settings.getTimeZone);
router.post('/api/v1/settings/time_zone', settingsScope, routes.user.requireAdmin, routes.settings.setTimeZone);
// settings routes
router.get ('/api/v1/settings/autoupdate_pattern', settingsScope, routes.settings.getAutoupdatePattern);
router.post('/api/v1/settings/autoupdate_pattern', settingsScope, routes.settings.setAutoupdatePattern);
router.get ('/api/v1/settings/cloudron_name', settingsScope, routes.settings.getCloudronName);
router.post('/api/v1/settings/cloudron_name', settingsScope, routes.settings.setCloudronName);
router.get ('/api/v1/settings/cloudron_avatar', settingsScope, routes.settings.getCloudronAvatar);
router.post('/api/v1/settings/cloudron_avatar', settingsScope, multipart, routes.settings.setCloudronAvatar);
router.get ('/api/v1/settings/dns_config', settingsScope, routes.settings.getDnsConfig);
router.post('/api/v1/settings/dns_config', settingsScope, routes.settings.setDnsConfig);
router.get ('/api/v1/settings/backup_config', settingsScope, routes.settings.getBackupConfig);
router.post('/api/v1/settings/backup_config', settingsScope, routes.settings.setBackupConfig);
router.post('/api/v1/settings/certificate', settingsScope, routes.settings.setCertificate);
router.post('/api/v1/settings/admin_certificate', settingsScope, routes.settings.setAdminCertificate);
// eventlog route
router.get('/api/v1/eventlog', settingsScope, routes.user.requireAdmin, routes.eventlog.get);
// backup routes
router.get ('/api/v1/backups', settingsScope, routes.backups.get);
router.post('/api/v1/backups', settingsScope, routes.backups.create);
router.get ('/api/v1/backups/:backupId', appsScope, routes.user.requireAdmin, routes.backups.download);
router.get ('/api/v1/backups', settingsScope, routes.user.requireAdmin, routes.backups.get);
router.post('/api/v1/backups', settingsScope, routes.user.requireAdmin, routes.backups.create);
router.post('/api/v1/backups/:backupId/download_url', appsScope, routes.user.requireAdmin, routes.backups.createDownloadUrl);
// disable server timeout. we use the timeout middleware to handle timeouts on a route level
// disable server socket "idle" timeout. we use the timeout middleware to handle timeouts on a route level
// we rely on nginx for timeouts on the TCP level (see client_header_timeout)
httpServer.setTimeout(0);
// upgrade handler
@@ -215,7 +234,7 @@ function initializeSysadminExpressSync() {
var app = express();
var httpServer = http.createServer(app);
var QUERY_LIMIT = '10mb'; // max size for json and urlencoded queries
var QUERY_LIMIT = '1mb'; // max size for json and urlencoded queries
var REQUEST_TIMEOUT = 10000; // timeout for all requests
var json = middleware.json({ strict: true, limit: QUERY_LIMIT }), // application/json
@@ -253,11 +272,13 @@ function start(callback) {
database.initialize,
cloudron.initialize, // keep this here because it reads activation state that others depend on
certificates.installAdminCertificate, // keep this before cron to block heartbeats until cert is ready
platform.initialize,
taskmanager.initialize,
mailer.initialize,
cron.initialize,
gHttpServer.listen.bind(gHttpServer, config.get('port'), '127.0.0.1'),
gSysadminHttpServer.listen.bind(gSysadminHttpServer, config.get('sysadminPort'), '127.0.0.1')
gSysadminHttpServer.listen.bind(gSysadminHttpServer, config.get('sysadminPort'), '127.0.0.1'),
eventlog.add.bind(null, eventlog.ACTION_START, { userId: null, username: 'boot' }, { version: config.version() })
], callback);
}
@@ -270,6 +291,7 @@ function stop(callback) {
auth.uninitialize,
cloudron.uninitialize,
taskmanager.uninitialize,
platform.uninitialize,
cron.uninitialize,
mailer.uninitialize,
database.uninitialize,
+9 -3
View File
@@ -51,6 +51,7 @@ var assert = require('assert'),
config = require('./config.js'),
CronJob = require('cron').CronJob,
DatabaseError = require('./databaseerror.js'),
moment = require('moment-timezone'),
paths = require('./paths.js'),
safe = require('safetydance'),
settingsdb = require('./settingsdb.js'),
@@ -62,7 +63,7 @@ var gDefaults = (function () {
result[exports.AUTOUPDATE_PATTERN_KEY] = '00 00 1,3,5,23 * * *';
result[exports.TIME_ZONE_KEY] = 'America/Los_Angeles';
result[exports.CLOUDRON_NAME_KEY] = 'Cloudron';
result[exports.DEVELOPER_MODE_KEY] = false;
result[exports.DEVELOPER_MODE_KEY] = true;
result[exports.DNS_CONFIG_KEY] = { };
result[exports.BACKUP_CONFIG_KEY] = { };
result[exports.TLS_CONFIG_KEY] = { provider: 'caas' };
@@ -132,6 +133,8 @@ function setTimeZone(tz, callback) {
assert.strictEqual(typeof tz, 'string');
assert.strictEqual(typeof callback, 'function');
if (moment.tz.names().indexOf(tz) === -1) return callback(new SettingsError(SettingsError.BAD_FIELD, 'Bad timeZone'));
settingsdb.set(exports.TIME_ZONE_KEY, tz, function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
@@ -145,7 +148,7 @@ function getTimeZone(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.TIME_ZONE_KEY, function (error, tz) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.AUTOUPDATE_PATTERN_KEY]);
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.TIME_ZONE_KEY]);
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
callback(null, tz);
@@ -166,7 +169,10 @@ function setCloudronName(name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
if (!name) return callback(new SettingsError(SettingsError.BAD_FIELD));
if (!name) return callback(new SettingsError(SettingsError.BAD_FIELD, 'name is empty'));
// some arbitrary restrictions (for sake of ui layout)
if (name.length > 32) return callback(new SettingsError(SettingsError.BAD_FIELD, 'name cannot exceed 32 characters'));
settingsdb.set(exports.CLOUDRON_NAME_KEY, name, function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
+17 -2
View File
@@ -2,7 +2,8 @@
exports = module.exports = {
sudo: sudo,
exec: exec
exec: exec,
execSync: execSync
};
var assert = require('assert'),
@@ -13,6 +14,20 @@ var assert = require('assert'),
var SUDO = '/usr/bin/sudo';
function execSync(tag, cmd, callback) {
assert.strictEqual(typeof tag, 'string');
assert.strictEqual(typeof cmd, 'string');
debug(cmd);
try {
child_process.execSync(cmd, { stdio: 'inherit' });
} catch (e) {
if (callback) return callback(e);
throw e;
}
if (callback) callback();
}
function exec(tag, file, args, callback) {
assert.strictEqual(typeof tag, 'string');
assert.strictEqual(typeof file, 'string');
@@ -35,7 +50,7 @@ function exec(tag, file, args, callback) {
cp.on('exit', function (code, signal) {
if (code || signal) debug(tag + ' code: %s, signal: %s', code, signal);
callback(code === 0 ? null : new Error(util.format('Exited with error %s signal %s', code, signal)));
callback(code === 0 ? null : new Error(util.format(tag + ' exited with error %s signal %s', code, signal)));
});
cp.on('error', function (error) {
+6 -4
View File
@@ -8,12 +8,12 @@ exports = module.exports = {
var apps = require('./apps.js'),
AppsError = apps.AppsError,
assert = require('assert'),
clientdb = require('./clientdb.js'),
clients = require('./clients.js'),
ClientsError = clients.ClientsError,
config = require('./config.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:src/simpleauth'),
eventlog = require('./eventlog.js'),
express = require('express'),
http = require('http'),
HttpError = require('connect-lastmile').HttpError,
@@ -37,7 +37,7 @@ function loginLogic(clientId, username, password, callback) {
if (error) return callback(error);
// only allow simple auth clients
if (clientObject.type !== clientdb.TYPE_SIMPLE_AUTH) return callback(new ClientsError(ClientsError.INVALID_CLIENT));
if (clientObject.type !== clients.TYPE_SIMPLE_AUTH) return callback(new ClientsError(ClientsError.INVALID_CLIENT));
var authFunction = (username.indexOf('@') === -1) ? user.verifyWithUsername : user.verifyWithEmail;
authFunction(username, password, function (error, userObject) {
@@ -53,7 +53,7 @@ function loginLogic(clientId, username, password, callback) {
var accessToken = tokendb.generateToken();
var expires = Date.now() + 24 * 60 * 60 * 1000; // 1 day
tokendb.add(accessToken, tokendb.PREFIX_USER + userObject.id, clientId, expires, clientObject.scope, function (error) {
tokendb.add(accessToken, userObject.id, clientId, expires, clientObject.scope, function (error) {
if (error) return callback(error);
debug('login: new access token for client %s and user %s: %s', clientId, username, accessToken);
@@ -86,7 +86,7 @@ function login(req, res, next) {
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'password is required'));
loginLogic(req.body.clientId, req.body.username, req.body.password, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new HttpError(401, 'Unknown client'));
if (error && error.reason === ClientsError.NOT_FOUND) return next(new HttpError(401, 'Unknown client'));
if (error && error.reason === ClientsError.INVALID_CLIENT) return next(new HttpError(401, 'Unkown client'));
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(401, 'Forbidden'));
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(401, 'Unkown app'));
@@ -94,6 +94,8 @@ function login(req, res, next) {
if (error && error.reason === AppsError.ACCESS_DENIED) return next(new HttpError(401, 'Forbidden'));
if (error) return next(new HttpError(500, error));
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'simpleauth', clientId: req.body.clientId }, { userId: result.user.id });
var tmp = {
accessToken: result.accessToken,
user: {
+1 -1
View File
@@ -43,7 +43,7 @@ SubdomainError.NOT_FOUND = 'No such domain';
SubdomainError.EXTERNAL_ERROR = 'External error';
SubdomainError.STILL_BUSY = 'Still busy';
SubdomainError.MISSING_CREDENTIALS = 'Missing credentials';
SubdomainError.INTERNAL_ERROR = 'Missing credentials';
SubdomainError.INTERNAL_ERROR = 'Internal error';
SubdomainError.ACCESS_DENIED = 'Access denied';
// choose which subdomain backend we use for test purpose we use route53
+22 -13
View File
@@ -19,13 +19,13 @@ var appdb = require('./appdb.js'),
cloudron = require('./cloudron.js'),
debug = require('debug')('box:taskmanager'),
locker = require('./locker.js'),
platform = require('./platform.js'),
sendFailureLogs = require('./logcollector.js').sendFailureLogs,
util = require('util'),
_ = require('underscore');
var gActiveTasks = { };
var gPendingTasks = [ ];
var gPlatformReady = false; // PaaS (addons) up and running
var TASK_CONCURRENCY = 5;
var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
@@ -35,17 +35,12 @@ function initialize(callback) {
locker.on('unlocked', startNextTask);
if (cloudron.isConfiguredSync()) {
resumeTasks();
if (platform.isReadySync()) {
platformReady();
} else {
cloudron.events.on(cloudron.EVENT_CONFIGURED, resumeTasks);
platform.events.on(platform.EVENT_READY, platformReady);
}
setTimeout(function () {
gPlatformReady = true;
resumeTasks();
}, 30000); // wait 30 seconds to signal platform ready
callback();
}
@@ -55,6 +50,8 @@ function uninitialize(callback) {
gPendingTasks = [ ]; // clear this first, otherwise stopAppTask will resume them
cloudron.events.removeListener(cloudron.EVENT_CONFIGURED, resumeTasks);
platform.events.removeListener(platform.EVENT_READY, platformReady);
locker.removeListener('unlocked', startNextTask);
async.eachSeries(Object.keys(gActiveTasks), stopAppTask, callback);
@@ -79,10 +76,22 @@ function waitForPendingTasks(callback) {
checkTasks();
}
// resume app installs and uninstalls
function platformReady() {
if (cloudron.isConfiguredSync()) {
debug('platformReady: configured, resuming tasks');
resumeTasks();
} else {
debug('platformReady: not configured yet. waiting for configured event');
cloudron.events.on(cloudron.EVENT_CONFIGURED, resumeTasks);
}
}
// resume app tasks when platform is ready or after a crash
function resumeTasks(callback) {
callback = callback || NOOP_CALLBACK;
debug('resuming tasks');
appdb.getAll(function (error, apps) {
if (error) return callback(error);
@@ -92,7 +101,7 @@ function resumeTasks(callback) {
if (app.installationState === appdb.ISTATE_ERROR) return;
debug('Creating process for %s (%s) with state %s', app.location, app.id, app.installationState);
startAppTask(app.id, NOOP_CALLBACK);
restartAppTask(app.id, NOOP_CALLBACK); // restart because the auto-installer could have queued up tasks already
});
callback(null);
@@ -115,7 +124,7 @@ function startAppTask(appId, callback) {
return callback(new Error(util.format('Task for %s is already active', appId)));
}
if (!gPlatformReady) {
if (!platform.isReadySync()) {
debug('Platform not ready yet, queueing task for %s', appId);
gPendingTasks.push(appId);
return callback();
@@ -164,7 +173,7 @@ function stopAppTask(appId, callback) {
if (gActiveTasks[appId]) {
debug('stopAppTask : Killing existing task of %s with pid %s', appId, gActiveTasks[appId].pid);
gActiveTasks[appId].once('exit', function () { callback(); });
gActiveTasks[appId].kill('SIGKILL'); // this will end up calling the 'exit' handler
gActiveTasks[appId].kill('SIGTERM'); // this will end up calling the 'exit' handler
return;
}
+63 -24
View File
@@ -28,7 +28,8 @@ describe('Apps', function () {
createdAt: 'sometime back',
modifiedAt: 'now',
resetToken: hat(256),
displayName: ''
displayName: '',
showTutorial: false
};
var USER_0 = {
@@ -40,7 +41,8 @@ describe('Apps', function () {
createdAt: 'sometime back',
modifiedAt: 'now',
resetToken: hat(256),
displayName: ''
displayName: '',
showTutorial: false
};
var USER_1 = {
@@ -52,7 +54,8 @@ describe('Apps', function () {
createdAt: 'sometime back',
modifiedAt: 'now',
resetToken: hat(256),
displayName: ''
displayName: '',
showTutorial: false
};
var GROUP_0 = 'somegroup';
@@ -113,9 +116,9 @@ describe('Apps', function () {
groups.create.bind(null, GROUP_1),
groups.addMember.bind(null, groups.ADMIN_GROUP_ID, ADMIN_0.id),
groups.addMember.bind(null, GROUP_0, USER_1.id),
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, null),
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, null),
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, null)
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)
], done);
});
@@ -201,24 +204,6 @@ describe('Apps', function () {
});
});
it('cannot getBySubdomain', function (done) {
apps.getBySubdomain('moang', function (error, app) {
expect(error).to.be.ok();
expect(error.reason).to.be(AppsError.NOT_FOUND);
done();
});
});
it('can getBySubdomain', function (done) {
apps.getBySubdomain(APP_0.location, function (error, app) {
expect(error).to.be(null);
expect(app).to.be.ok();
expect(app.iconUrl).to.eql(null);
expect(app.fqdn).to.eql(APP_0.location + '-' + config.fqdn());
done();
});
});
it('can getAll', function (done) {
apps.getAll(function (error, apps) {
expect(error).to.be(null);
@@ -341,4 +326,58 @@ describe('Apps', function () {
});
});
});
describe('configureInstalledApps', function () {
before(function (done) {
async.series([
appdb.update.bind(null, APP_0.id, { installationState: appdb.ISTATE_INSTALLED }),
appdb.update.bind(null, APP_1.id, { installationState: appdb.ISTATE_ERROR }),
appdb.update.bind(null, APP_2.id, { installationState: appdb.ISTATE_INSTALLED })
], done);
});
it('can mark apps for reconfigure', function (done) {
apps.configureInstalledApps(function (error) {
expect(error).to.be(null);
apps.getAll(function (error, apps) {
expect(apps[0].installationState).to.be(appdb.ISTATE_PENDING_CONFIGURE);
expect(apps[0].oldConfig).to.be(null);
expect(apps[1].installationState).to.be(appdb.ISTATE_PENDING_CONFIGURE); // erorred app can be reconfigured after restore
expect(apps[1].oldConfig).to.be(null);
expect(apps[2].installationState).to.be(appdb.ISTATE_PENDING_CONFIGURE);
expect(apps[2].oldConfig).to.be(null);
done();
});
});
});
});
describe('restoreInstalledApps', function () {
before(function (done) {
async.series([
appdb.update.bind(null, APP_0.id, { installationState: appdb.ISTATE_INSTALLED }),
appdb.update.bind(null, APP_1.id, { installationState: appdb.ISTATE_ERROR }),
appdb.update.bind(null, APP_2.id, { installationState: appdb.ISTATE_INSTALLED })
], done);
});
it('can mark apps for reconfigure', function (done) {
apps.restoreInstalledApps(function (error) {
expect(error).to.be(null);
apps.getAll(function (error, apps) {
expect(apps[0].installationState).to.be(appdb.ISTATE_PENDING_RESTORE);
expect(apps[0].oldConfig).to.be(null);
expect(apps[1].installationState).to.be(appdb.ISTATE_PENDING_RESTORE);
expect(apps[2].installationState).to.be(appdb.ISTATE_PENDING_RESTORE);
expect(apps[2].oldConfig).to.be(null);
done();
});
});
});
});
});
+2 -2
View File
@@ -31,7 +31,7 @@ var MANIFEST = {
"contactEmail": "support@cloudron.io",
"version": "0.1.0",
"manifestVersion": 1,
"dockerImage": "cloudron/test:8.0.0",
"dockerImage": "cloudron/test:16.0.0",
"healthCheckPath": "/",
"httpPort": 7777,
"tcpPorts": {
@@ -84,7 +84,7 @@ describe('apptask', function () {
config.set('version', '0.5.0');
async.series([
database.initialize,
appdb.add.bind(null, APP.id, APP.appStoreId, APP.manifest, APP.location, APP.portBindings, APP.accessRestriction, APP.memoryLimit, null),
appdb.add.bind(null, APP.id, APP.appStoreId, APP.manifest, APP.location, APP.portBindings, APP),
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }),
settings.setTlsConfig.bind(null, { provider: 'caas' })
], done);
+160 -3
View File
@@ -1,4 +1,3 @@
/* jslint node:true */
/* global it:false */
/* global describe:false */
/* global before:false */
@@ -6,8 +5,23 @@
'use strict';
var certificates = require('../certificates.js'),
expect = require('expect.js');
var async = require('async'),
certificates = require('../certificates.js'),
config = require('../config.js'),
database = require('../database.js'),
expect = require('expect.js'),
settings = require('../settings.js');
function setup(done) {
async.series([
database.initialize,
database._clear
], done);
}
function cleanup(done) {
database._clear(done);
}
describe('Certificates', function () {
describe('validateCertificate', function () {
@@ -87,4 +101,147 @@ describe('Certificates', function () {
expect(certificates.validateCertificate(validCert0, validKey1, 'foobar.com')).to.be.an(Error);
});
});
describe('getApi - caas', function () {
before(function (done) {
async.series([
setup,
settings.setTlsConfig.bind(null, { provider: 'caas' })
], done);
});
after(cleanup);
it('returns prod caas for prod cloudron', function (done) {
config.set('boxVersionsUrl', 'http://prod/release.json');
certificates._getApi({ }, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('caas');
expect(options.prod).to.be(true);
done();
});
});
it('returns non-prod caas for dev cloudron', function (done) {
config.set('boxVersionsUrl', 'http://dev/release.json');
certificates._getApi({ }, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('caas');
expect(options.prod).to.be(false);
done();
});
});
it('returns prod-acme with altDomain in prod cloudron', function (done) {
config.set('boxVersionsUrl', 'http://prod/release.json');
certificates._getApi({ altDomain: 'foo.something.com' }, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(true);
done();
});
});
it('returns non-prod acme with altDomain in dev cloudron', function (done) {
config.set('boxVersionsUrl', 'http://dev/release.json');
certificates._getApi({ altDomain: 'foo.something.com' }, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(false);
done();
});
});
});
describe('getApi - le-prod', function () {
before(function (done) {
async.series([
setup,
settings.setTlsConfig.bind(null, { provider: 'le-prod' })
], done);
});
after(cleanup);
it('returns prod acme in prod cloudron', function (done) {
config.set('boxVersionsUrl', 'http://prod/release.json');
certificates._getApi({ }, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(true);
done();
});
});
it('returns prod acme with altDomain in prod cloudron', function (done) {
config.set('boxVersionsUrl', 'http://prod/release.json');
certificates._getApi({ altDomain: 'foo.bar.com' }, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(true);
done();
});
});
it('returns prod acme in dev cloudron', function (done) {
config.set('boxVersionsUrl', 'http://dev/release.json');
certificates._getApi({ }, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(true);
done();
});
});
});
describe('getApi - le-staging', function () {
before(function (done) {
async.series([
setup,
settings.setTlsConfig.bind(null, { provider: 'le-staging' })
], done);
});
after(cleanup);
it('returns staging acme in prod cloudron', function (done) {
config.set('boxVersionsUrl', 'http://prod/release.json');
certificates._getApi({ }, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(false);
done();
});
});
it('returns staging acme in dev cloudron', function (done) {
config.set('boxVersionsUrl', 'http://dev/release.json');
certificates._getApi({ }, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(false);
done();
});
});
it('returns staging acme with altDomain in prod cloudron', function (done) {
config.set('boxVersionsUrl', 'http://prod/release.json');
certificates._getApi({ altDomain: 'foo.bar.com' }, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(false);
done();
});
});
});
});

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