Compare commits

..

194 Commits

Author SHA1 Message Date
Girish Ramakrishnan 12200f2e0d split aws code into storage backends 2015-11-06 18:22:29 -08:00
Girish Ramakrishnan a853afc407 backups: add api call 2015-11-06 18:14:59 -08:00
Girish Ramakrishnan de471b0012 return BackupError and not SubdomainError 2015-11-06 18:08:25 -08:00
Girish Ramakrishnan b6f1ad75b8 merge SubdomainError into subdomains.js like other error classes 2015-11-06 17:58:01 -08:00
Girish Ramakrishnan e6840f352d remove spurious debugs 2015-11-05 11:59:11 -08:00
Johannes Zellner 6456874f97 Avoid ui glitch in setup if no custom domain
This moves the wizard flow logic to next() instead of the views individually
2015-11-05 20:32:30 +01:00
Johannes Zellner 66b4a4b02a Remove unused favicon middleware 2015-11-05 19:29:08 +01:00
Girish Ramakrishnan 7e36b3f8e5 mailer: set dnsReady flag 2015-11-05 10:28:41 -08:00
Girish Ramakrishnan 12061cc707 mailDnsRecordIds is never used 2015-11-05 10:22:25 -08:00
Girish Ramakrishnan afcc62ecf6 mailServer is never used 2015-11-05 10:22:25 -08:00
Johannes Zellner bec6850c98 Specify icon for oauth views 2015-11-05 19:19:28 +01:00
Girish Ramakrishnan d253a06bab Revert "Remove unused very old yellowtent favicon"
This reverts commit c15a200d4a.

This is used by the favicon middleware
2015-11-05 10:14:59 -08:00
Johannes Zellner 857c5c69b1 force reload the webadmin on cert upload 2015-11-05 18:38:32 +01:00
Girish Ramakrishnan 766fc49f39 setup dkim records for custom domain 2015-11-05 09:30:23 -08:00
Johannes Zellner 941e09ca9f Update the cloudron icon 2015-11-05 18:05:19 +01:00
Johannes Zellner 2466a97fb8 Remove more unused image assets 2015-11-05 18:04:54 +01:00
Johannes Zellner 81f92f5182 Remover another unused favicon 2015-11-05 17:49:25 +01:00
Johannes Zellner 91e1d442ff Update the avatar 2015-11-05 17:48:18 +01:00
Johannes Zellner a1d6ae2296 Remove unused very old yellowtent favicon 2015-11-05 17:46:17 +01:00
Girish Ramakrishnan b529fd3bea we expect the server cert to be first like the rfc says 2015-11-04 19:22:37 -08:00
Girish Ramakrishnan bf319cf593 more verbose certificate message 2015-11-04 19:15:23 -08:00
Girish Ramakrishnan 15eedd2a84 pass on certificate errors 2015-11-04 19:13:16 -08:00
Girish Ramakrishnan d0cd3d1c32 Put dns first since it says dns & certs 2015-11-04 18:23:51 -08:00
Girish Ramakrishnan 747786d0c8 fix avatar url for custom domains 2015-11-04 18:21:46 -08:00
Girish Ramakrishnan b232255170 move dns and certs to own view 2015-11-04 18:21:43 -08:00
Girish Ramakrishnan bd2982ea69 move backups after about 2015-11-04 16:41:33 -08:00
Girish Ramakrishnan 1c948cc83c route53: Do no use weight and setIdentifier 2015-11-04 15:15:39 -08:00
Girish Ramakrishnan ccde1e51ad debug any failure 2015-11-04 14:28:26 -08:00
Girish Ramakrishnan 03ec940352 Add space in spf record 2015-11-04 14:22:56 -08:00
Girish Ramakrishnan bd5b15e279 insert any dns config into settings 2015-11-04 08:45:12 -08:00
Girish Ramakrishnan b6897a4577 Revert "Add isConfigured fallback for caas cloudrons"
This reverts commit 338f68a0f3.

We will send dns config from appstore instead
2015-11-04 08:28:21 -08:00
Johannes Zellner f7225523ec Add isConfigured fallback for caas cloudrons 2015-11-04 12:53:16 +01:00
Girish Ramakrishnan 9d9509525c listen on timezone key only when configured 2015-11-03 16:11:24 -08:00
Girish Ramakrishnan b1dbb3570b Add configured event
Cloudron code paths like cron/mailer/taskmanager now wait for configuration
to be complete before doing anything.

This is useful when a cloudron is moved from a non-custom domain to a custom domain.
In that case, we require route53 configs.
2015-11-03 16:06:38 -08:00
Girish Ramakrishnan c075160e5d Remove event listener 2015-11-03 15:22:02 -08:00
Girish Ramakrishnan 612ceba98a unsubscribe from events 2015-11-03 15:19:06 -08:00
Girish Ramakrishnan 7d5e0040bc debug only if error 2015-11-03 15:15:37 -08:00
Girish Ramakrishnan d6e19d2000 resume tasks only if cloudron is activated 2015-11-03 15:14:59 -08:00
Girish Ramakrishnan a01d5db2a0 minor refactor 2015-11-02 17:45:38 -08:00
Girish Ramakrishnan 5de3baffd4 send monotonic timestamp as well 2015-11-02 14:26:15 -08:00
Girish Ramakrishnan 63c10e8f02 fix typo 2015-11-02 14:23:02 -08:00
Girish Ramakrishnan a99e7c2783 disable logstream testing (since it requires journald) 2015-11-02 14:08:34 -08:00
Girish Ramakrishnan 88b1cc553f Use journalctl to get app logs 2015-11-02 14:08:34 -08:00
Girish Ramakrishnan 316e8dedd3 name is a query parameter 2015-11-02 14:08:34 -08:00
Johannes Zellner f106a76cd5 Fix the avatar and brand label links in navbar 2015-11-02 21:27:52 +01:00
Girish Ramakrishnan 95b2bea828 Give containers a name 2015-11-02 09:34:31 -08:00
Girish Ramakrishnan 58d6166592 fix indexOf matching in addDnsRecords 2015-10-30 18:12:24 -07:00
Girish Ramakrishnan d42f66bfed Fix casing of zone id 2015-10-30 18:05:08 -07:00
Girish Ramakrishnan 5bd8579e73 add null check 2015-10-30 16:24:56 -07:00
Girish Ramakrishnan 01cd0b6b87 fix indexOf matching 2015-10-30 16:12:39 -07:00
Girish Ramakrishnan b4aec552fc txtRecords is a single level array 2015-10-30 16:04:09 -07:00
Girish Ramakrishnan 93ab606d94 dns resolve using the authoritative nameserver 2015-10-30 15:57:15 -07:00
Girish Ramakrishnan 94e94f136d sendHearbeat on init 2015-10-30 14:58:48 -07:00
Girish Ramakrishnan 1b57128ef6 ListResourceRecordSet returns items based on lexical sorting 2015-10-30 14:41:34 -07:00
Girish Ramakrishnan 219a2b0798 rename function 2015-10-30 13:53:12 -07:00
Girish Ramakrishnan b37d5b0fda enable back spf 2015-10-30 13:48:46 -07:00
Girish Ramakrishnan 0e9aac14eb leave a note on subdomains.update 2015-10-30 13:47:10 -07:00
Girish Ramakrishnan cf81ab0306 subdomains.update now takes array 2015-10-30 13:45:10 -07:00
Girish Ramakrishnan 00d8148e46 fix get call 2015-10-30 13:45:10 -07:00
Johannes Zellner 0b59281dbb Remove leftover console.log() 2015-10-30 21:30:57 +01:00
Johannes Zellner e0c845ca16 Fix cloudron tests when run together 2015-10-30 21:30:57 +01:00
Girish Ramakrishnan d6bff57c7d subdomains.del now takes array values 2015-10-30 13:30:19 -07:00
Girish Ramakrishnan 5c4b4d764e implement subdomains.get for route53 2015-10-30 13:23:43 -07:00
Girish Ramakrishnan bf13b5b931 subdomains.add takes array values 2015-10-30 13:23:43 -07:00
Johannes Zellner afade0a5ac Ensure the next buttons are properly disabled when the fields are invalid 2015-10-30 21:10:01 +01:00
Johannes Zellner 40da8736d4 Do not use special wizard page controller for dns view 2015-10-30 21:10:01 +01:00
Johannes Zellner a55675b440 Fixup the enter focus flow 2015-10-30 21:10:01 +01:00
Girish Ramakrishnan 6ce71c7506 prepare dns backends to accepts array of values 2015-10-30 13:04:43 -07:00
Girish Ramakrishnan 0dda91078d remove listeners on uninitialize 2015-10-30 12:52:33 -07:00
Girish Ramakrishnan 93632f5c76 disable spf for testing 2015-10-30 12:50:47 -07:00
Girish Ramakrishnan cb4cd10013 settings changed callback provides the changed setting as first argument 2015-10-30 12:50:47 -07:00
Johannes Zellner 62bcf09ab4 Fix error message in set dns config 2015-10-30 20:32:58 +01:00
Girish Ramakrishnan b466dc1970 remove unused require 2015-10-30 11:43:40 -07:00
Girish Ramakrishnan 0a10eb66cc caas: add subdomains.get 2015-10-29 16:41:04 -07:00
Girish Ramakrishnan c6322c00aa remove unused subdomains.addMany 2015-10-29 16:39:07 -07:00
Girish Ramakrishnan b549a4bddf minor rename of variable 2015-10-29 16:38:18 -07:00
Girish Ramakrishnan 3fa50f2a1a handle getSubdomain error 2015-10-29 16:27:01 -07:00
Girish Ramakrishnan ddded0ebfb emit change event 2015-10-29 16:21:41 -07:00
Girish Ramakrishnan 71c0945607 add updateSubdomain in route53 backend 2015-10-29 15:37:51 -07:00
Girish Ramakrishnan f0295c5dc5 debug message for update already in progress 2015-10-29 15:34:30 -07:00
Girish Ramakrishnan 4e1286a8cf addDnsRecords on restarts after activation 2015-10-29 15:00:53 -07:00
Girish Ramakrishnan d69cead362 remove unused variable 2015-10-29 14:57:51 -07:00
Girish Ramakrishnan 7699cffa26 implement dns updates for custom domains 2015-10-29 14:33:34 -07:00
Girish Ramakrishnan 1021fc566f Add subdomains.update 2015-10-29 14:16:09 -07:00
Girish Ramakrishnan 1fb3b2c373 Add get and update subdomain to caas 2015-10-29 14:15:54 -07:00
Girish Ramakrishnan 2428000262 Add square bracket for empty string/no apps 2015-10-29 13:07:52 -07:00
Johannes Zellner 3d5b4f3191 Ensure we only show cert related ui parts for custom domain cloudrons 2015-10-29 21:01:24 +01:00
Girish Ramakrishnan eb6a217f4a set non-custom domain provider as caas 2015-10-29 12:41:31 -07:00
Girish Ramakrishnan 06aaf98716 dnsConfig provider can be caas 2015-10-29 12:33:10 -07:00
Girish Ramakrishnan 26fc1fd7a6 use debug again 2015-10-29 12:28:50 -07:00
Girish Ramakrishnan a9aa3c4fd8 use debug instead of console.error 2015-10-29 12:26:58 -07:00
Girish Ramakrishnan 61d4509a8e do not emit fake activation event
cloudron is simply initialized the first thing
2015-10-29 12:18:25 -07:00
Johannes Zellner 8cff4f4ff1 Only print an app alive digest 2015-10-29 19:54:20 +01:00
Girish Ramakrishnan 5dc30e02c4 mailer: do not start until activated 2015-10-29 10:12:44 -07:00
Girish Ramakrishnan 55f070e12c ensure cloudron.js is initialized first 2015-10-29 10:11:05 -07:00
Girish Ramakrishnan 0afb8f51c3 Fix typo 2015-10-29 09:00:31 -07:00
Girish Ramakrishnan 42f2637078 setup dns records on activation
do no wait for records to sync as well. the appstore does all the
waiting now (or the user in selfhosted case)
2015-10-29 08:43:25 -07:00
Girish Ramakrishnan bbec7c6610 send heartbeat regardless of activation 2015-10-29 08:40:52 -07:00
Johannes Zellner 76fc257661 Add a link to support page 2015-10-29 14:48:02 +01:00
Johannes Zellner 58ce50571a Adjust text 2015-10-29 14:41:59 +01:00
Johannes Zellner 14205d2810 Add missing = 2015-10-29 14:36:25 +01:00
Johannes Zellner d798fc4b3f Show current dns config in settings ui 2015-10-29 14:34:43 +01:00
Johannes Zellner d29d07cb2d Add Client.getDnsConfig() 2015-10-29 14:34:25 +01:00
Johannes Zellner 07a0b360f6 Add form logic for dns credentials 2015-10-29 13:53:48 +01:00
Johannes Zellner 8b253a8a61 Rename cert header 2015-10-29 12:56:12 +01:00
Johannes Zellner fddbf96c9c Add form feedback for cert forms in settings 2015-10-29 12:55:32 +01:00
Johannes Zellner d1d01ae4b8 Do not double submit the forms 2015-10-29 12:37:59 +01:00
Johannes Zellner 51706afc46 Fix typo in field name 2015-10-29 12:37:30 +01:00
Johannes Zellner d4ea23b1ac Add client.setAdminCertificate() 2015-10-29 12:29:25 +01:00
Johannes Zellner 0460beccf0 Add route to set the admin certificate
This route is separate until we can treat the webadmin just
like any other app
2015-10-29 12:27:57 +01:00
Johannes Zellner aa5ed17dfa streamline cert upload forms in settings 2015-10-29 11:52:53 +01:00
Girish Ramakrishnan 32173b19c9 Do not subscribe to activation event if already activated 2015-10-28 17:07:13 -07:00
Girish Ramakrishnan 1a8fd7dd92 remove gInitialized pattern
not sure why we initialize anything more than once
2015-10-28 17:05:28 -07:00
Girish Ramakrishnan f0047bc1aa console.error -> debug 2015-10-28 17:05:16 -07:00
Girish Ramakrishnan 917832e0ae Change DKIM selector to cloudron 2015-10-28 16:16:15 -07:00
Girish Ramakrishnan cf8948ac69 console.error to debug 2015-10-28 16:08:12 -07:00
Girish Ramakrishnan b2df639155 Move dns backends to separate directory 2015-10-28 16:04:49 -07:00
Girish Ramakrishnan 70ace09ff5 remove unused digitalocean.js 2015-10-28 15:25:22 -07:00
Girish Ramakrishnan 35a69f595a remove unused require 2015-10-28 15:22:10 -07:00
Girish Ramakrishnan f4c4a931d2 Fix debug message 2015-10-28 15:21:47 -07:00
Girish Ramakrishnan 7caced2fe8 Do not send email if SPF record is not setup correctly 2015-10-28 14:45:51 -07:00
Johannes Zellner 846e5deb36 Add cert form error reporting to app install form 2015-10-28 22:21:20 +01:00
Johannes Zellner eca328b247 Add cert and key to app install route 2015-10-28 22:09:19 +01:00
Johannes Zellner c0e9091e4b Preselect current user for singleuser apps 2015-10-28 22:07:41 +01:00
Johannes Zellner 6b6e417435 Add user id to profile object 2015-10-28 22:07:30 +01:00
Johannes Zellner 954bb7039c Make cert forms appear as a group 2015-10-28 21:20:59 +01:00
Johannes Zellner ae01f517c7 Mark cert fields as optional 2015-10-28 20:51:11 +01:00
Johannes Zellner 385bfe07e2 Add cert handling to install form 2015-10-28 20:50:55 +01:00
Johannes Zellner 25aff6a53b Remove unsed scope vars 2015-10-28 20:38:22 +01:00
Johannes Zellner edcbf79b85 Add form feedback for certs 2015-10-28 20:30:35 +01:00
Girish Ramakrishnan 2591b8e10c minor rewording 2015-10-28 10:17:46 -07:00
Johannes Zellner 9df9d1667f Test certs are now simply embedded 2015-10-28 16:34:09 +01:00
Johannes Zellner 7798111af1 Ensure the test certs match domain and the folder is created 2015-10-28 16:33:45 +01:00
Johannes Zellner 12351113a9 Fixup the tests for wildcard cert 2015-10-28 16:00:51 +01:00
Johannes Zellner d9256f99af Make sure the dns sync file is removed 2015-10-28 15:32:49 +01:00
Johannes Zellner cf021066ed Move cert validation to settings and use it for wildcard cert 2015-10-28 14:35:39 +01:00
Johannes Zellner 04eb2a982f Add proper cert validator 2015-10-28 14:20:25 +01:00
Johannes Zellner 22dcc787b5 Add x509 node module 2015-10-28 14:20:03 +01:00
Johannes Zellner 5d4d0c0a86 Add missing fs. 2015-10-28 12:56:09 +01:00
Johannes Zellner e81db9728a Set the cert and key dynamically when rendering nginx appconfig 2015-10-28 12:42:04 +01:00
Johannes Zellner db305af8c9 We name certs with .cert extension 2015-10-28 12:33:27 +01:00
Johannes Zellner 4b3aca7773 certs should be stored with app fqdn 2015-10-28 12:28:57 +01:00
Johannes Zellner 5b5abe99e7 Save the uploaded certs to app cert directory 2015-10-28 12:24:59 +01:00
Johannes Zellner 8f670eb755 Add per app cert dir 2015-10-28 12:23:16 +01:00
Johannes Zellner 21a604814c Add tests for app cert upload 2015-10-28 12:13:37 +01:00
Johannes Zellner 7eeb835d96 Adjust the settings view to upload certs as json body 2015-10-28 12:12:54 +01:00
Johannes Zellner 57de915133 Make settings certificate upload route also just using the json body 2015-10-28 12:12:06 +01:00
Johannes Zellner a892de5c2d Ensure cert and key are strings 2015-10-28 11:50:50 +01:00
Johannes Zellner 69cd01955b No more dns view 2015-10-28 10:36:55 +01:00
Girish Ramakrishnan f39809c941 EE API is synchronous 2015-10-27 22:18:02 -07:00
Girish Ramakrishnan 09c4bfeb51 Add DNS records for non-custom domains before activation 2015-10-27 21:10:00 -07:00
Girish Ramakrishnan 615789a9ad fix unregisterSubdomain loop 2015-10-27 18:53:06 -07:00
Girish Ramakrishnan bec5eaf3c9 send heartbeat immediately on startup 2015-10-27 17:05:56 -07:00
Girish Ramakrishnan 4f13ef9cea hearbeat does not rely on dns sync 2015-10-27 16:42:24 -07:00
Girish Ramakrishnan 873de48beb Do not add DNS records for custom domain 2015-10-27 16:23:08 -07:00
Girish Ramakrishnan 87e70b86d3 sendHeartbeat on activation event 2015-10-27 16:20:14 -07:00
Girish Ramakrishnan 140aa85223 Add cloudron.isActivatedSync 2015-10-27 16:12:05 -07:00
Girish Ramakrishnan 3ac3207497 send heartbeats regardless of activation 2015-10-27 16:05:19 -07:00
Girish Ramakrishnan e36a0b9a30 create cron jobs only on activation 2015-10-27 16:04:29 -07:00
Girish Ramakrishnan 0b1aac7687 add null check for all jobs 2015-10-27 16:02:42 -07:00
Girish Ramakrishnan e008cde2ff Add dns records on activation 2015-10-27 16:00:31 -07:00
Girish Ramakrishnan d1e46be8ad Do not set dns config if null 2015-10-27 12:41:13 -07:00
Girish Ramakrishnan dc18a18248 remove unused variables 2015-10-27 12:39:06 -07:00
Girish Ramakrishnan b9a0ad73ab $location is not defined 2015-10-27 12:37:01 -07:00
Girish Ramakrishnan e2c3fb309c Add custom domain setup step 2015-10-27 12:04:27 -07:00
Girish Ramakrishnan d5255b8cf4 Add Client.setDnsConfig 2015-10-27 12:02:47 -07:00
Girish Ramakrishnan 42e70e870b deploymentConfig is never used 2015-10-27 11:38:05 -07:00
Johannes Zellner 8ffd7b0197 Adjust the webadmin code for cert upload 2015-10-27 18:38:46 +01:00
Johannes Zellner 01ead194d8 Move cert upload route to /settings 2015-10-27 18:38:46 +01:00
Girish Ramakrishnan 80b9d4be50 awscredentials route is not called anymore 2015-10-27 10:24:42 -07:00
Girish Ramakrishnan ef06836804 make apps-test partially work 2015-10-27 10:14:51 -07:00
Johannes Zellner 916870b546 Send cert and key with configure 2015-10-27 18:11:48 +01:00
Girish Ramakrishnan 2da7216be6 make apptask-test work 2015-10-27 10:02:43 -07:00
Girish Ramakrishnan 54215cff7a Use the aws backend for tests 2015-10-27 10:02:43 -07:00
Girish Ramakrishnan 166257bbdc Allow endpoint to be configured (for the tests) 2015-10-27 10:02:43 -07:00
Girish Ramakrishnan d502e04cbd use aws backend for custom domains 2015-10-27 10:02:43 -07:00
Girish Ramakrishnan 1fca680a67 read dns config from settings 2015-10-27 10:02:43 -07:00
Johannes Zellner 4ea3238391 Pass certs down to apps.configure 2015-10-27 16:36:09 +01:00
Johannes Zellner fa12e7bd97 Add cert and key to app configure route 2015-10-27 15:44:47 +01:00
Johannes Zellner 6118535c4a Add test helper script to generate a selfsigned cert 2015-10-27 15:06:53 +01:00
Johannes Zellner 920f04aab3 Use test-app image 10.0.0 2015-10-27 14:20:19 +01:00
Johannes Zellner ed13f2d6ef Add basic form elements for certificate in app configure 2015-10-27 12:26:55 +01:00
Johannes Zellner dff27fe7b3 Remove unused dns views 2015-10-27 10:40:05 +01:00
Johannes Zellner 5d589e7330 Move certificate upload form from dns to settings 2015-10-27 10:39:02 +01:00
Johannes Zellner 01ec16f472 Remove useless console.log() 2015-10-27 10:38:11 +01:00
Girish Ramakrishnan f510d4bc10 add route for setting/getting dns settings 2015-10-26 16:52:59 -07:00
Girish Ramakrishnan 2db2eb13af add settings.get/setDnsConfig 2015-10-26 16:35:50 -07:00
Girish Ramakrishnan 82e1c07722 separate out dns and backup credentials 2015-10-26 16:23:41 -07:00
Girish Ramakrishnan 23ba078a17 Fix redis hostname 2015-10-23 19:24:22 -07:00
Girish Ramakrishnan b5358e7565 recreate docker containers for hostname change 2015-10-23 16:30:17 -07:00
Girish Ramakrishnan 697699bd5f test the new env vars APP_* 2015-10-23 16:27:40 -07:00
Girish Ramakrishnan dd2a806ab8 Do not set hostname of app container
Some apps like pasteboard try to curl the public app url from inside
the container. This fails because we set the hostname and the hostname
maps to the internal docker IP.

To fix this, simply export two environment variables providing the
app's domain and origin. The hostname is set to the app location instead
of the FQDN for debugging.

Fixes #521
2015-10-23 16:17:35 -07:00
Girish Ramakrishnan 84d96cebee linter fixes 2015-10-23 16:06:55 -07:00
Johannes Zellner 10658606d7 Bring back 'Cloudron' in the login header 2015-10-23 20:21:31 +02:00
70 changed files with 2055 additions and 911 deletions
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

+12 -42
View File
@@ -1918,48 +1918,6 @@
}
}
},
"node-sass": {
"version": "3.3.3",
"from": "node-sass@>=3.0.0-alpha.0 <4.0.0",
"resolved": "https://registry.npmjs.org/node-sass/-/node-sass-3.3.3.tgz",
"dependencies": {
"ansi-styles": {
"version": "2.1.0",
"from": "ansi-styles@>=2.1.0 <3.0.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.1.0.tgz"
},
"chalk": {
"version": "1.1.1",
"from": "chalk@>=1.1.1 <2.0.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.1.tgz"
},
"get-stdin": {
"version": "4.0.1",
"from": "get-stdin@>=4.0.1 <5.0.0",
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz"
},
"glob": {
"version": "5.0.15",
"from": "glob@>=5.0.14 <6.0.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz"
},
"minimatch": {
"version": "3.0.0",
"from": "minimatch@>=2.0.0 <3.0.0||>=3.0.0 <4.0.0",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.0.tgz"
},
"nan": {
"version": "2.1.0",
"from": "nan@>=2.0.8 <3.0.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.1.0.tgz"
},
"strip-ansi": {
"version": "3.0.0",
"from": "strip-ansi@>=3.0.0 <4.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.0.tgz"
}
}
},
"node-uuid": {
"version": "1.4.3",
"from": "node-uuid@>=1.4.3 <2.0.0",
@@ -3725,6 +3683,18 @@
"from": "wrench@>=1.5.8 <1.6.0",
"resolved": "https://registry.npmjs.org/wrench/-/wrench-1.5.8.tgz"
},
"x509": {
"version": "0.2.2",
"from": "x509@*",
"resolved": "https://registry.npmjs.org/x509/-/x509-0.2.2.tgz",
"dependencies": {
"nan": {
"version": "2.0.9",
"from": "nan@2.0.9",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.0.9.tgz"
}
}
},
"xtend": {
"version": "4.0.0",
"from": "xtend@>=4.0.0 <4.1.0",
+2 -1
View File
@@ -62,7 +62,8 @@
"tail-stream": "https://registry.npmjs.org/tail-stream/-/tail-stream-0.2.1.tgz",
"underscore": "^1.7.0",
"valid-url": "^1.0.9",
"validator": "^3.30.0"
"validator": "^3.30.0",
"x509": "^0.2.2"
},
"devDependencies": {
"apidoc": "*",
+2 -2
View File
@@ -3,7 +3,7 @@
# If you change the infra version, be sure to put a warning
# in the change log
INFRA_VERSION=18
INFRA_VERSION=20
# WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
# These constants are used in the installer script as well
@@ -12,7 +12,7 @@ MYSQL_IMAGE=cloudron/mysql:0.7.0
POSTGRESQL_IMAGE=cloudron/postgresql:0.7.0
MONGODB_IMAGE=cloudron/mongodb:0.7.0
REDIS_IMAGE=cloudron/redis:0.7.0 # if you change this, fix src/addons.js as well
MAIL_IMAGE=cloudron/mail:0.7.0
MAIL_IMAGE=cloudron/mail:0.8.0
GRAPHITE_IMAGE=cloudron/graphite:0.7.0
MYSQL_REPO=cloudron/mysql
+4
View File
@@ -18,6 +18,7 @@ arg_version=""
arg_web_server_origin=""
arg_backup_key=""
arg_aws=""
arg_dns_config=""
args=$(getopt -o "" -l "data:,retire" -n "$0" -- "$@")
eval set -- "${args}"
@@ -49,6 +50,9 @@ EOF
arg_aws=$(echo "$2" | $json aws)
[[ "${arg_aws}" == "null" ]] && arg_aws=""
arg_dns_config=$(echo "$2" | $json dnsConfig)
[[ "${arg_dns_config}" == "null" ]] && arg_dns_config=""
shift 2
;;
--) break;;
+2 -2
View File
@@ -29,10 +29,10 @@ infra_version="none"
if [[ "${arg_retire}" == "true" || "${infra_version}" != "${INFRA_VERSION}" ]]; then
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}\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
-O "{ \"vhost\": \"~^(.+)\$\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
else
${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}\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
-O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
fi
echo '{ "update": { "percent": "10", "message": "Updating cloudron software" }, "backup": null }' > "${SETUP_WEBSITE_DIR}/progress.json"
+10 -1
View File
@@ -38,6 +38,7 @@ set_progress "10" "Ensuring directories"
# keep these in sync with paths.js
[[ "${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}/graphite"
@@ -106,7 +107,7 @@ ${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/nginx.ejs
# generate these for update code paths as well to overwrite splash
${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
-O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"admin\", \"sourceDir\": \"${BOX_SRC_DIR}\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
-O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"admin\", \"sourceDir\": \"${BOX_SRC_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
mkdir -p "${DATA_DIR}/nginx/cert"
echo "${arg_tls_cert}" > ${DATA_DIR}/nginx/cert/host.cert
@@ -154,6 +155,14 @@ cat > "${BOX_SRC_DIR}/webadmin/dist/config.json" <<CONF_END
CONF_END
EOF
# Add DNS Configuration
if [[ ! -z "${arg_dns_config}" ]]; then
echo "Add DNS Config"
mysql -u root -p${mysql_root_password} \
-e "REPLACE INTO settings (name, value) VALUES (\"dns_config\", '$arg_dns_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
+2 -2
View File
@@ -10,8 +10,8 @@ server {
ssl on;
# paths are relative to prefix and not to this file
ssl_certificate cert/host.cert;
ssl_certificate_key cert/host.key;
ssl_certificate <%= certFilePath %>;
ssl_certificate_key <%= keyFilePath %>;
ssl_session_timeout 5m;
ssl_session_cache shared:SSL:50m;
+23 -1
View File
@@ -9,6 +9,7 @@ exports = module.exports = {
getEnvironment: getEnvironment,
getLinksSync: getLinksSync,
getBindsSync: getBindsSync,
getContainerNamesSync: getContainerNamesSync,
// exported for testing
_setupOauth: setupOauth,
@@ -239,6 +240,27 @@ function getBindsSync(app, addons) {
return binds;
}
function getContainerNamesSync(app, addons) {
assert.strictEqual(typeof app, 'object');
assert(!addons || typeof addons === 'object');
var names = [ ];
if (!addons) return names;
for (var addon in addons) {
switch (addon) {
case 'scheduler':
// names here depend on how scheduler.js creates containers
names = names.concat(Object.keys(addons.scheduler).map(function (taskName) { return app.id + '-' + taskName; }));
break;
default: break;
}
}
return names;
}
function setupOauth(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
@@ -754,7 +776,7 @@ function setupRedis(app, options, callback) {
var createOptions = {
name: 'redis-' + app.id,
Hostname: config.appFqdn(app.location),
Hostname: 'redis-' + app.location,
Tty: true,
Image: 'cloudron/redis:0.7.0', // if you change this, fix setup/INFRA_VERSION as well
Cmd: null,
+3 -1
View File
@@ -97,7 +97,6 @@ function checkAppHealth(app, callback) {
debugApp(app, 'not alive : %s', error || res.status);
setHealth(app, appdb.HEALTH_UNHEALTHY, callback);
} else {
debugApp(app, 'alive');
setHealth(app, appdb.HEALTH_HEALTHY, callback);
}
});
@@ -110,6 +109,9 @@ function processApps(callback) {
async.each(apps, checkAppHealth, function (error) {
if (error) console.error(error);
debug('apps alive: [%s]', apps.map(function (a) { return a.location; }).join(', '));
callback(null);
});
});
+56 -40
View File
@@ -23,7 +23,6 @@ exports = module.exports = {
backup: backup,
backupApp: backupApp,
getLogStream: getLogStream,
getLogs: getLogs,
start: start,
@@ -59,8 +58,10 @@ var addons = require('./addons.js'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
semver = require('semver'),
shell = require('./shell.js'),
spawn = require('child_process').spawn,
split = require('split'),
superagent = require('superagent'),
taskmanager = require('./taskmanager.js'),
@@ -119,6 +120,7 @@ AppsError.PORT_CONFLICT = 'Port Conflict';
AppsError.BILLING_REQUIRED = 'Billing Required';
AppsError.ACCESS_DENIED = 'Access denied';
AppsError.USER_REQUIRED = 'User required';
AppsError.BAD_CERTIFICATE = 'Invalid certificate';
// Hostname validation comes from RFC 1123 (section 2.1)
// Domain name validation comes from RFC 2181 (Name syntax)
@@ -294,7 +296,7 @@ function purchase(appStoreId, callback) {
});
}
function install(appId, appStoreId, manifest, location, portBindings, accessRestriction, oauthProxy, icon, callback) {
function install(appId, appStoreId, manifest, location, portBindings, accessRestriction, oauthProxy, icon, cert, key, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof appStoreId, 'string');
assert(manifest && typeof manifest === 'object');
@@ -303,6 +305,8 @@ function install(appId, appStoreId, manifest, location, portBindings, accessRest
assert.strictEqual(typeof accessRestriction, 'object');
assert.strictEqual(typeof oauthProxy, 'boolean');
assert(!icon || typeof icon === 'string');
assert(cert === null || typeof cert === 'string');
assert(key === null || typeof key === 'string');
assert.strictEqual(typeof callback, 'function');
var error = manifestFormat.parse(manifest);
@@ -332,6 +336,9 @@ function install(appId, appStoreId, manifest, location, portBindings, accessRest
}
}
error = settings.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) {
@@ -341,6 +348,12 @@ function install(appId, appStoreId, manifest, location, portBindings, accessRest
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location.toLowerCase(), 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);
callback(null);
@@ -348,12 +361,14 @@ function install(appId, appStoreId, manifest, location, portBindings, accessRest
});
}
function configure(appId, location, portBindings, accessRestriction, oauthProxy, callback) {
function configure(appId, location, portBindings, accessRestriction, oauthProxy, cert, key, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof portBindings, 'object');
assert.strictEqual(typeof accessRestriction, 'object');
assert.strictEqual(typeof oauthProxy, 'boolean');
assert(cert === null || typeof cert === 'string');
assert(key === null || typeof key === 'string');
assert.strictEqual(typeof callback, 'function');
var error = validateHostname(location, config.fqdn());
@@ -362,6 +377,9 @@ function configure(appId, location, portBindings, accessRestriction, oauthProxy,
error = validateAccessRestriction(accessRestriction);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
error = settings.validateCertificate(cert, key, config.appFqdn(location));
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
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));
@@ -369,6 +387,12 @@ function configure(appId, location, portBindings, accessRestriction, oauthProxy,
error = validatePortBindings(portBindings, app.manifest.tcpPorts);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 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));
}
var values = {
location: location.toLowerCase(),
accessRestriction: accessRestriction,
@@ -451,58 +475,50 @@ function update(appId, force, manifest, portBindings, icon, callback) {
});
}
function getLogStream(appId, fromLine, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof fromLine, 'number'); // behaves like tail -n
assert.strictEqual(typeof callback, 'function');
function appLogFilter(app) {
var names = [ app.id ].concat(addons.getContainerNamesSync(app, app.manifest.addons));
debug('Getting logs for %s', appId);
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));
if (app.installationState !== appdb.ISTATE_INSTALLED) return callback(new AppsError(AppsError.BAD_STATE, util.format('App is in %s state.', app.installationState)));
var container = docker.getContainer(app.containerId);
var tail = fromLine < 0 ? -fromLine : 'all';
// note: cannot access docker file directly because it needs root access
container.logs({ stdout: true, stderr: true, follow: true, timestamps: true, tail: tail }, function (error, logStream) {
if (error && error.statusCode === 404) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
var lineCount = 0;
var skipLinesStream = split(function mapper(line) {
if (++lineCount < fromLine) return undefined;
var timestamp = line.substr(0, line.indexOf(' ')); // sometimes this has square brackets around it
return JSON.stringify({ lineNumber: lineCount, timestamp: timestamp.replace(/[[\]]/g,''), log: line.substr(timestamp.length + 1) });
});
skipLinesStream.close = logStream.req.abort;
logStream.pipe(skipLinesStream);
return callback(null, skipLinesStream);
});
});
return names.map(function (name) { return 'CONTAINER_NAME=' + name; });
}
function getLogs(appId, callback) {
function getLogs(appId, lines, follow, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof lines, 'number');
assert.strictEqual(typeof follow, 'boolean');
assert.strictEqual(typeof callback, 'function');
debug('Getting logs for %s', appId);
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));
if (app.installationState !== appdb.ISTATE_INSTALLED) return callback(new AppsError(AppsError.BAD_STATE, util.format('App is in %s state.', app.installationState)));
var container = docker.getContainer(app.containerId);
// note: cannot access docker file directly because it needs root access
container.logs({ stdout: true, stderr: true, follow: false, timestamps: true, tail: 'all' }, function (error, logStream) {
if (error && error.statusCode === 404) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
var args = [ '--output=json', '--no-pager', '--lines=' + lines ];
if (follow) args.push('--follow');
args = args.concat(appLogFilter(app));
return callback(null, logStream);
var cp = spawn('/bin/journalctl', args);
var transformStream = split(function mapper(line) {
var obj = safe.JSON.parse(line);
if (!obj) return undefined;
var source = obj.CONTAINER_NAME.slice(app.id.length + 1);
return JSON.stringify({
realtimeTimestamp: obj.__REALTIME_TIMESTAMP,
monotonicTimestamp: obj.__MONOTONIC_TIMESTAMP,
message: obj.MESSAGE,
source: source || 'main'
});
});
transformStream.close = cp.kill.bind(cp, 'SIGKILL'); // closing stream kills the child process
cp.stdout.pipe(transformStream);
return callback(null, transformStream);
});
}
+22 -22
View File
@@ -50,9 +50,8 @@ var addons = require('./addons.js'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
semver = require('semver'),
shell = require('./shell.js'),
SubdomainError = require('./subdomainerror.js'),
SubdomainError = require('./subdomains.js').SubdomainError,
subdomains = require('./subdomains.js'),
superagent = require('superagent'),
sysinfo = require('./sysinfo.js'),
@@ -78,14 +77,6 @@ function debugApp(app, args) {
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
function targetBoxVersion(manifest) {
if ('targetBoxVersion' in manifest) return manifest.targetBoxVersion;
if ('minBoxVersion' in manifest) return manifest.minBoxVersion;
return '0.0.1';
}
// We expect conflicts to not happen despite closing the port (parallel app installs, app update does not reconfigure nginx etc)
// https://tools.ietf.org/html/rfc6056#section-3.5 says linux uses random ephemeral port allocation
function getFreePort(callback) {
@@ -108,7 +99,20 @@ function configureNginx(app, callback) {
var sourceDir = path.resolve(__dirname, '..');
var endpoint = app.oauthProxy ? 'oauthproxy' : 'app';
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, { sourceDir: sourceDir, adminOrigin: config.adminOrigin(), vhost: config.appFqdn(app.location), port: freePort, endpoint: endpoint });
var vhost = config.appFqdn(app.location);
var certFilePath = safe.fs.statSync(path.join(paths.APP_CERTS_DIR, vhost + '.cert')) ? path.join(paths.APP_CERTS_DIR, vhost + '.cert') : 'cert/host.cert';
var keyFilePath = safe.fs.statSync(path.join(paths.APP_CERTS_DIR, vhost + '.key')) ? path.join(paths.APP_CERTS_DIR, vhost + '.key') : 'cert/host.key';
var data = {
sourceDir: sourceDir,
adminOrigin: config.adminOrigin(),
vhost: vhost,
port: freePort,
endpoint: endpoint,
certFilePath: certFilePath,
keyFilePath: keyFilePath
};
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
debugApp(app, 'writing config to %s', nginxConfigFilename);
@@ -242,12 +246,10 @@ function downloadIcon(app, callback) {
function registerSubdomain(app, callback) {
// even though the bare domain is already registered in the appstore, we still
// need to register it so that we have a dnsRecordId to wait for it to complete
var record = { subdomain: app.location, type: 'A', value: sysinfo.getIp() };
async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Registering subdomain location [%s]', app.location);
subdomains.add(record, function (error, changeId) {
subdomains.add(app.location, 'A', [ sysinfo.getIp() ], function (error, changeId) {
if (error && (error.reason === SubdomainError.STILL_BUSY || error.reason === SubdomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
retryCallback(null, error || changeId);
@@ -266,18 +268,16 @@ function unregisterSubdomain(app, location, callback) {
return callback(null);
}
var record = { subdomain: location, type: 'A', value: sysinfo.getIp() };
async.retry({ times: 30, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Unregistering subdomain: %s', location);
subdomains.remove(record, function (error) {
if (error && (error.reason === SubdomainError.STILL_BUSY || error.reason === SubdomainError.EXTERNAL_ERROR))return retryCallback(error); // try again
subdomains.remove(location, 'A', [ sysinfo.getIp() ], function (error) {
if (error && (error.reason === SubdomainError.STILL_BUSY || error.reason === SubdomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
retryCallback(error);
retryCallback(null, error);
});
}, function (error) {
if (error) debugApp(app, 'Error unregistering subdomain: %s', error);
}, function (error, result) {
if (error || result instanceof Error) return callback(error || result);
updateApp(app, { dnsRecordId: null }, callback);
});
@@ -714,7 +714,7 @@ if (require.main === module) {
if (error) throw error;
startTask(process.argv[2], function (error) {
if (error) console.error(error);
if (error) debug('Apptask completed with error', error);
debug('Apptask completed for %s', process.argv[2]);
// https://nodejs.org/api/process.html are exit codes used by node. apps.js uses the value below
+11 -4
View File
@@ -12,9 +12,10 @@ exports = module.exports = {
};
var assert = require('assert'),
aws = require('./aws.js'),
caas = require('./storage/caas.js'),
config = require('./config.js'),
debug = require('debug')('box:backups'),
s3 = require('./storage/s3.js'),
superagent = require('superagent'),
util = require('util');
@@ -39,6 +40,12 @@ function BackupsError(reason, errorOrMessage) {
util.inherits(BackupsError, Error);
BackupsError.EXTERNAL_ERROR = 'external error';
BackupsError.INTERNAL_ERROR = 'internal error';
BackupsError.MISSING_CREDENTIALS = 'missing credentials';
// choose which storage backend we use for test purpose we use s3
function api() {
return config.token() ? caas : s3;
}
function getAllPaged(page, perPage, callback) {
assert.strictEqual(typeof page, 'number');
@@ -68,7 +75,7 @@ function getBackupUrl(app, callback) {
filename = util.format('backup_%s-v%s.tar.gz', (new Date()).toISOString(), config.version());
}
aws.getSignedUploadUrl(filename, function (error, result) {
api().getSignedUploadUrl(filename, function (error, result) {
if (error) return callback(error);
var obj = {
@@ -89,7 +96,7 @@ function getRestoreUrl(backupId, callback) {
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof callback, 'function');
aws.getSignedDownloadUrl(backupId, function (error, result) {
api().getSignedDownloadUrl(backupId, function (error, result) {
if (error) return callback(error);
var obj = {
@@ -111,7 +118,7 @@ function copyLastBackup(app, callback) {
assert.strictEqual(typeof callback, 'function');
var toFilename = util.format('appbackup_%s_%s-v%s.tar.gz', app.id, (new Date()).toISOString(), app.manifest.version);
aws.copyObject(app.lastBackupId, toFilename, function (error) {
api().copyObject(app.lastBackupId, toFilename, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
return callback(null, toFilename);
+140 -78
View File
@@ -11,15 +11,20 @@ exports = module.exports = {
getConfig: getConfig,
getStatus: getStatus,
setCertificate: setCertificate,
sendHeartbeat: sendHeartbeat,
update: update,
reboot: reboot,
migrate: migrate,
backup: backup,
ensureBackup: ensureBackup
ensureBackup: ensureBackup,
isConfiguredSync: isConfiguredSync,
events: new (require('events').EventEmitter)(),
EVENT_ACTIVATED: 'activated',
EVENT_CONFIGURED: 'configured'
};
var apps = require('./apps.js'),
@@ -51,14 +56,16 @@ var apps = require('./apps.js'),
util = require('util'),
webhooks = require('./webhooks.js');
var RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh'),
REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'),
var REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'),
BACKUP_BOX_CMD = path.join(__dirname, 'scripts/backupbox.sh'),
BACKUP_SWAP_CMD = path.join(__dirname, 'scripts/backupswap.sh'),
INSTALLER_UPDATE_URL = 'http://127.0.0.1:2020/api/v1/installer/update';
var gAddDnsRecordsTimerId = null,
gCloudronDetails = null; // cached cloudron details like region,size...
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
var gUpdatingDns = false, // flag for dns update reentrancy
gCloudronDetails = null, // cached cloudron details like region,size...
gIsConfigured = null; // cached configured state so that return value is synchronous. null means we are not initialized yet
function debugApp(app, args) {
assert(!app || typeof app === 'object');
@@ -76,7 +83,6 @@ function ignoreError(func) {
};
}
function CloudronError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
@@ -110,22 +116,61 @@ CloudronError.NOT_FOUND = 'Not found';
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
if (process.env.BOX_ENV !== 'test') {
addDnsRecords();
}
exports.events.on(exports.EVENT_CONFIGURED, addDnsRecords);
callback(null);
syncConfigState(callback);
}
function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function');
clearTimeout(gAddDnsRecordsTimerId);
gAddDnsRecordsTimerId = null;
exports.events.removeListener(exports.EVENT_CONFIGURED, addDnsRecords);
callback(null);
}
function isConfiguredSync() {
return gIsConfigured === true;
}
function isConfigured(callback) {
// set of rules to see if we have the configs required for cloudron to function
// note this checks for missing configs and not invalid configs
settings.getDnsConfig(function (error, dnsConfig) {
if (error) return callback(error);
if (!dnsConfig) return callback(null, false);
var isConfigured = (config.isCustomDomain() && dnsConfig.provider === 'route53') ||
(!config.isCustomDomain() && dnsConfig.provider === 'caas');
callback(null, isConfigured);
});
}
function syncConfigState(callback) {
assert(!gIsConfigured);
callback = callback || NOOP_CALLBACK;
isConfigured(function (error, configured) {
if (error) return callback(error);
debug('syncConfigState: configured = %s', configured);
if (configured) {
exports.events.emit(exports.EVENT_CONFIGURED);
} else {
settings.events.once(settings.DNS_CONFIG_KEY, function () { syncConfigState(); }); // check again later
}
gIsConfigured = configured;
callback();
});
}
function setTimeZone(ip, callback) {
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -177,6 +222,9 @@ function activate(username, password, email, ip, callback) {
tokendb.add(token, tokendb.PREFIX_USER + 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); });
callback(null, { token: token, expires: expires });
});
});
@@ -265,9 +313,6 @@ function getConfig(callback) {
}
function sendHeartbeat() {
// Only send heartbeats after the admin dns record is synced to give appstore a chance to know that fact
if (!config.dnsInSync()) return;
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/heartbeat';
superagent.post(url).query({ token: config.token(), version: config.version() }).timeout(10000).end(function (error, result) {
@@ -277,92 +322,109 @@ function sendHeartbeat() {
});
}
function addDnsRecords() {
if (config.dnsInSync()) return sendHeartbeat(); // already registered send heartbeat
var DKIM_SELECTOR = 'mail';
var DMARC_REPORT_EMAIL = 'dmarc-report@cloudron.io';
function readDkimPublicKeySync() {
var dkimPublicKeyFile = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn() + '/public');
var publicKey = safe.fs.readFileSync(dkimPublicKeyFile, 'utf8');
if (publicKey === null) {
console.error('Error reading dkim public key. Stop DNS setup.');
return;
debug('Error reading dkim public key.', safe.error);
return null;
}
// remove header, footer and new lines
publicKey = publicKey.split('\n').slice(1, -2).join('');
// note that dmarc requires special DNS records for external RUF and RUA
var records = [
// naked domain
{ subdomain: '', type: 'A', value: sysinfo.getIp() },
// webadmin domain
{ subdomain: 'my', type: 'A', value: sysinfo.getIp() },
// softfail all mails not from our IP. Note that this uses IP instead of 'a' should we use a load balancer in the future
{ subdomain: '', type: 'TXT', value: '"v=spf1 ip4:' + sysinfo.getIp() + ' ~all"' },
// t=s limits the domainkey to this domain and not it's subdomains
{ subdomain: DKIM_SELECTOR + '._domainkey', type: 'TXT', value: '"v=DKIM1; t=s; p=' + publicKey + '"' },
// DMARC requires special setup if report email id is in different domain
{ subdomain: '_dmarc', type: 'TXT', value: '"v=DMARC1; p=none; pct=100; rua=mailto:' + DMARC_REPORT_EMAIL + '; ruf=' + DMARC_REPORT_EMAIL + '"' }
];
return publicKey;
}
debug('addDnsRecords:', records);
function txtRecordsWithSpf(callback) {
assert.strictEqual(typeof callback, 'function');
subdomains.addMany(records, function (error, changeIds) {
if (error) {
console.error('Admin DNS record addition failed', error);
gAddDnsRecordsTimerId = setTimeout(addDnsRecords, 10000);
return;
subdomains.get('', 'TXT', function (error, txtRecords) {
if (error) return callback(error);
debug('txtRecordsWithSpf: current txt records - %j', txtRecords);
var i, validSpf;
for (i = 0; i < txtRecords.length; i++) {
if (txtRecords[i].indexOf('"v=spf1 ') !== 0) continue; // not SPF
validSpf = txtRecords[i].indexOf(' a:' + config.fqdn() + ' ') !== -1;
break;
}
function checkIfInSync() {
debug('addDnsRecords: Check if admin DNS record is in sync.');
if (validSpf) return callback(null, null);
async.eachSeries(changeIds, function (changeId, callback) {
subdomains.status(changeId, function (error, result) {
if (error) return callback(new Error('Failed to check if admin DNS record is in sync.', error));
if (result !== 'done') return callback(new Error(changeId + ' is not in sync. result:' + result));
callback(null);
});
}, function (error) {
if (error) {
debug(error.message);
gAddDnsRecordsTimerId = setTimeout(checkIfInSync, 5000);
return;
}
debug('addDnsRecords: done');
config.setDnsInSync('DNS is in sync for ip ' + sysinfo.getIp());
sendHeartbeat(); // send heartbeat after the dns records are done
});
if (i == txtRecords.length) {
txtRecords[i] = '"v=spf1 a:' + config.fqdn() + ' ~all"';
} else {
txtRecords[i] = '"v=spf1 a:' + config.fqdn() + ' ' + txtRecords[i].slice('"v=spf1 '.length);
}
checkIfInSync();
return callback(null, txtRecords);
});
}
function setCertificate(certificate, key, callback) {
assert.strictEqual(typeof certificate, 'string');
assert.strictEqual(typeof key, 'string');
assert.strictEqual(typeof callback, 'function');
function addDnsRecords() {
var callback = NOOP_CALLBACK;
debug('Updating certificates');
if (process.env.BOX_ENV === 'test') return callback();
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), certificate)) {
return callback(new CloudronError(CloudronError.INTERNAL_ERROR, safe.error.message));
if (gUpdatingDns) {
debug('addDnsRecords: dns update already in progress');
return callback();
}
gUpdatingDns = true;
var DKIM_SELECTOR = 'cloudron';
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')));
var nakedDomainRecord = { subdomain: '', type: 'A', values: [ sysinfo.getIp() ] };
var webadminRecord = { subdomain: 'my', type: 'A', values: [ sysinfo.getIp() ] };
// 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 records = [ ];
if (config.isCustomDomain()) {
records.push(webadminRecord);
records.push(dkimRecord);
} else {
records.push(nakedDomainRecord);
records.push(webadminRecord);
records.push(dkimRecord);
records.push(dmarcRecord);
}
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), key)) {
return callback(new CloudronError(CloudronError.INTERNAL_ERROR, safe.error.message));
}
debug('addDnsRecords: %j', records);
shell.sudo('setCertificate', [ RELOAD_NGINX_CMD ], function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
async.retry({ times: 10, interval: 20000 }, function (retryCallback) {
txtRecordsWithSpf(function (error, txtRecords) {
if (error) return retryCallback(error);
return callback(null);
if (txtRecords) records.push({ subdomain: '', type: 'TXT', values: txtRecords });
debug('addDnsRecords: will update %j', records);
async.eachSeries(records, function (record, iteratorCallback) {
subdomains.update(record.subdomain, record.type, record.values, iteratorCallback);
}, function (error) {
if (error) debug('addDnsRecords: failed to update : %s. will retry', error);
retryCallback(error);
});
});
}, function (error) {
gUpdatingDns = false;
debug('addDnsRecords: done updating records with error:', error);
callback(error);
});
}
-2
View File
@@ -75,9 +75,7 @@ function initConfig() {
data.fqdn = 'localhost';
data.token = null;
data.mailServer = null;
data.adminEmail = null;
data.mailDnsRecordIds = [ ];
data.boxVersionsUrl = null;
data.version = null;
data.isCustomDomain = false;
+29 -27
View File
@@ -25,8 +25,6 @@ var gAutoupdaterJob = null,
gDockerVolumeCleanerJob = null,
gSchedulerSyncJob = null;
var gInitialized = false;
var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
// cron format
@@ -40,14 +38,19 @@ var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
if (gInitialized) return callback();
gHeartbeatJob = new CronJob({
cronTime: '00 */1 * * * *', // every minute
onTick: cloudron.sendHeartbeat,
start: true
});
cloudron.sendHeartbeat(); // latest unpublished version of CronJob has runOnInit
settings.events.on(settings.TIME_ZONE_KEY, recreateJobs);
settings.events.on(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
gInitialized = true;
recreateJobs(callback);
if (cloudron.isConfiguredSync()) {
recreateJobs(callback);
} else {
cloudron.events.on(cloudron.EVENT_ACTIVATED, recreateJobs);
callback();
}
}
function recreateJobs(unusedTimeZone, callback) {
@@ -56,14 +59,6 @@ function recreateJobs(unusedTimeZone, callback) {
settings.getAll(function (error, allSettings) {
debug('Creating jobs with timezone %s', allSettings[settings.TIME_ZONE_KEY]);
if (gHeartbeatJob) gHeartbeatJob.stop();
gHeartbeatJob = new CronJob({
cronTime: '00 */1 * * * *', // every minute
onTick: cloudron.sendHeartbeat,
start: true,
timeZone: allSettings[settings.TIME_ZONE_KEY]
});
if (gBackupJob) gBackupJob.stop();
gBackupJob = new CronJob({
cronTime: '00 00 */4 * * *', // every 4 hours
@@ -112,14 +107,20 @@ function recreateJobs(unusedTimeZone, callback) {
timeZone: allSettings[settings.TIME_ZONE_KEY]
});
settings.events.removeListener(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
settings.events.on(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
autoupdatePatternChanged(allSettings[settings.AUTOUPDATE_PATTERN_KEY]);
settings.events.removeListener(settings.TIME_ZONE_KEY, recreateJobs);
settings.events.on(settings.TIME_ZONE_KEY, recreateJobs);
if (callback) callback();
});
}
function autoupdatePatternChanged(pattern) {
assert.strictEqual(typeof pattern, 'string');
assert(gBoxUpdateCheckerJob);
debug('Auto update pattern changed to %s', pattern);
@@ -149,33 +150,34 @@ function autoupdatePatternChanged(pattern) {
function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function');
if (!gInitialized) return callback();
cloudron.events.removeListener(cloudron.EVENT_ACTIVATED, recreateJobs);
settings.events.removeListener(settings.TIME_ZONE_KEY, recreateJobs);
settings.events.removeListener(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
if (gAutoupdaterJob) gAutoupdaterJob.stop();
gAutoupdaterJob = null;
gBoxUpdateCheckerJob.stop();
if (gBoxUpdateCheckerJob) gBoxUpdateCheckerJob.stop();
gBoxUpdateCheckerJob = null;
gAppUpdateCheckerJob.stop();
if (gAppUpdateCheckerJob) gAppUpdateCheckerJob.stop();
gAppUpdateCheckerJob = null;
gHeartbeatJob.stop();
if (gHeartbeatJob) gHeartbeatJob.stop();
gHeartbeatJob = null;
gBackupJob.stop();
if (gBackupJob) gBackupJob.stop();
gBackupJob = null;
gCleanupTokensJob.stop();
if (gCleanupTokensJob) gCleanupTokensJob.stop();
gCleanupTokensJob = null;
gDockerVolumeCleanerJob.stop();
if (gDockerVolumeCleanerJob) gDockerVolumeCleanerJob.stop();
gDockerVolumeCleanerJob = null;
gSchedulerSyncJob.stop();
if (gSchedulerSyncJob) gSchedulerSyncJob.stop();
gSchedulerSyncJob = null;
gInitialized = false;
callback();
}
-46
View File
@@ -1,46 +0,0 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
checkPtrRecord: checkPtrRecord
};
var assert = require('assert'),
debug = require('debug')('box:digitalocean'),
dns = require('native-dns');
function checkPtrRecord(ip, fqdn, callback) {
assert(ip === null || typeof ip === 'string');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof callback, 'function');
debug('checkPtrRecord: ' + ip);
if (!ip) return callback(new Error('Network down'));
dns.resolve4('ns1.digitalocean.com', function (error, rdnsIps) {
if (error || rdnsIps.length === 0) return callback(new Error('Failed to query DO DNS'));
var reversedIp = ip.split('.').reverse().join('.');
var req = dns.Request({
question: dns.Question({ name: reversedIp + '.in-addr.arpa', type: 'PTR' }),
server: { address: rdnsIps[0] },
timeout: 5000
});
req.on('timeout', function () { return callback(new Error('Timedout')); });
req.on('message', function (error, message) {
if (error || !message.answer || message.answer.length === 0) return callback(new Error('Failed to query PTR'));
debug('checkPtrRecord: Actual:%s Expecting:%s', message.answer[0].data, fqdn);
callback(null, message.answer[0].data === fqdn);
});
req.send();
});
}
+55 -15
View File
@@ -3,32 +3,35 @@
'use strict';
exports = module.exports = {
addSubdomain: addSubdomain,
delSubdomain: delSubdomain,
getChangeStatus: getChangeStatus
add: add,
del: del,
update: update,
getChangeStatus: getChangeStatus,
get: get
};
var assert = require('assert'),
config = require('./config.js'),
debug = require('debug')('box:caas'),
SubdomainError = require('./subdomainerror.js'),
config = require('../config.js'),
debug = require('debug')('box:dns/caas'),
SubdomainError = require('../subdomains.js').SubdomainError,
superagent = require('superagent'),
util = require('util');
util = require('util'),
_ = require('underscore');
function addSubdomain(zoneName, subdomain, type, value, callback) {
function add(zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
var fqdn = subdomain !== '' && type === 'TXT' ? subdomain + '.' + config.fqdn() : config.appFqdn(subdomain);
debug('addSubdomain: zoneName: %s subdomain: %s type: %s value: %s fqdn: %s', zoneName, subdomain, type, value, fqdn);
debug('add: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
var data = {
type: type,
value: value
values: values
};
superagent
@@ -44,18 +47,55 @@ function addSubdomain(zoneName, subdomain, type, value, callback) {
});
}
function delSubdomain(zoneName, subdomain, type, value, callback) {
function get(zoneName, subdomain, type, callback) {
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert.strictEqual(typeof callback, 'function');
debug('delSubdomain: %s for domain %s.', subdomain, zoneName);
var fqdn = subdomain !== '' && type === 'TXT' ? subdomain + '.' + config.fqdn() : config.appFqdn(subdomain);
debug('get: zoneName: %s subdomain: %s type: %s fqdn: %s', zoneName, subdomain, type, fqdn);
superagent
.get(config.apiServerOrigin() + '/api/v1/domains/' + fqdn)
.query({ token: config.token(), type: type })
.end(function (error, result) {
if (error) return callback(error);
if (result.status !== 200) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
return callback(null, result.body.values);
});
}
function update(zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
get(zoneName, subdomain, type, function (error, result) {
if (error) return callback(error);
if (_.isEqual(values, result)) return callback();
add(zoneName, subdomain, type, values, callback);
});
}
function del(zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug('add: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
var data = {
type: type,
value: value
values: values
};
superagent
+81 -131
View File
@@ -3,103 +3,37 @@
'use strict';
exports = module.exports = {
getSignedUploadUrl: getSignedUploadUrl,
getSignedDownloadUrl: getSignedDownloadUrl,
addSubdomain: addSubdomain,
delSubdomain: delSubdomain,
getChangeStatus: getChangeStatus,
copyObject: copyObject
add: add,
get: get,
del: del,
update: update,
getChangeStatus: getChangeStatus
};
var assert = require('assert'),
AWS = require('aws-sdk'),
config = require('./config.js'),
debug = require('debug')('box:aws'),
SubdomainError = require('./subdomainerror.js'),
superagent = require('superagent');
config = require('../config.js'),
debug = require('debug')('box:dns/route53'),
settings = require('../settings.js'),
SubdomainError = require('../subdomains.js').SubdomainError,
util = require('util'),
_ = require('underscore');
function getAWSCredentials(callback) {
function getDnsCredentials(callback) {
assert.strictEqual(typeof callback, 'function');
// CaaS
if (config.token()) {
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/awscredentials';
superagent.post(url).query({ token: config.token() }).end(function (error, result) {
if (error) return callback(error);
if (result.statusCode !== 201) return callback(new Error(result.text));
if (!result.body || !result.body.credentials) return callback(new Error('Unexpected response'));
var credentials = {
accessKeyId: result.body.credentials.AccessKeyId,
secretAccessKey: result.body.credentials.SecretAccessKey,
sessionToken: result.body.credentials.SessionToken,
region: 'us-east-1'
};
if (config.aws().endpoint) credentials.endpoint = new AWS.Endpoint(config.aws().endpoint);
callback(null, credentials);
});
} else {
if (!config.aws().accessKeyId || !config.aws().secretAccessKey) return callback(new SubdomainError(SubdomainError.MISSING_CREDENTIALS));
settings.getDnsConfig(function (error, dnsConfig) {
if (error) return callback(new SubdomainError(SubdomainError.INTERNAL_ERROR, error));
var credentials = {
accessKeyId: config.aws().accessKeyId,
secretAccessKey: config.aws().secretAccessKey,
region: 'us-east-1'
accessKeyId: dnsConfig.accessKeyId,
secretAccessKey: dnsConfig.secretAccessKey,
region: dnsConfig.region
};
if (config.aws().endpoint) credentials.endpoint = new AWS.Endpoint(config.aws().endpoint);
if (dnsConfig.endpoint) credentials.endpoint = new AWS.Endpoint(dnsConfig.endpoint);
callback(null, credentials);
}
}
function getSignedUploadUrl(filename, callback) {
assert.strictEqual(typeof filename, 'string');
assert.strictEqual(typeof callback, 'function');
debug('getSignedUploadUrl: %s', filename);
getAWSCredentials(function (error, credentials) {
if (error) return callback(error);
var s3 = new AWS.S3(credentials);
var params = {
Bucket: config.aws().backupBucket,
Key: config.aws().backupPrefix + '/' + filename,
Expires: 60 * 30 /* 30 minutes */
};
var url = s3.getSignedUrl('putObject', params);
callback(null, { url : url, sessionToken: credentials.sessionToken });
});
}
function getSignedDownloadUrl(filename, callback) {
assert.strictEqual(typeof filename, 'string');
assert.strictEqual(typeof callback, 'function');
debug('getSignedDownloadUrl: %s', filename);
getAWSCredentials(function (error, credentials) {
if (error) return callback(error);
var s3 = new AWS.S3(credentials);
var params = {
Bucket: config.aws().backupBucket,
Key: config.aws().backupPrefix + '/' + filename,
Expires: 60 * 30 /* 30 minutes */
};
var url = s3.getSignedUrl('getObject', params);
callback(null, { url: url, sessionToken: credentials.sessionToken });
});
}
@@ -107,9 +41,7 @@ function getZoneByName(zoneName, callback) {
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof callback, 'function');
debug('getZoneByName: %s', zoneName);
getAWSCredentials(function (error, credentials) {
getDnsCredentials(function (error, credentials) {
if (error) return callback(error);
var route53 = new AWS.Route53(credentials);
@@ -122,26 +54,26 @@ function getZoneByName(zoneName, callback) {
if (!zone) return callback(new SubdomainError(SubdomainError.NOT_FOUND, 'no such zone'));
debug('getZoneByName: found zone', zone);
callback(null, zone);
});
});
}
function addSubdomain(zoneName, subdomain, type, value, callback) {
function add(zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug('addSubdomain: ' + subdomain + ' for domain ' + zoneName + ' with value ' + value);
debug('add: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
getZoneByName(zoneName, function (error, zone) {
if (error) return callback(error);
var fqdn = config.appFqdn(subdomain);
var records = values.map(function (v) { return { Value: v }; });
var params = {
ChangeBatch: {
Changes: [{
@@ -149,11 +81,7 @@ function addSubdomain(zoneName, subdomain, type, value, callback) {
ResourceRecordSet: {
Type: type,
Name: fqdn,
ResourceRecords: [{
Value: value
}],
Weight: 0,
SetIdentifier: fqdn,
ResourceRecords: records,
TTL: 1
}
}]
@@ -161,7 +89,7 @@ function addSubdomain(zoneName, subdomain, type, value, callback) {
HostedZoneId: zone.Id
};
getAWSCredentials(function (error, credentials) {
getDnsCredentials(function (error, credentials) {
if (error) return callback(error);
var route53 = new AWS.Route53(credentials);
@@ -172,35 +100,78 @@ function addSubdomain(zoneName, subdomain, type, value, callback) {
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
}
debug('addSubdomain: success. changeInfoId:%j', result);
callback(null, result.ChangeInfo.Id);
});
});
});
}
function delSubdomain(zoneName, subdomain, type, value, callback) {
function update(zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug('delSubdomain: %s for domain %s.', subdomain, zoneName);
get(zoneName, subdomain, type, function (error, result) {
if (error) return callback(error);
if (_.isEqual(values, result)) return callback();
add(zoneName, subdomain, type, values, callback);
});
}
function get(zoneName, subdomain, type, callback) {
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
getZoneByName(zoneName, function (error, zone) {
if (error) return callback(error);
var params = {
HostedZoneId: zone.Id,
MaxItems: '1',
StartRecordName: (subdomain ? subdomain + '.' : '') + zoneName + '.',
StartRecordType: type
};
getDnsCredentials(function (error, credentials) {
if (error) return callback(error);
var route53 = new AWS.Route53(credentials);
route53.listResourceRecordSets(params, function (error, result) {
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
if (result.ResourceRecordSets.length === 0) return callback(null, [ ]);
if (result.ResourceRecordSets[0].Name !== params.StartRecordName && result.ResourceRecordSets[0].Type !== params.StartRecordType) return callback(null, [ ]);
var values = result.ResourceRecordSets[0].ResourceRecords.map(function (record) { return record.Value; });
callback(null, values);
});
});
});
}
function del(zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
getZoneByName(zoneName, function (error, zone) {
if (error) return callback(error);
var fqdn = config.appFqdn(subdomain);
var records = values.map(function (v) { return { Value: v }; });
var resourceRecordSet = {
Name: fqdn,
Type: type,
ResourceRecords: [{
Value: value
}],
Weight: 0,
SetIdentifier: fqdn,
ResourceRecords: records,
TTL: 1
};
@@ -214,7 +185,7 @@ function delSubdomain(zoneName, subdomain, type, value, callback) {
HostedZoneId: zone.Id
};
getAWSCredentials(function (error, credentials) {
getDnsCredentials(function (error, credentials) {
if (error) return callback(error);
var route53 = new AWS.Route53(credentials);
@@ -236,8 +207,6 @@ function delSubdomain(zoneName, subdomain, type, value, callback) {
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
}
debug('delSubdomain: success');
callback(null);
});
});
@@ -250,7 +219,7 @@ function getChangeStatus(changeId, callback) {
if (changeId === '') return callback(null, 'INSYNC');
getAWSCredentials(function (error, credentials) {
getDnsCredentials(function (error, credentials) {
if (error) return callback(error);
var route53 = new AWS.Route53(credentials);
@@ -261,22 +230,3 @@ function getChangeStatus(changeId, callback) {
});
});
}
function copyObject(from, to, callback) {
assert.strictEqual(typeof from, 'string');
assert.strictEqual(typeof to, 'string');
assert.strictEqual(typeof callback, 'function');
getAWSCredentials(function (error, credentials) {
if (error) return callback(error);
var params = {
Bucket: config.aws().backupBucket, // target bucket
Key: config.aws().backupPrefix + '/' + to, // target file
CopySource: config.aws().backupBucket + '/' + config.aws().backupPrefix + '/' + from, // source
};
var s3 = new AWS.S3(credentials);
s3.copyObject(params, callback);
});
}
+10 -5
View File
@@ -117,8 +117,9 @@ function downloadImage(manifest, callback) {
}, callback);
}
function createSubcontainer(app, cmd, callback) {
function createSubcontainer(app, name, cmd, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof name, 'string');
assert(!cmd || util.isArray(cmd));
assert.strictEqual(typeof callback, 'function');
@@ -129,8 +130,10 @@ function createSubcontainer(app, cmd, callback) {
var exposedPorts = {}, dockerPortBindings = { };
var stdEnv = [
'CLOUDRON=1',
'WEBADMIN_ORIGIN' + '=' + config.adminOrigin(),
'API_ORIGIN' + '=' + config.adminOrigin()
'WEBADMIN_ORIGIN=' + config.adminOrigin(),
'API_ORIGIN=' + config.adminOrigin(),
'APP_ORIGIN=https://' + config.appFqdn(app.location),
'APP_DOMAIN=' + config.appFqdn(app.location)
];
// docker portBindings requires ports to be exposed
@@ -155,7 +158,9 @@ function createSubcontainer(app, cmd, callback) {
if (error) return callback(new Error('Error getting addon environment : ' + error));
var containerOptions = {
Hostname: config.appFqdn(app.location),
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
Hostname: semver.gte(targetBoxVersion(app.manifest), '0.0.77') ? app.location : config.appFqdn(app.location),
Tty: isAppContainer,
Image: app.manifest.dockerImage,
Cmd: cmd,
@@ -198,7 +203,7 @@ function createSubcontainer(app, cmd, callback) {
}
function createContainer(app, callback) {
createSubcontainer(app, null, callback);
createSubcontainer(app, app.id /* name */, null /* cmd */, callback);
}
function startContainer(containerId, callback) {
+2 -2
View File
@@ -2,9 +2,9 @@
Dear <%= user.username %>,
I am excited to welcome you to my Cloudron <%= fqdn %>!
Welcome to my Cloudron <%= fqdn %>!
The Cloudron is our own Private Cloud. You can read more about it
The Cloudron is our own Smart Server. You can read more about it
at https://www.cloudron.io.
You username is '<%= user.username %>'
+61 -7
View File
@@ -25,16 +25,16 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
cloudron = require('./cloudron.js'),
config = require('./config.js'),
debug = require('debug')('box:mailer'),
digitalocean = require('./digitalocean.js'),
dns = require('native-dns'),
docker = require('./docker.js').connection,
ejs = require('ejs'),
nodemailer = require('nodemailer'),
path = require('path'),
safe = require('safetydance'),
smtpTransport = require('nodemailer-smtp-transport'),
sysinfo = require('./sysinfo.js'),
userdb = require('./userdb.js'),
util = require('util'),
_ = require('underscore');
@@ -48,13 +48,20 @@ var gMailQueue = [ ],
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
checkDns();
if (cloudron.isConfiguredSync()) {
checkDns();
} else {
cloudron.events.on(cloudron.EVENT_CONFIGURED, checkDns);
}
callback(null);
}
function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function');
cloudron.events.removeListener(cloudron.EVENT_CONFIGURED, checkDns);
// TODO: interrupt processQueue as well
clearTimeout(gCheckDnsTimerId);
gCheckDnsTimerId = null;
@@ -65,20 +72,67 @@ function uninitialize(callback) {
callback(null);
}
function getTxtRecords(callback) {
dns.resolveNs(config.zoneName(), function (error, nameservers) {
if (error || !nameservers) return callback(error || new Error('Unable to get nameservers'));
var nameserver = nameservers[0];
dns.resolve4(nameserver, function (error, nsIps) {
if (error || !nsIps || nsIps.length === 0) return callback(error);
var req = dns.Request({
question: dns.Question({ name: config.fqdn(), type: 'TXT' }),
server: { address: nsIps[0] },
timeout: 5000
});
req.on('timeout', function () { return callback(new Error('ETIMEOUT')); });
req.on('message', function (error, message) {
if (error || !message.answer || message.answer.length === 0) return callback(null, null);
var records = message.answer.map(function (a) { return a.data[0]; });
callback(null, records);
});
req.send();
});
});
}
function checkDns() {
digitalocean.checkPtrRecord(sysinfo.getIp(), config.fqdn(), function (error, ok) {
if (error || !ok) {
debug('PTR record not setup yet');
gCheckDnsTimerId = setTimeout(checkDns, 10000);
getTxtRecords(function (error, records) {
if (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;
}
var allowedToSendMail = false;
for (var i = 0; i < records.length; i++) {
if (records[i].indexOf('v=spf1 ') !== 0) continue; // not SPF
allowedToSendMail = records[i].indexOf('a:' + config.fqdn()) !== -1;
break; // only one SPF record can exist (https://support.google.com/a/answer/4568483?hl=en)
}
if (!allowedToSendMail) {
debug('checkDns: SPF records disallow sending email from cloudron. %j', records);
gCheckDnsTimerId = setTimeout(checkDns, 60000);
return;
}
debug('checkDns: SPF check passed. commencing mail processing');
gDnsReady = true;
processQueue();
});
}
function processQueue() {
assert(gDnsReady);
docker.getContainer('mail').inspect(function (error, data) {
if (error) return console.error(error);
+3
View File
@@ -6,6 +6,8 @@
<title> Cloudron Login </title>
<link href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
<!-- Custom Fonts -->
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" type="text/css">
<link href="https://fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet" type="text/css">
@@ -32,6 +34,7 @@
<div class="container-fluid">
<div class="navbar-header">
<span class="navbar-brand navbar-brand-icon"><img src="/api/v1/cloudron/avatar" width="40" height="40"/></span>
<span class="navbar-brand">Cloudron</span>
</div>
</div>
</nav>
+1 -2
View File
@@ -22,12 +22,11 @@ exports = module.exports = {
BOX_DATA_DIR: path.join(config.baseDir(), 'data/box'),
// this is not part of appdata because an icon may be set before install
APPICONS_DIR: path.join(config.baseDir(), 'data/box/appicons'),
APP_CERTS_DIR: path.join(config.baseDir(), 'data/box/certs'),
MAIL_DATA_DIR: path.join(config.baseDir(), 'data/box/mail'),
CLOUDRON_AVATAR_FILE: path.join(config.baseDir(), 'data/box/avatar.png'),
CLOUDRON_DEFAULT_AVATAR_FILE: path.join(__dirname + '/../assets/avatar.png'),
FAVICON_FILE: path.join(__dirname + '/../assets/favicon.ico'),
UPDATE_CHECKER_FILE: path.join(config.baseDir(), 'data/box/updatechecker.json')
};
+20 -7
View File
@@ -117,18 +117,23 @@ function installApp(req, res, next) {
if (typeof data.accessRestriction !== 'object') return next(new HttpError(400, 'accessRestriction is required'));
if (typeof data.oauthProxy !== 'boolean') return next(new HttpError(400, 'oauthProxy must be a boolean'));
if ('icon' in data && typeof data.icon !== 'string') return next(new HttpError(400, 'icon is not a string'));
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 (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'));
// allow tests to provide an appId for testing
var appId = (process.env.BOX_ENV === 'test' && typeof data.appId === 'string') ? data.appId : uuid.v4();
debug('Installing app id:%s storeid:%s loc:%s port:%j accessRestriction:%j oauthproxy:%s manifest:%j', appId, data.appStoreId, data.location, data.portBindings, data.accessRestriction, data.oauthProxy, data.manifest);
apps.install(appId, data.appStoreId, data.manifest, data.location, data.portBindings || null, data.accessRestriction, data.oauthProxy, data.icon || null, function (error) {
apps.install(appId, data.appStoreId, data.manifest, data.location, data.portBindings || null, data.accessRestriction, data.oauthProxy, data.icon || null, data.cert || null, data.key || null, function (error) {
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.BILLING_REQUIRED) return next(new HttpError(402, 'Billing required'));
if (error && error.reason === AppsError.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) return next(new HttpError(500, error));
@@ -154,16 +159,21 @@ function configureApp(req, res, next) {
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 (typeof data.oauthProxy !== 'boolean') return next(new HttpError(400, 'oauthProxy must be a boolean'));
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 (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'));
debug('Configuring app id:%s location:%s bindings:%j accessRestriction:%j oauthProxy:%s', req.params.id, data.location, data.portBindings, data.accessRestriction, data.oauthProxy);
apps.configure(req.params.id, data.location, data.portBindings || null, data.accessRestriction, data.oauthProxy, function (error) {
apps.configure(req.params.id, data.location, data.portBindings || null, data.accessRestriction, data.oauthProxy, data.cert || null, data.key || null, function (error) {
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.BAD_CERTIFICATE) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, { }));
@@ -276,14 +286,14 @@ function getLogStream(req, res, next) {
debug('Getting logstream of app id:%s', req.params.id);
var fromLine = req.query.fromLine ? parseInt(req.query.fromLine, 10) : -10; // we ignore last-event-id
if (isNaN(fromLine)) return next(new HttpError(400, 'fromLine must be a valid number'));
var lines = req.query.lines ? parseInt(req.query.lines, 10) : -10; // we ignore last-event-id
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a valid number'));
function sse(id, data) { return 'id: ' + id + '\ndata: ' + data + '\n\n'; }
if (req.headers.accept !== 'text/event-stream') return next(new HttpError(400, 'This API call requires EventStream'));
apps.getLogStream(req.params.id, fromLine, function (error, logStream) {
apps.getLogs(req.params.id, lines, true /* follow */, function (error, logStream) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(412, error.message));
if (error) return next(new HttpError(500, error));
@@ -299,7 +309,7 @@ function getLogStream(req, res, next) {
res.on('close', logStream.close);
logStream.on('data', function (data) {
var obj = JSON.parse(data);
res.write(sse(obj.lineNumber, JSON.stringify(obj)));
res.write(sse(obj.monotonicTimestamp, JSON.stringify(obj))); // send timestamp as id
});
logStream.on('end', res.end.bind(res));
logStream.on('error', res.end.bind(res, null));
@@ -309,9 +319,12 @@ function getLogStream(req, res, next) {
function getLogs(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
var lines = req.query.lines ? parseInt(req.query.lines, 10) : 100;
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a number'));
debug('Getting logs of app id:%s', req.params.id);
apps.getLogs(req.params.id, function (error, logStream) {
apps.getLogs(req.params.id, lines, false /* follow */, function (error, logStream) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(412, error.message));
if (error) return next(new HttpError(500, error));
-19
View File
@@ -11,7 +11,6 @@ exports = module.exports = {
getConfig: getConfig,
update: update,
migrate: migrate,
setCertificate: setCertificate,
feedback: feedback
};
@@ -25,7 +24,6 @@ var assert = require('assert'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
superagent = require('superagent'),
safe = require('safetydance'),
updateChecker = require('../updatechecker.js');
/**
@@ -140,23 +138,6 @@ function migrate(req, res, next) {
});
}
function setCertificate(req, res, next) {
assert.strictEqual(typeof req.files, 'object');
if (!req.files.certificate) return next(new HttpError(400, 'certificate must be provided'));
var certificate = safe.fs.readFileSync(req.files.certificate.path, 'utf8');
if (!req.files.key) return next(new HttpError(400, 'key must be provided'));
var key = safe.fs.readFileSync(req.files.key.path, 'utf8');
cloudron.setCertificate(certificate, key, function (error) {
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
});
}
function feedback(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
+58 -1
View File
@@ -10,7 +10,13 @@ exports = module.exports = {
setCloudronName: setCloudronName,
getCloudronAvatar: getCloudronAvatar,
setCloudronAvatar: setCloudronAvatar
setCloudronAvatar: setCloudronAvatar,
getDnsConfig: getDnsConfig,
setDnsConfig: setDnsConfig,
setCertificate: setCertificate,
setAdminCertificate: setAdminCertificate
};
var assert = require('assert'),
@@ -83,3 +89,54 @@ function getCloudronAvatar(req, res, next) {
res.status(200).send(avatar);
});
}
function getDnsConfig(req, res, next) {
settings.getDnsConfig(function (error, config) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, config));
});
}
function setDnsConfig(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required'));
settings.setDnsConfig(req.body, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
});
}
// default fallback cert
function setCertificate(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!req.body.cert || typeof req.body.cert !== 'string') return next(new HttpError(400, 'cert must be a string'));
if (!req.body.key || typeof req.body.key !== 'string') return next(new HttpError(400, 'key must be a string'));
settings.setCertificate(req.body.cert, req.body.key, function (error) {
if (error && error.reason === SettingsError.INVALID_CERT) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
});
}
// only webadmin cert, until it can be treated just like a normal app
function setAdminCertificate(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!req.body.cert || typeof req.body.cert !== 'string') return next(new HttpError(400, 'cert must be a string'));
if (!req.body.key || typeof req.body.key !== 'string') return next(new HttpError(400, 'key must be a string'));
settings.setAdminCertificate(req.body.cert, req.body.key, function (error) {
if (error && error.reason === SettingsError.INVALID_CERT) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
});
}
+73 -17
View File
@@ -41,7 +41,7 @@ var SERVER_URL = 'http://localhost:' + config.get('port');
// Test image information
var TEST_IMAGE_REPO = 'cloudron/test';
var TEST_IMAGE_TAG = '9.0.0';
var TEST_IMAGE_TAG = '10.0.0';
var TEST_IMAGE_ID = child_process.execSync('docker inspect --format={{.Id}} ' + TEST_IMAGE_REPO + ':' + TEST_IMAGE_TAG).toString('utf8').trim();
var APP_STORE_ID = 'test', APP_ID;
@@ -61,7 +61,7 @@ var USERNAME_1 = 'user', PASSWORD_1 = 'password', EMAIL_1 ='user@me.com';
var token = null; // authentication token
var token_1 = null;
var awsHostedZones = {
var awsHostedZones = {
HostedZones: [{
Id: '/hostedzone/ZONEID',
Name: 'localhost.',
@@ -584,15 +584,14 @@ describe('App installation', function () {
function (callback) {
apiHockInstance
.get('/api/v1/apps/' + APP_STORE_ID + '/versions/' + APP_MANIFEST.version + '/icon')
.replyWithFile(200, path.resolve(__dirname, '../../../webadmin/src/img/appicon_fallback.png'))
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=APPSTORE_TOKEN')
.max(Infinity)
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } }, { 'Content-Type': 'application/json' });
.replyWithFile(200, path.resolve(__dirname, '../../../webadmin/src/img/appicon_fallback.png'));
var port = parseInt(url.parse(config.apiServerOrigin()).port, 10);
apiHockServer = http.createServer(apiHockInstance.handler).listen(port, callback);
},
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }),
function (callback) {
awsHockInstance
.get('/2013-04-01/hostedzone')
@@ -603,8 +602,7 @@ describe('App installation', function () {
.max(Infinity)
.reply(200, js2xml('ChangeResourceRecordSetsResponse', { ChangeInfo: { Id: 'dnsrecordid', Status: 'INSYNC' } }), { 'Content-Type': 'application/xml' });
var port = parseInt(url.parse(config.aws().endpoint).port, 10);
awsHockServer = http.createServer(awsHockInstance.handler).listen(port, callback);
awsHockServer = http.createServer(awsHockInstance.handler).listen(5353, callback);
}
], done);
});
@@ -672,6 +670,9 @@ describe('App installation', function () {
expect(data.Config.Env).to.contain('WEBADMIN_ORIGIN=' + config.adminOrigin());
expect(data.Config.Env).to.contain('API_ORIGIN=' + config.adminOrigin());
expect(data.Config.Env).to.contain('CLOUDRON=1');
expect(data.Config.Env).to.contain('APP_ORIGIN=https://' + config.appFqdn(APP_LOCATION));
expect(data.Config.Env).to.contain('APP_DOMAIN=' + config.appFqdn(APP_LOCATION));
expect(data.Config.Hostname).to.be(APP_LOCATION);
clientdb.getByAppIdAndType(appResult.id, clientdb.TYPE_OAUTH, function (error, client) {
expect(error).to.not.be.ok();
expect(client.id.length).to.be(40); // cid- + 32 hex chars (128 bits) + 4 hyphens
@@ -837,7 +838,7 @@ describe('App installation', function () {
}, done);
});
it('logs - stdout and stderr', function (done) {
xit('logs - stdout and stderr', function (done) {
request.get(SERVER_URL + '/api/v1/apps/' + APP_ID + '/logs')
.query({ access_token: token })
.end(function (err, res) {
@@ -851,7 +852,7 @@ describe('App installation', function () {
});
});
it('logStream - requires event-stream accept header', function (done) {
xit('logStream - requires event-stream accept header', function (done) {
request.get(SERVER_URL + '/api/v1/apps/' + APP_ID + '/logstream')
.query({ access_token: token, fromLine: 0 })
.end(function (err, res) {
@@ -861,7 +862,7 @@ describe('App installation', function () {
});
it('logStream - stream logs', function (done) {
xit('logStream - stream logs', function (done) {
var options = {
port: config.get('port'), host: 'localhost', path: '/api/v1/apps/' + APP_ID + '/logstream?access_token=' + token,
headers: { 'Accept': 'text/event-stream', 'Connection': 'keep-alive' }
@@ -1020,8 +1021,15 @@ describe('App installation - port bindings', function () {
var awsHockInstance = hock.createHock({ throwOnUnmatched: false }), awsHockServer;
var imageDeleted = false, imageCreated = false;
// *.foobar.com
var validCert1 = '-----BEGIN CERTIFICATE-----\nMIIBvjCCAWgCCQCg957GWuHtbzANBgkqhkiG9w0BAQsFADBmMQswCQYDVQQGEwJE\nRTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEDAOBgNVBAoMB05l\nYnVsb24xDDAKBgNVBAsMA0NUTzEVMBMGA1UEAwwMKi5mb29iYXIuY29tMB4XDTE1\nMTAyODEzMDI1MFoXDTE2MTAyNzEzMDI1MFowZjELMAkGA1UEBhMCREUxDzANBgNV\nBAgMBkJlcmxpbjEPMA0GA1UEBwwGQmVybGluMRAwDgYDVQQKDAdOZWJ1bG9uMQww\nCgYDVQQLDANDVE8xFTATBgNVBAMMDCouZm9vYmFyLmNvbTBcMA0GCSqGSIb3DQEB\nAQUAA0sAMEgCQQC0FKf07ZWMcABFlZw+GzXK9EiZrlJ1lpnu64RhN99z7MXRr8cF\nnZVgY3jgatuyR5s3WdzUvye2eJ0rNicl2EZJAgMBAAEwDQYJKoZIhvcNAQELBQAD\nQQAw4bteMZAeJWl2wgNLw+wTwAH96E0jyxwreCnT5AxJLmgimyQ0XOF4FsssdRFj\nxD9WA+rktelBodJyPeTDNhIh\n-----END CERTIFICATE-----';
var validKey1 = '-----BEGIN RSA PRIVATE KEY-----\nMIIBOQIBAAJBALQUp/TtlYxwAEWVnD4bNcr0SJmuUnWWme7rhGE333PsxdGvxwWd\nlWBjeOBq27JHmzdZ3NS/J7Z4nSs2JyXYRkkCAwEAAQJALV2eykcoC48TonQEPmkg\nbhaIS57syw67jMLsQImQ02UABKzqHPEKLXPOZhZPS9hsC/hGIehwiYCXMUlrl+WF\nAQIhAOntBI6qaecNjAAVG7UbZclMuHROUONmZUF1KNq6VyV5AiEAxRLkfHWy52CM\njOQrX347edZ30f4QczvugXwsyuU9A1ECIGlGZ8Sk4OBA8n6fAUcyO06qnmCJVlHg\npTUeOvKk5c9RAiBs28+8dCNbrbhVhx/yQr9FwNM0+ttJW/yWJ+pyNQhr0QIgJTT6\nxwCWYOtbioyt7B9l+ENy3AMSO3Uq+xmIKkvItK4=\n-----END RSA PRIVATE KEY-----';
before(function (done) {
config.set('fqdn', 'test.foobar.com');
APP_ID = uuid.v4();
async.series([
function (callback) {
dockerProxy = startDockerProxy(function interceptor(req, res) {
@@ -1045,15 +1053,14 @@ describe('App installation - port bindings', function () {
function (callback) {
apiHockInstance
.get('/api/v1/apps/' + APP_STORE_ID + '/versions/' + APP_MANIFEST.version + '/icon')
.replyWithFile(200, path.resolve(__dirname, '../../../webadmin/src/img/appicon_fallback.png'))
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=APPSTORE_TOKEN')
.max(Infinity)
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } }, { 'Content-Type': 'application/json' });
.replyWithFile(200, path.resolve(__dirname, '../../../webadmin/src/img/appicon_fallback.png'));
var port = parseInt(url.parse(config.apiServerOrigin()).port, 10);
apiHockServer = http.createServer(apiHockInstance.handler).listen(port, callback);
},
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }),
function (callback) {
awsHockInstance
.get('/2013-04-01/hostedzone')
@@ -1064,8 +1071,7 @@ describe('App installation - port bindings', function () {
.max(Infinity)
.reply(200, js2xml('ChangeResourceRecordSetsResponse', { ChangeInfo: { Id: 'dnsrecordid', Status: 'INSYNC' } }), { 'Content-Type': 'application/xml' });
var port = parseInt(url.parse(config.aws().endpoint).port, 10);
awsHockServer = http.createServer(awsHockInstance.handler).listen(port, callback);
awsHockServer = http.createServer(awsHockInstance.handler).listen(5353, callback);
}
], done);
});
@@ -1285,6 +1291,46 @@ describe('App installation - port bindings', function () {
});
});
it('cannot reconfigure app with only the cert, no key', function (done) {
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token })
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null, oauthProxy: true, cert: validCert1 })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('cannot reconfigure app with only the key, no cert', function (done) {
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token })
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null, oauthProxy: true, key: validKey1 })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('cannot reconfigure app with cert not bein a string', function (done) {
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token })
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null, oauthProxy: true, cert: 1234, key: validKey1 })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('cannot reconfigure app with key not bein a string', function (done) {
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token })
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null, oauthProxy: true, cert: validCert1, key: 1234 })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('non admin cannot reconfigure app', function (done) {
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token_1 })
@@ -1379,6 +1425,16 @@ describe('App installation - port bindings', function () {
}, done);
});
it('can reconfigure app with custom certificate', function (done) {
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token })
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null, oauthProxy: true, cert: validCert1, key: validKey1 })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
checkConfigureStatus(0, done);
});
});
it('can stop app', function (done) {
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/stop')
.query({ access_token: token })
+5 -103
View File
@@ -10,11 +10,7 @@ var async = require('async'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
fs = require('fs'),
os = require('os'),
path = require('path'),
nock = require('nock'),
paths = require('../../paths.js'),
request = require('superagent'),
server = require('../../server.js'),
shell = require('../../shell.js');
@@ -28,6 +24,7 @@ var server;
function setup(done) {
nock.cleanAll();
config.set('version', '0.5.0');
config.set('fqdn', 'localhost');
server.start(done);
}
@@ -167,101 +164,6 @@ describe('Cloudron', function () {
});
});
describe('Certificates API', function () {
var certFile, keyFile;
before(function (done) {
certFile = path.join(os.tmpdir(), 'host.cert');
fs.writeFileSync(certFile, 'test certificate');
keyFile = path.join(os.tmpdir(), 'host.key');
fs.writeFileSync(keyFile, 'test key');
async.series([
setup,
function (callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
request.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result).to.be.ok();
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
// stash token for further use
token = result.body.token;
callback();
});
},
], done);
});
after(function (done) {
fs.unlinkSync(certFile);
fs.unlinkSync(keyFile);
cleanup(done);
});
it('cannot set certificate without token', function (done) {
request.post(SERVER_URL + '/api/v1/cloudron/certificate')
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
});
it('cannot set certificate without certificate', function (done) {
request.post(SERVER_URL + '/api/v1/cloudron/certificate')
.query({ access_token: token })
.attach('key', keyFile, 'key')
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('cannot set certificate without key', function (done) {
request.post(SERVER_URL + '/api/v1/cloudron/certificate')
.query({ access_token: token })
.attach('certificate', certFile, 'certificate')
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('can set certificate', function (done) {
request.post(SERVER_URL + '/api/v1/cloudron/certificate')
.query({ access_token: token })
.attach('key', keyFile, 'key')
.attach('certificate', certFile, 'certificate')
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(202);
done();
});
});
it('did set the certificate', function (done) {
var cert = fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'));
expect(cert).to.eql(fs.readFileSync(certFile));
var key = fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'));
expect(key).to.eql(fs.readFileSync(keyFile));
done();
});
});
describe('get config', function () {
before(function (done) {
async.series([
@@ -310,11 +212,11 @@ describe('Cloudron', function () {
expect(result.statusCode).to.equal(200);
expect(result.body.apiServerOrigin).to.eql('http://localhost:6060');
expect(result.body.webServerOrigin).to.eql(null);
expect(result.body.fqdn).to.eql('localhost');
expect(result.body.fqdn).to.eql(config.fqdn());
expect(result.body.isCustomDomain).to.eql(false);
expect(result.body.progress).to.be.an('object');
expect(result.body.update).to.be.an('object');
expect(result.body.version).to.eql('0.5.0');
expect(result.body.version).to.eql(config.version());
expect(result.body.developerMode).to.be.a('boolean');
expect(result.body.size).to.eql(null);
expect(result.body.region).to.eql(null);
@@ -335,11 +237,11 @@ describe('Cloudron', function () {
expect(result.statusCode).to.equal(200);
expect(result.body.apiServerOrigin).to.eql('http://localhost:6060');
expect(result.body.webServerOrigin).to.eql(null);
expect(result.body.fqdn).to.eql('localhost');
expect(result.body.fqdn).to.eql(config.fqdn());
expect(result.body.isCustomDomain).to.eql(false);
expect(result.body.progress).to.be.an('object');
expect(result.body.update).to.be.an('object');
expect(result.body.version).to.eql('0.5.0');
expect(result.body.version).to.eql(config.version());
expect(result.body.developerMode).to.be.a('boolean');
expect(result.body.size).to.eql('1gb');
expect(result.body.region).to.eql('sfo');
+141 -2
View File
@@ -11,6 +11,7 @@ var appdb = require('../../appdb.js'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
path = require('path'),
paths = require('../../paths.js'),
request = require('superagent'),
server = require('../../server.js'),
@@ -26,6 +27,8 @@ var token = null;
var server;
function setup(done) {
config.set('fqdn', 'foobar.com');
async.series([
server.start.bind(server),
@@ -212,7 +215,7 @@ describe('Settings API', function () {
it('set succeeds', function (done) {
request.post(SERVER_URL + '/api/v1/settings/cloudron_avatar')
.query({ access_token: token })
.attach('avatar', paths.FAVICON_FILE)
.attach('avatar', paths.CLOUDRON_DEFAULT_AVATAR_FILE)
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
done();
@@ -224,10 +227,146 @@ describe('Settings API', function () {
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.toString()).to.eql(fs.readFileSync(paths.FAVICON_FILE, 'utf-8'));
expect(res.body.toString()).to.eql(fs.readFileSync(paths.CLOUDRON_DEFAULT_AVATAR_FILE, 'utf-8'));
done(err);
});
});
});
describe('dns_config', function () {
it('get dns_config fails', function (done) {
request.get(SERVER_URL + '/api/v1/settings/dns_config')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body).to.eql({});
done(err);
});
});
it('cannot set without data', function (done) {
request.post(SERVER_URL + '/api/v1/settings/dns_config')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('set succeeds', function (done) {
request.post(SERVER_URL + '/api/v1/settings/dns_config')
.query({ access_token: token })
.send({ provider: 'route53', accessKeyId: 'accessKey', secretAccessKey: 'secretAccessKey' })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
done();
});
});
it('get succeeds', function (done) {
request.get(SERVER_URL + '/api/v1/settings/dns_config')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body).to.eql({ provider: 'route53', accessKeyId: 'accessKey', secretAccessKey: 'secretAccessKey', region: 'us-east-1', endpoint: null });
done(err);
});
});
});
describe('Certificates API', function () {
// foobar.com
var validCert0 = '-----BEGIN CERTIFICATE-----\nMIIBujCCAWQCCQCjLyTKzAJ4FDANBgkqhkiG9w0BAQsFADBkMQswCQYDVQQGEwJE\nRTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEDAOBgNVBAoMB05l\nYnVsb24xDDAKBgNVBAsMA0NUTzETMBEGA1UEAwwKZm9vYmFyLmNvbTAeFw0xNTEw\nMjgxMjM5MjZaFw0xNjEwMjcxMjM5MjZaMGQxCzAJBgNVBAYTAkRFMQ8wDQYDVQQI\nDAZCZXJsaW4xDzANBgNVBAcMBkJlcmxpbjEQMA4GA1UECgwHTmVidWxvbjEMMAoG\nA1UECwwDQ1RPMRMwEQYDVQQDDApmb29iYXIuY29tMFwwDQYJKoZIhvcNAQEBBQAD\nSwAwSAJBAMeYofgwHeNVmGkGe0gj4dnX2ciifDi7X2K/oVHp7mxuHjGMSYP9Z7b6\n+mu0IMf4OedwXStHBeO8mwjKxZmE7p8CAwEAATANBgkqhkiG9w0BAQsFAANBAJI7\nFUUHXjR63UFk8pgxp0c7hEGqj4VWWGsmo8oZnnX8jGVmQDKbk8o3MtDujfqupmMR\nMo7tSAFlG7zkm3GYhpw=\n-----END CERTIFICATE-----';
var validKey0 = '-----BEGIN RSA PRIVATE KEY-----\nMIIBOwIBAAJBAMeYofgwHeNVmGkGe0gj4dnX2ciifDi7X2K/oVHp7mxuHjGMSYP9\nZ7b6+mu0IMf4OedwXStHBeO8mwjKxZmE7p8CAwEAAQJBAJS59Sb8o6i8JT9NJxvQ\nMQCkSJGqEaosZJ0uccSZ7aE48v+H7HiPzXAueitohcEif2Wp1EZ1RbRMURhznNiZ\neLECIQDxxqhakO6wc7H68zmpRXJ5ZxGUNbM24AMtpONAtEw9iwIhANNWtp6P74OV\ntvfOmtubbqw768fmGskFCOcp5oF8oF29AiBkTAf9AhCyjFwyAYJTEScq67HkLN66\njfVjkvpfFixmfwIgI+xldmZ5DCDyzQSthg7RrS0yUvRmMS1N6h1RNUl96PECIQDl\nit4lFcytbqNo1PuBZvzQE+plCjiJqXHYo3WCst1Jbg==\n-----END RSA PRIVATE KEY-----';
// *.foobar.com
var validCert1 = '-----BEGIN CERTIFICATE-----\nMIIBvjCCAWgCCQCg957GWuHtbzANBgkqhkiG9w0BAQsFADBmMQswCQYDVQQGEwJE\nRTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEDAOBgNVBAoMB05l\nYnVsb24xDDAKBgNVBAsMA0NUTzEVMBMGA1UEAwwMKi5mb29iYXIuY29tMB4XDTE1\nMTAyODEzMDI1MFoXDTE2MTAyNzEzMDI1MFowZjELMAkGA1UEBhMCREUxDzANBgNV\nBAgMBkJlcmxpbjEPMA0GA1UEBwwGQmVybGluMRAwDgYDVQQKDAdOZWJ1bG9uMQww\nCgYDVQQLDANDVE8xFTATBgNVBAMMDCouZm9vYmFyLmNvbTBcMA0GCSqGSIb3DQEB\nAQUAA0sAMEgCQQC0FKf07ZWMcABFlZw+GzXK9EiZrlJ1lpnu64RhN99z7MXRr8cF\nnZVgY3jgatuyR5s3WdzUvye2eJ0rNicl2EZJAgMBAAEwDQYJKoZIhvcNAQELBQAD\nQQAw4bteMZAeJWl2wgNLw+wTwAH96E0jyxwreCnT5AxJLmgimyQ0XOF4FsssdRFj\nxD9WA+rktelBodJyPeTDNhIh\n-----END CERTIFICATE-----';
var validKey1 = '-----BEGIN RSA PRIVATE KEY-----\nMIIBOQIBAAJBALQUp/TtlYxwAEWVnD4bNcr0SJmuUnWWme7rhGE333PsxdGvxwWd\nlWBjeOBq27JHmzdZ3NS/J7Z4nSs2JyXYRkkCAwEAAQJALV2eykcoC48TonQEPmkg\nbhaIS57syw67jMLsQImQ02UABKzqHPEKLXPOZhZPS9hsC/hGIehwiYCXMUlrl+WF\nAQIhAOntBI6qaecNjAAVG7UbZclMuHROUONmZUF1KNq6VyV5AiEAxRLkfHWy52CM\njOQrX347edZ30f4QczvugXwsyuU9A1ECIGlGZ8Sk4OBA8n6fAUcyO06qnmCJVlHg\npTUeOvKk5c9RAiBs28+8dCNbrbhVhx/yQr9FwNM0+ttJW/yWJ+pyNQhr0QIgJTT6\nxwCWYOtbioyt7B9l+ENy3AMSO3Uq+xmIKkvItK4=\n-----END RSA PRIVATE KEY-----';
it('cannot set certificate without token', function (done) {
request.post(SERVER_URL + '/api/v1/settings/certificate')
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
});
it('cannot set certificate without certificate', function (done) {
request.post(SERVER_URL + '/api/v1/settings/certificate')
.query({ access_token: token })
.send({ key: validKey1 })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('cannot set certificate without key', function (done) {
request.post(SERVER_URL + '/api/v1/settings/certificate')
.query({ access_token: token })
.send({ cert: validCert1 })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('cannot set certificate with cert not being a string', function (done) {
request.post(SERVER_URL + '/api/v1/settings/certificate')
.query({ access_token: token })
.send({ cert: 1234, key: validKey1 })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('cannot set certificate with key not being a string', function (done) {
request.post(SERVER_URL + '/api/v1/settings/certificate')
.query({ access_token: token })
.send({ cert: validCert1, key: true })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('cannot set non wildcard certificate', function (done) {
request.post(SERVER_URL + '/api/v1/settings/certificate')
.query({ access_token: token })
.send({ cert: validCert0, key: validKey0 })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('can set certificate', function (done) {
request.post(SERVER_URL + '/api/v1/settings/certificate')
.query({ access_token: token })
.send({ cert: validCert1, key: validKey1 })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(202);
done();
});
});
it('did set the certificate', function (done) {
var cert = fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), 'utf-8');
expect(cert).to.eql(validCert1);
var key = fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), 'utf-8');
expect(key).to.eql(validKey1);
done();
});
});
});
+3 -2
View File
@@ -16,7 +16,7 @@ var appdb = require('./appdb.js'),
safe = require('safetydance'),
_ = require('underscore');
var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
var NOOP_CALLBACK = function (error) { if (error) debug('Unhandled error: ', error); };
// appId -> { schedulerConfig (manifest), cronjobs, containerIds }
var gState = (function loadState() {
@@ -180,7 +180,8 @@ function doTask(appId, taskName, callback) {
debug('Creating createSubcontainer for %s/%s : %s', app.id, taskName, gState[appId].schedulerConfig[taskName].command);
docker.createSubcontainer(app, [ '/bin/sh', '-c', gState[appId].schedulerConfig[taskName].command ], function (error, container) {
// NOTE: if you change container name here, fix addons.js to return correct container names
docker.createSubcontainer(app, app.id + '-' + taskName, [ '/bin/sh', '-c', gState[appId].schedulerConfig[taskName].command ], function (error, container) {
appState.containerIds[taskName] = container.id;
saveState(gState);
+5 -3
View File
@@ -53,7 +53,6 @@ function initializeExpressSync() {
.use(json)
.use(urlencoded)
.use(middleware.cookieParser())
.use(middleware.favicon(paths.FAVICON_FILE)) // used when serving oauth login page
.use(middleware.cors({ origins: [ '*' ], allowCredentials: true }))
.use(middleware.session({ secret: 'yellow is blue', resave: true, saveUninitialized: true, cookie: { path: '/', httpOnly: true, secure: false, maxAge: 600000 } }))
.use(passport.initialize())
@@ -95,7 +94,6 @@ function initializeExpressSync() {
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.post('/api/v1/cloudron/migrate', rootScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.cloudron.migrate);
router.post('/api/v1/cloudron/certificate', rootScope, multipart, routes.cloudron.setCertificate);
router.get ('/api/v1/cloudron/graphs', rootScope, routes.graphs.getGraphs);
// feedback
@@ -161,6 +159,10 @@ function initializeExpressSync() {
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.post('/api/v1/settings/certificate', settingsScope, routes.settings.setCertificate);
router.post('/api/v1/settings/admin_certificate', settingsScope, routes.settings.setAdminCertificate);
// backup routes
router.get ('/api/v1/backups', settingsScope, routes.backups.get);
@@ -231,8 +233,8 @@ function start(callback) {
async.series([
auth.initialize,
database.initialize,
cloudron.initialize, // keep this here because it reads activation state that others depend on
taskmanager.initialize,
cloudron.initialize,
mailer.initialize,
cron.initialize,
gHttpServer.listen.bind(gHttpServer, config.get('port'), '127.0.0.1'),
+164
View File
@@ -20,25 +20,39 @@ exports = module.exports = {
getDeveloperMode: getDeveloperMode,
setDeveloperMode: setDeveloperMode,
getDnsConfig: getDnsConfig,
setDnsConfig: setDnsConfig,
getDefaultSync: getDefaultSync,
getAll: getAll,
validateCertificate: validateCertificate,
setCertificate: setCertificate,
setAdminCertificate: setAdminCertificate,
AUTOUPDATE_PATTERN_KEY: 'autoupdate_pattern',
TIME_ZONE_KEY: 'time_zone',
CLOUDRON_NAME_KEY: 'cloudron_name',
DEVELOPER_MODE_KEY: 'developer_mode',
DNS_CONFIG_KEY: 'dns_config',
events: new (require('events').EventEmitter)()
};
var assert = require('assert'),
config = require('./config.js'),
constants = require('./constants.js'),
CronJob = require('cron').CronJob,
DatabaseError = require('./databaseerror.js'),
ejs = require('ejs'),
fs = require('fs'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
settingsdb = require('./settingsdb.js'),
shell = require('./shell.js'),
util = require('util'),
x509 = require('x509'),
_ = require('underscore');
var gDefaults = (function () {
@@ -47,10 +61,14 @@ var gDefaults = (function () {
result[exports.TIME_ZONE_KEY] = 'America/Los_Angeles';
result[exports.CLOUDRON_NAME_KEY] = 'Cloudron';
result[exports.DEVELOPER_MODE_KEY] = false;
result[exports.DNS_CONFIG_KEY] = { };
return result;
})();
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/../setup/start/nginx/appconfig.ejs', { encoding: 'utf8' }),
RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh');
if (config.TEST) {
// avoid noisy warnings during npm test
exports.events.setMaxListeners(100);
@@ -78,6 +96,7 @@ util.inherits(SettingsError, Error);
SettingsError.INTERNAL_ERROR = 'Internal Error';
SettingsError.NOT_FOUND = 'Not Found';
SettingsError.BAD_FIELD = 'Bad Field';
SettingsError.INVALID_CERT = 'Invalid certificate';
function setAutoupdatePattern(pattern, callback) {
assert.strictEqual(typeof pattern, 'string');
@@ -207,6 +226,51 @@ function setDeveloperMode(enabled, callback) {
});
}
function getDnsConfig(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.DNS_CONFIG_KEY, function (error, value) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.DNS_CONFIG_KEY]);
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
callback(null, JSON.parse(value)); // accessKeyId, secretAccessKey, region
});
}
function setDnsConfig(dnsConfig, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
var credentials;
if (dnsConfig.provider === 'route53') {
if (typeof dnsConfig.accessKeyId !== 'string') return callback(new SettingsError(SettingsError.BAD_FIELD, 'accessKeyId must be a string'));
if (typeof dnsConfig.secretAccessKey !== 'string') return callback(new SettingsError(SettingsError.BAD_FIELD, 'secretAccessKey must be a string'));
credentials = {
provider: dnsConfig.provider,
accessKeyId: dnsConfig.accessKeyId,
secretAccessKey: dnsConfig.secretAccessKey,
region: dnsConfig.region || 'us-east-1',
endpoint: dnsConfig.endpoint || null
};
} else if (dnsConfig.provider === 'caas') {
credentials = {
provider: dnsConfig.provider
};
} else {
return callback(new SettingsError(SettingsError.BAD_FIELD, 'provider must be route53 or caas'));
}
settingsdb.set(exports.DNS_CONFIG_KEY, JSON.stringify(credentials), function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
exports.events.emit(exports.DNS_CONFIG_KEY, dnsConfig);
callback(null);
});
}
function getDefaultSync(name) {
assert.strictEqual(typeof name, 'string');
@@ -225,3 +289,103 @@ function getAll(callback) {
callback(null, result);
});
}
// note: https://tools.ietf.org/html/rfc4346#section-7.4.2 (certificate_list) requires that the
// servers certificate appears first (and not the intermediate cert)
function validateCertificate(cert, key, fqdn) {
assert(cert === null || typeof cert === 'string');
assert(key === null || typeof key === 'string');
assert.strictEqual(typeof fqdn, 'string');
if (cert === null && key === null) return null;
if (!cert && key) return new Error('missing cert');
if (cert && !key) return new Error('missing key');
var content;
try {
content = x509.parseCert(cert);
} catch (e) {
return new Error('invalid cert: ' + e.message);
}
// check expiration
if (content.notAfter < new Date()) return new Error('cert expired');
function matchesDomain(domain) {
if (domain === fqdn) return true;
if (domain.indexOf('*') === 0 && domain.slice(2) === fqdn.slice(fqdn.indexOf('.') + 1)) return true;
return false;
}
// check domain
var domains = content.altNames.concat(content.subject.commonName);
if (!domains.some(matchesDomain)) return new Error(util.format('cert is not valid for this domain. Expecting %s in %j', fqdn, domains));
// http://httpd.apache.org/docs/2.0/ssl/ssl_faq.html#verify
var certModulus = safe.child_process.execSync('openssl x509 -noout -modulus', { encoding: 'utf8', input: cert });
var keyModulus = safe.child_process.execSync('openssl rsa -noout -modulus', { encoding: 'utf8', input: key });
if (certModulus !== keyModulus) return new Error('key does not match the cert');
return null;
}
function setCertificate(cert, key, callback) {
assert.strictEqual(typeof cert, 'string');
assert.strictEqual(typeof key, 'string');
assert.strictEqual(typeof callback, 'function');
var error = validateCertificate(cert, key, '*.' + config.fqdn());
if (error) return callback(new SettingsError(SettingsError.INVALID_CERT, error.message));
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), cert)) {
return callback(new SettingsError(SettingsError.INTERNAL_ERROR, safe.error.message));
}
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), key)) {
return callback(new SettingsError(SettingsError.INTERNAL_ERROR, safe.error.message));
}
shell.sudo('setCertificate', [ RELOAD_NGINX_CMD ], function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
return callback(null);
});
}
function setAdminCertificate(cert, key, callback) {
assert.strictEqual(typeof cert, 'string');
assert.strictEqual(typeof key, 'string');
assert.strictEqual(typeof callback, 'function');
var sourceDir = path.resolve(__dirname, '..');
var endpoint = 'admin';
var vhost = config.appFqdn(constants.ADMIN_LOCATION);
var certFilePath = path.join(paths.APP_CERTS_DIR, 'admin.cert');
var keyFilePath = path.join(paths.APP_CERTS_DIR, 'admin.key');
var error = validateCertificate(cert, key, vhost);
if (error) return callback(new SettingsError(SettingsError.INVALID_CERT, error.message));
if (!safe.fs.writeFileSync(certFilePath, cert)) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(keyFilePath, key)) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, safe.error.message));
var data = {
sourceDir: sourceDir,
adminOrigin: config.adminOrigin(),
vhost: vhost,
endpoint: endpoint,
certFilePath: certFilePath,
keyFilePath: keyFilePath
};
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, 'admin.conf');
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) return callback(safe.error);
shell.sudo('setAdminCertificate', [ RELOAD_NGINX_CMD ], function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
return callback(null);
});
}
+99
View File
@@ -0,0 +1,99 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
getSignedUploadUrl: getSignedUploadUrl,
getSignedDownloadUrl: getSignedDownloadUrl,
copyObject: copyObject
};
var assert = require('assert'),
AWS = require('aws-sdk'),
config = require('../config.js'),
superagent = require('superagent');
function getBackupCredentials(callback) {
assert.strictEqual(typeof callback, 'function');
assert(config.token());
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/awscredentials';
superagent.post(url).query({ token: config.token() }).end(function (error, result) {
if (error) return callback(error);
if (result.statusCode !== 201) return callback(new Error(result.text));
if (!result.body || !result.body.credentials) return callback(new Error('Unexpected response'));
var credentials = {
accessKeyId: result.body.credentials.AccessKeyId,
secretAccessKey: result.body.credentials.SecretAccessKey,
sessionToken: result.body.credentials.SessionToken,
region: 'us-east-1'
};
if (config.aws().endpoint) credentials.endpoint = new AWS.Endpoint(config.aws().endpoint);
callback(null, credentials);
});
}
function getSignedUploadUrl(filename, callback) {
assert.strictEqual(typeof filename, 'string');
assert.strictEqual(typeof callback, 'function');
getBackupCredentials(function (error, credentials) {
if (error) return callback(error);
var s3 = new AWS.S3(credentials);
var params = {
Bucket: config.aws().backupBucket,
Key: config.aws().backupPrefix + '/' + filename,
Expires: 60 * 30 /* 30 minutes */
};
var url = s3.getSignedUrl('putObject', params);
callback(null, { url : url, sessionToken: credentials.sessionToken });
});
}
function getSignedDownloadUrl(filename, callback) {
assert.strictEqual(typeof filename, 'string');
assert.strictEqual(typeof callback, 'function');
getBackupCredentials(function (error, credentials) {
if (error) return callback(error);
var s3 = new AWS.S3(credentials);
var params = {
Bucket: config.aws().backupBucket,
Key: config.aws().backupPrefix + '/' + filename,
Expires: 60 * 30 /* 30 minutes */
};
var url = s3.getSignedUrl('getObject', params);
callback(null, { url: url, sessionToken: credentials.sessionToken });
});
}
function copyObject(from, to, callback) {
assert.strictEqual(typeof from, 'string');
assert.strictEqual(typeof to, 'string');
assert.strictEqual(typeof callback, 'function');
getBackupCredentials(function (error, credentials) {
if (error) return callback(error);
var params = {
Bucket: config.aws().backupBucket, // target bucket
Key: config.aws().backupPrefix + '/' + to, // target file
CopySource: config.aws().backupBucket + '/' + config.aws().backupPrefix + '/' + from, // source
};
var s3 = new AWS.S3(credentials);
s3.copyObject(params, callback);
});
}
+91
View File
@@ -0,0 +1,91 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
getSignedUploadUrl: getSignedUploadUrl,
getSignedDownloadUrl: getSignedDownloadUrl,
copyObject: copyObject
};
var assert = require('assert'),
AWS = require('aws-sdk'),
config = require('../config.js');
function getBackupCredentials(callback) {
assert.strictEqual(typeof callback, 'function');
if (!config.aws().accessKeyId || !config.aws().secretAccessKey) return callback(new Error('missing credentials'));
var credentials = {
accessKeyId: config.aws().accessKeyId,
secretAccessKey: config.aws().secretAccessKey,
region: 'us-east-1'
};
if (config.aws().endpoint) credentials.endpoint = new AWS.Endpoint(config.aws().endpoint);
callback(null, credentials);
}
function getSignedUploadUrl(filename, callback) {
assert.strictEqual(typeof filename, 'string');
assert.strictEqual(typeof callback, 'function');
getBackupCredentials(function (error, credentials) {
if (error) return callback(error);
var s3 = new AWS.S3(credentials);
var params = {
Bucket: config.aws().backupBucket,
Key: config.aws().backupPrefix + '/' + filename,
Expires: 60 * 30 /* 30 minutes */
};
var url = s3.getSignedUrl('putObject', params);
callback(null, { url : url, sessionToken: credentials.sessionToken });
});
}
function getSignedDownloadUrl(filename, callback) {
assert.strictEqual(typeof filename, 'string');
assert.strictEqual(typeof callback, 'function');
getBackupCredentials(function (error, credentials) {
if (error) return callback(error);
var s3 = new AWS.S3(credentials);
var params = {
Bucket: config.aws().backupBucket,
Key: config.aws().backupPrefix + '/' + filename,
Expires: 60 * 30 /* 30 minutes */
};
var url = s3.getSignedUrl('getObject', params);
callback(null, { url: url, sessionToken: credentials.sessionToken });
});
}
function copyObject(from, to, callback) {
assert.strictEqual(typeof from, 'string');
assert.strictEqual(typeof to, 'string');
assert.strictEqual(typeof callback, 'function');
getBackupCredentials(function (error, credentials) {
if (error) return callback(error);
var params = {
Bucket: config.aws().backupBucket, // target bucket
Key: config.aws().backupPrefix + '/' + to, // target file
CopySource: config.aws().backupBucket + '/' + config.aws().backupPrefix + '/' + from, // source
};
var s3 = new AWS.S3(credentials);
s3.copyObject(params, callback);
});
}
-33
View File
@@ -1,33 +0,0 @@
/* jslint node:true */
'use strict';
var assert = require('assert'),
util = require('util');
exports = module.exports = SubdomainError;
function SubdomainError(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(SubdomainError, Error);
SubdomainError.NOT_FOUND = 'No such domain';
SubdomainError.EXTERNAL_ERROR = 'External error';
SubdomainError.STILL_BUSY = 'Still busy';
SubdomainError.MISSING_CREDENTIALS = 'Missing credentials';
+66 -44
View File
@@ -2,76 +2,98 @@
'use strict';
var assert = require('assert'),
async = require('async'),
aws = require('./aws.js'),
caas = require('./caas.js'),
config = require('./config.js'),
debug = require('debug')('box:subdomains'),
util = require('util'),
SubdomainError = require('./subdomainerror.js');
module.exports = exports = {
add: add,
addMany: addMany,
remove: remove,
status: status
status: status,
update: update, // unlike add, this fetches latest value, compares and adds if necessary. atomicity depends on backend
get: get,
SubdomainError: SubdomainError
};
// choose which subdomain backend we use
// for test purpose we use aws
var assert = require('assert'),
caas = require('./dns/caas.js'),
config = require('./config.js'),
route53 = require('./dns/route53.js'),
util = require('util');
function SubdomainError(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(SubdomainError, Error);
SubdomainError.NOT_FOUND = 'No such domain';
SubdomainError.EXTERNAL_ERROR = 'External error';
SubdomainError.STILL_BUSY = 'Still busy';
SubdomainError.MISSING_CREDENTIALS = 'Missing credentials';
// choose which subdomain backend we use for test purpose we use route53
function api() {
return config.token() && !config.TEST ? caas : aws;
return config.isCustomDomain() || config.TEST ? route53 : caas;
}
function add(record, callback) {
assert.strictEqual(typeof record, 'object');
assert.strictEqual(typeof record.subdomain, 'string');
assert.strictEqual(typeof record.type, 'string');
assert.strictEqual(typeof record.value, 'string');
function add(subdomain, type, values, callback) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug('add: ', record);
api().addSubdomain(config.zoneName(), record.subdomain, record.type, record.value, function (error, changeId) {
api().add(config.zoneName(), subdomain, type, values, function (error, changeId) {
if (error) return callback(error);
callback(null, changeId);
});
}
function addMany(records, callback) {
assert(util.isArray(records));
function get(subdomain, type, callback) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
debug('addMany: ', records);
var changeIds = [];
async.eachSeries(records, function (record, callback) {
add(record, function (error, changeId) {
if (error) return callback(error);
changeIds.push(changeId);
callback(null);
});
}, function (error) {
api().get(config.zoneName(), subdomain, type, function (error, values) {
if (error) return callback(error);
callback(null, changeIds);
callback(null, values);
});
}
function remove(record, callback) {
assert.strictEqual(typeof record, 'object');
function update(subdomain, type, values, callback) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug('remove: ', record);
api().update(config.zoneName(), subdomain, type, values, function (error) {
if (error) return callback(error);
api().delSubdomain(config.zoneName(), record.subdomain, record.type, record.value, function (error) {
callback(null);
});
}
function remove(subdomain, type, values, callback) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
api().del(config.zoneName(), subdomain, type, values, function (error) {
if (error && error.reason !== SubdomainError.NOT_FOUND) return callback(error);
debug('deleteSubdomain: successfully deleted subdomain from aws.');
callback(null);
});
}
+32 -17
View File
@@ -10,6 +10,7 @@ exports = module.exports = {
var appdb = require('./appdb.js'),
assert = require('assert'),
child_process = require('child_process'),
cloudron = require('./cloudron.js'),
debug = require('debug')('box:taskmanager'),
locker = require('./locker.js'),
_ = require('underscore');
@@ -18,12 +19,41 @@ var gActiveTasks = { };
var gPendingTasks = [ ];
var TASK_CONCURRENCY = 5;
var NOOP_CALLBACK = function (error) { console.error(error); };
var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
// resume app installs and uninstalls
locker.on('unlocked', startNextTask);
if (cloudron.isConfiguredSync()) {
resumeTasks();
} else {
cloudron.events.on(cloudron.EVENT_CONFIGURED, resumeTasks);
}
callback();
}
function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function');
gPendingTasks = [ ]; // clear this first, otherwise stopAppTask will resume them
for (var appId in gActiveTasks) {
stopAppTask(appId);
}
cloudron.events.removeListener(cloudron.EVENT_CONFIGURED, resumeTasks);
locker.removeListener('unlocked', startNextTask);
callback(null);
}
// resume app installs and uninstalls
function resumeTasks(callback) {
callback = callback || NOOP_CALLBACK;
appdb.getAll(function (error, apps) {
if (error) return callback(error);
@@ -36,21 +66,6 @@ function initialize(callback) {
callback(null);
});
locker.on('unlocked', startNextTask);
}
function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function');
gPendingTasks = [ ]; // clear this first, otherwise stopAppTask will resume them
for (var appId in gActiveTasks) {
stopAppTask(appId);
}
locker.removeListener('unlocked', startNextTask);
callback(null);
}
function startNextTask() {
+8 -15
View File
@@ -9,6 +9,7 @@
var addons = require('../addons.js'),
appdb = require('../appdb.js'),
apptask = require('../apptask.js'),
async = require('async'),
config = require('../config.js'),
database = require('../database.js'),
expect = require('expect.js'),
@@ -17,6 +18,7 @@ var addons = require('../addons.js'),
net = require('net'),
nock = require('nock'),
paths = require('../paths.js'),
settings = require('../settings.js'),
_ = require('underscore');
var MANIFEST = {
@@ -80,10 +82,11 @@ var APP = {
describe('apptask', function () {
before(function (done) {
config.set('version', '0.5.0');
database.initialize(function (error) {
expect(error).to.be(null);
appdb.add(APP.id, APP.appStoreId, APP.manifest, APP.location, APP.portBindings, APP.accessRestriction, APP.oauthProxy, done);
});
async.series([
database.initialize,
appdb.add.bind(null, APP.id, APP.appStoreId, APP.manifest, APP.location, APP.portBindings, APP.accessRestriction, APP.oauthProxy),
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' })
], done);
});
after(function (done) {
@@ -200,12 +203,8 @@ describe('apptask', function () {
it('registers subdomain', function (done) {
nock.cleanAll();
var scope = nock(config.apiServerOrigin())
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=APPSTORE_TOKEN')
.times(2)
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
var awsScope = nock(config.aws().endpoint)
var awsScope = nock('http://localhost:5353')
.get('/2013-04-01/hostedzone')
.reply(200, js2xml('ListHostedZonesResponse', awsHostedZones, { arrayMap: { HostedZones: 'HostedZone'} }))
.post('/2013-04-01/hostedzone/ZONEID/rrset/')
@@ -213,7 +212,6 @@ describe('apptask', function () {
apptask._registerSubdomain(APP, function (error) {
expect(error).to.be(null);
expect(scope.isDone()).to.be.ok();
expect(awsScope.isDone()).to.be.ok();
done();
});
@@ -221,10 +219,6 @@ describe('apptask', function () {
it('unregisters subdomain', function (done) {
nock.cleanAll();
var scope = nock(config.apiServerOrigin())
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=APPSTORE_TOKEN')
.times(2)
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
var awsScope = nock(config.aws().endpoint)
.get('/2013-04-01/hostedzone')
@@ -234,7 +228,6 @@ describe('apptask', function () {
apptask._unregisterSubdomain(APP, APP.location, function (error) {
expect(error).to.be(null);
expect(scope.isDone()).to.be.ok();
expect(awsScope.isDone()).to.be.ok();
done();
});
+3 -2
View File
@@ -3,6 +3,7 @@
set -eu
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
readonly TEST_IMAGE="cloudron/test:10.0.0"
source ${SOURCE_DIR}/setup/INFRA_VERSION
@@ -34,8 +35,8 @@ for script in "${scripts[@]}"; do
fi
done
if ! docker inspect cloudron/test:9.0.0 >/dev/null 2>/dev/null; then
echo "docker pull cloudron/test:8.0.0 for tests to run"
if ! docker inspect "${TEST_IMAGE}" >/dev/null 2>/dev/null; then
echo "docker pull "${TEST_IMAGE}" for tests to run"
exit 1
fi
+5 -1
View File
@@ -9,17 +9,21 @@
var constants = require('../constants.js'),
expect = require('expect.js'),
fs = require('fs'),
path = require('path');
path = require('path'),
paths = require('../paths.js'),
safe = require('safetydance');
var config = null;
describe('config', function () {
before(function () {
safe.fs.unlinkSync(paths.DNS_IN_SYNC_FILE);
delete require.cache[require.resolve('../config.js')];
config = require('../config.js');
});
after(function () {
safe.fs.unlinkSync(paths.DNS_IN_SYNC_FILE);
delete require.cache[require.resolve('../config.js')];
});
+155 -54
View File
@@ -23,71 +23,172 @@ function cleanup(done) {
}
describe('Settings', function () {
before(setup);
after(cleanup);
describe('values', function () {
before(setup);
after(cleanup);
it('can get default timezone', function (done) {
settings.getTimeZone(function (error, tz) {
expect(error).to.be(null);
expect(tz.length).to.not.be(0);
done();
it('can get default timezone', function (done) {
settings.getTimeZone(function (error, tz) {
expect(error).to.be(null);
expect(tz.length).to.not.be(0);
done();
});
});
it('can get default autoupdate_pattern', function (done) {
settings.getAutoupdatePattern(function (error, pattern) {
expect(error).to.be(null);
expect(pattern).to.be('00 00 1,3,5,23 * * *');
done();
});
});
it ('can get default cloudron name', function (done) {
settings.getCloudronName(function (error, name) {
expect(error).to.be(null);
expect(name).to.be('Cloudron');
done();
});
});
it('can get default cloudron avatar', function (done) {
settings.getCloudronAvatar(function (error, gravatar) {
expect(error).to.be(null);
expect(gravatar).to.be.a(Buffer);
done();
});
});
it('can get default developer mode', function (done) {
settings.getDeveloperMode(function (error, enabled) {
expect(error).to.be(null);
expect(enabled).to.equal(false);
done();
});
});
it('can set developer mode', function (done) {
settings.setDeveloperMode(true, function (error) {
expect(error).to.be(null);
done();
});
});
it('can get developer mode', function (done) {
settings.getDeveloperMode(function (error, enabled) {
expect(error).to.be(null);
expect(enabled).to.equal(true);
done();
});
});
it('can set dns config', function (done) {
settings.setDnsConfig({ provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey' }, function (error) {
expect(error).to.be(null);
done();
});
});
it('can get dns config', function (done) {
settings.getDnsConfig(function (error, dnsConfig) {
expect(error).to.be(null);
expect(dnsConfig.provider).to.be('route53');
expect(dnsConfig.accessKeyId).to.be('accessKeyId');
expect(dnsConfig.secretAccessKey).to.be('secretAccessKey');
expect(dnsConfig.region).to.be('us-east-1');
done();
});
});
it('can get all values', function (done) {
settings.getAll(function (error, allSettings) {
expect(error).to.be(null);
expect(allSettings[settings.TIME_ZONE_KEY]).to.be.a('string');
expect(allSettings[settings.AUTOUPDATE_PATTERN_KEY]).to.be.a('string');
expect(allSettings[settings.CLOUDRON_NAME_KEY]).to.be.a('string');
done();
});
});
});
it('can get default autoupdate_pattern', function (done) {
settings.getAutoupdatePattern(function (error, pattern) {
expect(error).to.be(null);
expect(pattern).to.be('00 00 1,3,5,23 * * *');
done();
});
});
describe('validateCertificate', function () {
before(setup);
after(cleanup);
it ('can get default cloudron name', function (done) {
settings.getCloudronName(function (error, name) {
expect(error).to.be(null);
expect(name).to.be('Cloudron');
done();
});
});
/*
Generate these with:
openssl genrsa -out server.key 512
openssl req -new -key server.key -out server.csr -subj "/C=DE/ST=Berlin/L=Berlin/O=Nebulon/OU=CTO/CN=baz.foobar.com"
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
*/
it('can get default cloudron avatar', function (done) {
settings.getCloudronAvatar(function (error, gravatar) {
expect(error).to.be(null);
expect(gravatar).to.be.a(Buffer);
done();
});
});
// foobar.com
var validCert0 = '-----BEGIN CERTIFICATE-----\nMIIBujCCAWQCCQCjLyTKzAJ4FDANBgkqhkiG9w0BAQsFADBkMQswCQYDVQQGEwJE\nRTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEDAOBgNVBAoMB05l\nYnVsb24xDDAKBgNVBAsMA0NUTzETMBEGA1UEAwwKZm9vYmFyLmNvbTAeFw0xNTEw\nMjgxMjM5MjZaFw0xNjEwMjcxMjM5MjZaMGQxCzAJBgNVBAYTAkRFMQ8wDQYDVQQI\nDAZCZXJsaW4xDzANBgNVBAcMBkJlcmxpbjEQMA4GA1UECgwHTmVidWxvbjEMMAoG\nA1UECwwDQ1RPMRMwEQYDVQQDDApmb29iYXIuY29tMFwwDQYJKoZIhvcNAQEBBQAD\nSwAwSAJBAMeYofgwHeNVmGkGe0gj4dnX2ciifDi7X2K/oVHp7mxuHjGMSYP9Z7b6\n+mu0IMf4OedwXStHBeO8mwjKxZmE7p8CAwEAATANBgkqhkiG9w0BAQsFAANBAJI7\nFUUHXjR63UFk8pgxp0c7hEGqj4VWWGsmo8oZnnX8jGVmQDKbk8o3MtDujfqupmMR\nMo7tSAFlG7zkm3GYhpw=\n-----END CERTIFICATE-----';
var validKey0 = '-----BEGIN RSA PRIVATE KEY-----\nMIIBOwIBAAJBAMeYofgwHeNVmGkGe0gj4dnX2ciifDi7X2K/oVHp7mxuHjGMSYP9\nZ7b6+mu0IMf4OedwXStHBeO8mwjKxZmE7p8CAwEAAQJBAJS59Sb8o6i8JT9NJxvQ\nMQCkSJGqEaosZJ0uccSZ7aE48v+H7HiPzXAueitohcEif2Wp1EZ1RbRMURhznNiZ\neLECIQDxxqhakO6wc7H68zmpRXJ5ZxGUNbM24AMtpONAtEw9iwIhANNWtp6P74OV\ntvfOmtubbqw768fmGskFCOcp5oF8oF29AiBkTAf9AhCyjFwyAYJTEScq67HkLN66\njfVjkvpfFixmfwIgI+xldmZ5DCDyzQSthg7RrS0yUvRmMS1N6h1RNUl96PECIQDl\nit4lFcytbqNo1PuBZvzQE+plCjiJqXHYo3WCst1Jbg==\n-----END RSA PRIVATE KEY-----';
it('can get default developer mode', function (done) {
settings.getDeveloperMode(function (error, enabled) {
expect(error).to.be(null);
expect(enabled).to.equal(false);
done();
});
});
// *.foobar.com
var validCert1 = '-----BEGIN CERTIFICATE-----\nMIIBvjCCAWgCCQCg957GWuHtbzANBgkqhkiG9w0BAQsFADBmMQswCQYDVQQGEwJE\nRTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEDAOBgNVBAoMB05l\nYnVsb24xDDAKBgNVBAsMA0NUTzEVMBMGA1UEAwwMKi5mb29iYXIuY29tMB4XDTE1\nMTAyODEzMDI1MFoXDTE2MTAyNzEzMDI1MFowZjELMAkGA1UEBhMCREUxDzANBgNV\nBAgMBkJlcmxpbjEPMA0GA1UEBwwGQmVybGluMRAwDgYDVQQKDAdOZWJ1bG9uMQww\nCgYDVQQLDANDVE8xFTATBgNVBAMMDCouZm9vYmFyLmNvbTBcMA0GCSqGSIb3DQEB\nAQUAA0sAMEgCQQC0FKf07ZWMcABFlZw+GzXK9EiZrlJ1lpnu64RhN99z7MXRr8cF\nnZVgY3jgatuyR5s3WdzUvye2eJ0rNicl2EZJAgMBAAEwDQYJKoZIhvcNAQELBQAD\nQQAw4bteMZAeJWl2wgNLw+wTwAH96E0jyxwreCnT5AxJLmgimyQ0XOF4FsssdRFj\nxD9WA+rktelBodJyPeTDNhIh\n-----END CERTIFICATE-----';
var validKey1 = '-----BEGIN RSA PRIVATE KEY-----\nMIIBOQIBAAJBALQUp/TtlYxwAEWVnD4bNcr0SJmuUnWWme7rhGE333PsxdGvxwWd\nlWBjeOBq27JHmzdZ3NS/J7Z4nSs2JyXYRkkCAwEAAQJALV2eykcoC48TonQEPmkg\nbhaIS57syw67jMLsQImQ02UABKzqHPEKLXPOZhZPS9hsC/hGIehwiYCXMUlrl+WF\nAQIhAOntBI6qaecNjAAVG7UbZclMuHROUONmZUF1KNq6VyV5AiEAxRLkfHWy52CM\njOQrX347edZ30f4QczvugXwsyuU9A1ECIGlGZ8Sk4OBA8n6fAUcyO06qnmCJVlHg\npTUeOvKk5c9RAiBs28+8dCNbrbhVhx/yQr9FwNM0+ttJW/yWJ+pyNQhr0QIgJTT6\nxwCWYOtbioyt7B9l+ENy3AMSO3Uq+xmIKkvItK4=\n-----END RSA PRIVATE KEY-----';
it('can set developer mode', function (done) {
settings.setDeveloperMode(true, function (error) {
expect(error).to.be(null);
done();
});
});
// baz.foobar.com
var validCert2 = '-----BEGIN CERTIFICATE-----\nMIIBwjCCAWwCCQDIKtL9RCDCkDANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJE\nRTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEDAOBgNVBAoMB05l\nYnVsb24xDDAKBgNVBAsMA0NUTzEXMBUGA1UEAwwOYmF6LmZvb2Jhci5jb20wHhcN\nMTUxMDI4MTMwNTMzWhcNMTYxMDI3MTMwNTMzWjBoMQswCQYDVQQGEwJERTEPMA0G\nA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEDAOBgNVBAoMB05lYnVsb24x\nDDAKBgNVBAsMA0NUTzEXMBUGA1UEAwwOYmF6LmZvb2Jhci5jb20wXDANBgkqhkiG\n9w0BAQEFAANLADBIAkEAw7UWW/VoQePv2l92l3XcntZeyw1nBiHxk1axZwC6auOW\n2/zfA//Tg7fv4q5qKnV1n/71IiMAheeFcpfogY5rTwIDAQABMA0GCSqGSIb3DQEB\nCwUAA0EAtluL6dGNfOdNkzoO/UwzRaIvEm2reuqe+Ik4WR/k+DJ4igrmRCQqXwjW\nJaGYsFWsuk3QLOWQ9YgCKlcIYd+1/A==\n-----END CERTIFICATE-----';
var validKey2 = '-----BEGIN RSA PRIVATE KEY-----\nMIIBOQIBAAJBAMO1Flv1aEHj79pfdpd13J7WXssNZwYh8ZNWsWcAumrjltv83wP/\n04O37+Kuaip1dZ/+9SIjAIXnhXKX6IGOa08CAwEAAQJAUPD3Y2cXDJFaJQXwhWnw\nqhzdLbvITUgCor5rNr+dWhE2MopGPpRHiabA1PeWEPx8CfblyTZGd8KUR/2W1c0r\naQIhAP4ZxB3+uhuzzMfyRrn/khr12pFn/FCIDbwnDbyUxLrTAiEAxSuVOFs+Mupt\nYCz/pPrDCx3eid0wyXRObbkLHOxJiBUCIBTp5fxaBNNW3xnt1OhmIo5Zgd3J4zh1\nmjvMMxM8Y1zFAiAxOP0qsZSoj1+41+MGY9fXaaCJ2F96m3+M4tpEYTTGNQIgdESZ\nz+hzHBeYVbWJpIR8uaNkx7wveUF90FpipXyeTsA=\n-----END RSA PRIVATE KEY-----';
it('can get developer mode', function (done) {
settings.getDeveloperMode(function (error, enabled) {
expect(error).to.be(null);
expect(enabled).to.equal(true);
done();
it('allows both null', function () {
expect(settings.validateCertificate(null, null, 'foobar.com')).to.be(null);
});
});
it('can get all values', function (done) {
settings.getAll(function (error, allSettings) {
expect(error).to.be(null);
expect(allSettings[settings.TIME_ZONE_KEY]).to.be.a('string');
expect(allSettings[settings.AUTOUPDATE_PATTERN_KEY]).to.be.a('string');
expect(allSettings[settings.CLOUDRON_NAME_KEY]).to.be.a('string');
done();
it('does not allow only cert', function () {
expect(settings.validateCertificate('cert', null, 'foobar.com')).to.be.an(Error);
});
it('does not allow only key', function () {
expect(settings.validateCertificate(null, 'key', 'foobar.com')).to.be.an(Error);
});
it('does not allow empty string for cert', function () {
expect(settings.validateCertificate('', 'key', 'foobar.com')).to.be.an(Error);
});
it('does not allow empty string for key', function () {
expect(settings.validateCertificate('cert', '', 'foobar.com')).to.be.an(Error);
});
it('does not allow invalid cert', function () {
expect(settings.validateCertificate('someinvalidcert', validKey0, 'foobar.com')).to.be.an(Error);
});
it('does not allow invalid key', function () {
expect(settings.validateCertificate(validCert0, 'invalidkey', 'foobar.com')).to.be.an(Error);
});
it('does not allow cert without matching domain', function () {
expect(settings.validateCertificate(validCert0, validKey0, 'cloudron.io')).to.be.an(Error);
});
it('allows valid cert with matching domain', function () {
expect(settings.validateCertificate(validCert0, validKey0, 'foobar.com')).to.be(null);
});
it('allows valid cert with matching domain (wildcard)', function () {
expect(settings.validateCertificate(validCert1, validKey1, 'abc.foobar.com')).to.be(null);
});
it('does now allow cert without matching domain (wildcard)', function () {
expect(settings.validateCertificate(validCert1, validKey1, 'foobar.com')).to.be.an(Error);
expect(settings.validateCertificate(validCert1, validKey1, 'bar.abc.foobar.com')).to.be.an(Error);
});
it('allows valid cert with matching domain (subdomain)', function () {
expect(settings.validateCertificate(validCert2, validKey2, 'baz.foobar.com')).to.be(null);
});
it('does not allow cert without matching domain (subdomain)', function () {
expect(settings.validateCertificate(validCert0, validKey0, 'baz.foobar.com')).to.be.an(Error);
});
it('does not allow invalid cert/key tuple', function () {
expect(settings.validateCertificate(validCert0, validKey1, 'foobar.com')).to.be.an(Error);
});
});
});
+1 -1
View File
@@ -11,7 +11,7 @@ readonly source_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")"/../.. && pwd)"
rm -rf $HOME/.cloudron_test
mkdir -p $HOME/.cloudron_test
cd $HOME/.cloudron_test
mkdir -p data/appdata data/box/appicons data/mail data/nginx/cert data/nginx/applications data/collectd/collectd.conf.d data/addons configs
mkdir -p data/appdata data/box/appicons data/mail data/nginx/cert data/nginx/applications data/collectd/collectd.conf.d data/addons configs data/box/certs
webadmin_scopes="root,profile,users,apps,settings"
webadmin_origin="https://${ADMIN_LOCATION}-localhost"
-2
View File
@@ -1,2 +0,0 @@
{
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1021 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

+3 -3
View File
@@ -120,8 +120,8 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand navbar-brand-icon" href="index.html"><img src="/api/v1/cloudron/avatar" width="40" height="40"/></a>
<a class="navbar-brand" href="index.html">Cloudron</a>
<a class="navbar-brand navbar-brand-icon" href="#/"><img src="/api/v1/cloudron/avatar" width="40" height="40"/></a>
<a class="navbar-brand" href="#/">Cloudron</a>
</div>
<!-- /.navbar-header -->
@@ -145,9 +145,9 @@
<a href="" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><img ng-src="{{user.gravatar}}"/> {{user.username}} <span class="caret"></span></a>
<ul class="dropdown-menu" role="menu">
<li><a href="#/account"><i class="fa fa-user fa-fw"></i> Account</a></li>
<li ng-show="user.admin && config.isDev"><a href="#/dns"><i class="fa fa-wrench fa-fw"></i> DNS Management</a></li>
<li ng-show="user.admin"><a href="#/graphs"><i class="fa fa-bar-chart fa-fw"></i> Graphs</a></li>
<li><a href="#/support"><i class="fa fa-comment fa-fw"></i> Support</a></li>
<li ng-show="user.admin"><a href="#/certs"><i class="fa fa-certificate fa-fw"></i> DNS & Certs</a></li>
<li ng-show="user.admin"><a href="#/settings"><i class="fa fa-wrench fa-fw"></i> Settings</a></li>
<li class="divider"></li>
<li><a href="" ng-click="logout($event)"><i class="fa fa-sign-out fa-fw"></i> Logout</a></li>
+45 -11
View File
@@ -61,6 +61,7 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
this._configListener = [];
this._readyListener = [];
this._userInfo = {
id: null,
username: null,
email: null,
admin: false
@@ -122,6 +123,7 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
Client.prototype.setUserInfo = function (userInfo) {
// In order to keep the angular bindings alive, set each property individually
this._userInfo.id = userInfo.id;
this._userInfo.username = userInfo.username;
this._userInfo.email = userInfo.email;
this._userInfo.admin = !!userInfo.admin;
@@ -204,7 +206,17 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
Client.prototype.installApp = function (id, manifest, title, config, callback) {
var that = this;
var data = { appStoreId: id, manifest: manifest, location: config.location, portBindings: config.portBindings, accessRestriction: config.accessRestriction, oauthProxy: config.oauthProxy };
var data = {
appStoreId: id,
manifest: manifest,
location: config.location,
portBindings: config.portBindings,
accessRestriction: config.accessRestriction,
oauthProxy: config.oauthProxy,
cert: config.cert,
key: config.key
};
$http.post(client.apiOrigin + '/api/v1/apps/install', data).success(function (data, status) {
if (status !== 202 || typeof data !== 'object') return defaultErrorHandler(callback);
@@ -238,7 +250,17 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
};
Client.prototype.configureApp = function (id, password, config, callback) {
var data = { appId: id, password: password, location: config.location, portBindings: config.portBindings, accessRestriction: config.accessRestriction, oauthProxy: config.oauthProxy };
var data = {
appId: id,
password: password,
location: config.location,
portBindings: config.portBindings,
accessRestriction: config.accessRestriction,
oauthProxy: config.oauthProxy,
cert: config.cert,
key: config.key
};
$http.post(client.apiOrigin + '/api/v1/apps/' + id + '/configure', data).success(function (data, status) {
if (status !== 202) return callback(new ClientError(status, data));
callback(null);
@@ -292,6 +314,20 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
}).error(defaultErrorHandler(callback));
};
Client.prototype.setDnsConfig = function (dnsConfig, callback) {
$http.post(client.apiOrigin + '/api/v1/settings/dns_config', dnsConfig).success(function(data, status) {
if (status !== 200) return callback(new ClientError(status, data));
callback(null);
}).error(defaultErrorHandler(callback));
};
Client.prototype.getDnsConfig = function (callback) {
$http.get(client.apiOrigin + '/api/v1/settings/dns_config').success(function(data, status) {
if (status !== 200) return callback(new ClientError(status, data));
callback(null, data);
}).error(defaultErrorHandler(callback));
};
Client.prototype.getBackups = function (callback) {
$http.get(client.apiOrigin + '/api/v1/backups').success(function (data, status) {
if (status !== 200 || typeof data !== 'object') return callback(new ClientError(status, data));
@@ -434,16 +470,14 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
};
Client.prototype.setCertificate = function (certificateFile, keyFile, callback) {
console.log('will set certificate');
$http.post(client.apiOrigin + '/api/v1/settings/certificate', { cert: certificateFile, key: keyFile }).success(function(data, status) {
if (status !== 202) return callback(new ClientError(status, data));
callback(null);
}).error(defaultErrorHandler(callback));
};
var fd = new FormData();
fd.append('certificate', certificateFile);
fd.append('key', keyFile);
$http.post(client.apiOrigin + '/api/v1/cloudron/certificate', fd, {
headers: { 'Content-Type': undefined },
transformRequest: angular.identity
}).success(function(data, status) {
Client.prototype.setAdminCertificate = function (certificateFile, keyFile, callback) {
$http.post(client.apiOrigin + '/api/v1/settings/admin_certificate', { cert: certificateFile, key: keyFile }).success(function(data, status) {
if (status !== 202) return callback(new ClientError(status, data));
callback(null);
}).error(defaultErrorHandler(callback));
+3 -3
View File
@@ -25,15 +25,15 @@ app.config(['$routeProvider', function ($routeProvider) {
}).when('/apps', {
controller: 'AppsController',
templateUrl: 'views/apps.html'
}).when('/dns', {
controller: 'DnsController',
templateUrl: 'views/dns.html'
}).when('/account', {
controller: 'AccountController',
templateUrl: 'views/account.html'
}).when('/graphs', {
controller: 'GraphsController',
templateUrl: 'views/graphs.html'
}).when('/certs', {
controller: 'CertsController',
templateUrl: 'views/certs.html'
}).when('/settings', {
controller: 'SettingsController',
templateUrl: 'views/settings.html'
+44 -6
View File
@@ -28,8 +28,11 @@ app.config(['$routeProvider', function ($routeProvider) {
controller: 'StepController',
templateUrl: 'views/setup/step2.html'
}).when('/step3', {
controller: 'FinishController',
controller: 'StepController',
templateUrl: 'views/setup/step3.html'
}).when('/step4', {
controller: 'FinishController',
templateUrl: 'views/setup/step4.html'
}).otherwise({ redirectTo: '/'});
}]);
@@ -95,6 +98,7 @@ app.service('Wizard', [ function () {
}];
this.avatar = {};
this.avatarBlob = null;
this.dnsConfig = null;
}
Wizard.prototype.setPreviewAvatar = function (avatar) {
@@ -146,8 +150,24 @@ app.service('Wizard', [ function () {
app.controller('StepController', ['$scope', '$route', '$location', 'Wizard', function ($scope, $route, $location, Wizard) {
$scope.wizard = Wizard;
$scope.next = function (page, bad) {
if (!bad) $location.path(page);
$scope.next = function (bad) {
if (bad) return;
var current = $location.path();
var next = '';
if (current === '/step1') {
next = '/step2';
} else if (current === '/step2') {
if (Wizard.dnsConfig.provider === 'caas') next = '/step4';
else next = '/step3';
} else if (current === '/step3') {
next = '/step4';
} else {
next = '/step1';
}
$location.path(next);
};
$scope.focusNext = function (elemId, bad) {
@@ -190,11 +210,13 @@ app.controller('StepController', ['$scope', '$route', '$location', 'Wizard', fun
image = null;
};
image.src = $scope.wizard.availableAvatars[randomIndex].data || $scope.wizard.availableAvatars[randomIndex].url;
} else if ($route.current.templateUrl === 'views/setup/step3.html' && Wizard.dnsConfig.provider === 'caas') {
$location.path('/step4'); // not using custom domain
}
}]);
app.controller('FinishController', ['$scope', '$location', '$timeout', 'Wizard', 'Client', function ($scope, $location, $timeout, Wizard, Client) {
app.controller('FinishController', ['$scope', '$location', 'Wizard', 'Client', function ($scope, $location, Wizard, Client) {
$scope.wizard = Wizard;
Client.createAdmin($scope.wizard.username, $scope.wizard.password, $scope.wizard.email, $scope.setupToken, function (error) {
@@ -207,7 +229,11 @@ app.controller('FinishController', ['$scope', '$location', '$timeout', 'Wizard',
Client.changeCloudronAvatar($scope.wizard.avatarBlob, function (error) {
if (error) return console.error('Unable to set avatar.', error);
window.location.href = '/';
Client.setDnsConfig($scope.wizard.dnsConfig, function (error) {
if (error) return console.error('Unable to set dns config.', error);
window.location.href = '/';
});
});
});
}]);
@@ -224,7 +250,19 @@ app.controller('SetupController', ['$scope', '$location', 'Client', 'Wizard', fu
if (!search.email) return window.location.href = '/error.html?errorCode=3';
Wizard.email = search.email;
Wizard.hostname = window.location.host.indexOf('my-') === 0 ? window.location.host.slice(3) : window.location.host;
if (search.customDomain === 'true') {
Wizard.dnsConfig = {
provider: 'route53',
accessKeyId: null,
secretAccessKey: null
};
} else {
Wizard.dnsConfig = {
provider: 'caas',
accessKeyId: '',
secretAccessKey: ''
};
}
Client.isServerFirstTime(function (error, isFirstTime) {
if (error) {
+24
View File
@@ -47,6 +47,30 @@
<option value="1">Visible only to Cloudron users</option>
</select>
</div>
<br/>
<label class="control-label" for="appConfigureCertificateInput" ng-show="config.isCustomDomain">Certificate (optional)</label>
<div class="has-error text-center" ng-show="appConfigure.error.cert && config.isCustomDomain">{{ appConfigure.error.cert }}</div>
<div class="form-group" ng-class="{ 'has-error': !appConfigureForm.certificate.$dirty && appConfigure.error.cert }" ng-show="config.isCustomDomain">
<div class="input-group">
<input type="file" id="appConfigureCertificateFileInput" style="display:none"/>
<input type="text" class="form-control" placeholder="Certificate" ng-model="appConfigure.certificateFileName" id="appConfigureCertificateInput" name="certificate" onclick="getElementById('appConfigureCertificateFileInput').click();" style="cursor: pointer;" ng-required="appConfigure.keyFileName">
<span class="input-group-addon">
<i class="fa fa-upload" onclick="getElementById('appConfigureCertificateFileInput').click();"></i>
</span>
</div>
</div>
<div class="form-group" ng-class="{ 'has-error': !appConfigureForm.key.$dirty && appConfigure.error.cert }" ng-show="config.isCustomDomain">
<div class="input-group">
<input type="file" id="appConfigureKeyFileInput" style="display:none"/>
<input type="text" class="form-control" placeholder="Key" ng-model="appConfigure.keyFileName" id="appConfigureKeyInput" name="key" onclick="getElementById('appConfigureKeyFileInput').click();" style="cursor: pointer;" ng-required="appConfigure.certificateFileName">
<span class="input-group-addon">
<i class="fa fa-upload" onclick="getElementById('appConfigureKeyFileInput').click();"></i>
</span>
</div>
</div>
<a ng-show="!!appConfigure.app.manifest.configurePath" ng-href="https://{{ appConfigure.app.location }}{{ !appConfigure.app.location ? '' : (config.isCustomDomain ? '.' : '-') }}{{ config.fqdn }}/{{ appConfigure.app.manifest.configurePath }}" target="_blank">Application Specific Settings</a>
<br/>
<br/>
+49 -5
View File
@@ -19,7 +19,11 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
portBindings: {},
portBindingsEnabled: {},
portBindingsInfo: {},
oauthProxy: ''
oauthProxy: '',
certificateFile: null,
certificateFileName: '',
keyFile: null,
keyFileName: ''
};
$scope.appUninstall = {
@@ -51,8 +55,13 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
$scope.appConfigure.app = {};
$scope.appConfigure.location = '';
$scope.appConfigure.password = '';
$scope.appConfigure.portBindings = {};
$scope.appConfigure.portBindings = {}; // This is the actual model holding the env:port pair
$scope.appConfigure.portBindingsEnabled = {}; // This is the actual model holding the enabled/disabled flag
$scope.appConfigure.oauthProxy = '';
$scope.appConfigure.certificateFile = null;
$scope.appConfigure.certificateFileName = '';
$scope.appConfigure.keyFile = null;
$scope.appConfigure.keyFileName = '';
$scope.appConfigureForm.$setPristine();
$scope.appConfigureForm.$setUntouched();
@@ -84,15 +93,42 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
$scope.appRestoreForm.$setUntouched();
};
document.getElementById('appConfigureCertificateFileInput').onchange = function (event) {
$scope.$apply(function () {
$scope.appConfigure.certificateFile = null;
$scope.appConfigure.certificateFileName = event.target.files[0].name;
var reader = new FileReader();
reader.onload = function (result) {
if (!result.target || !result.target.result) return console.error('Unable to read local file');
$scope.appConfigure.certificateFile = result.target.result;
};
reader.readAsText(event.target.files[0]);
});
};
document.getElementById('appConfigureKeyFileInput').onchange = function (event) {
$scope.$apply(function () {
$scope.appConfigure.keyFile = null;
$scope.appConfigure.keyFileName = event.target.files[0].name;
var reader = new FileReader();
reader.onload = function (result) {
if (!result.target || !result.target.result) return console.error('Unable to read local file');
$scope.appConfigure.keyFile = result.target.result;
};
reader.readAsText(event.target.files[0]);
});
};
$scope.showConfigure = function (app) {
$scope.reset();
// fill relevant info from the app
$scope.appConfigure.app = app;
$scope.appConfigure.location = app.location;
$scope.appConfigure.oauthProxy = app.oauthProxy ? '1' : '';
$scope.appConfigure.portBindingsInfo = app.manifest.tcpPorts || {}; // Portbinding map only for information
$scope.appConfigure.portBindings = {}; // This is the actual model holding the env:port pair
$scope.appConfigure.portBindingsEnabled = {}; // This is the actual model holding the enabled/disabled flag
// fill the portBinding structures. There might be holes in the app.portBindings, which signalizes a disabled port
for (var env in $scope.appConfigure.portBindingsInfo) {
@@ -126,7 +162,9 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
location: $scope.appConfigure.location || '',
portBindings: finalPortBindings,
oauthProxy: !!$scope.appConfigure.oauthProxy,
accessRestriction: $scope.appConfigure.app.accessRestriction
accessRestriction: $scope.appConfigure.app.accessRestriction,
cert: $scope.appConfigure.certificateFile,
key: $scope.appConfigure.keyFile,
};
Client.configureApp($scope.appConfigure.app.id, $scope.appConfigure.password, data, function (error) {
@@ -141,6 +179,12 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
$scope.appConfigure.error.password = 'Wrong password provided.';
$scope.appConfigure.password = '';
$('#appConfigurePasswordInput').focus();
} else if (error.statusCode === 400 && error.message.indexOf('cert') !== -1 ) {
$scope.appConfigure.error.cert = error.message;
$scope.appConfigure.certificateFileName = '';
$scope.appConfigure.certificateFile = null;
$scope.appConfigure.keyFileName = '';
$scope.appConfigure.keyFile = null;
} else {
$scope.appConfigure.error.other = error.message;
}
+27 -2
View File
@@ -24,6 +24,7 @@
</div>
</div>
</div>
<div class="has-error text-center" ng-show="appInstall.error.port">{{ appInstall.error.port }}</div>
<div ng-repeat="(env, info) in appInstall.portBindingsInfo">
<ng-form name="portInfo_form">
@@ -33,13 +34,37 @@
</div>
</ng-form>
</div>
<div class="form-group" ng-show="appInstall.app.manifest.singleUser">
<label class="control-label" for="accessRestriction">User</label>
<p>This is a single user application.</p>
<select class="form-control" id="accessRestriction" ng-model="appInstall.accessRestriction" ng-required="appInstall.app.manifest.singleUser">
<option ng-repeat="user in users" value="{{user.id}}">{{user.username}} - {{user.email}}</option>
<select class="form-control" id="accessRestriction" ng-model="appInstall.accessRestriction" ng-options="user as user.username for user in users track by user.id" ng-required="appInstall.app.manifest.singleUser">
</select>
</div>
<br/>
<label class="control-label" for="appInstallCertificateInput" ng-show="config.isCustomDomain">Certificate (optional)</label>
<div class="has-error text-center" ng-show="appInstall.error.cert && config.isCustomDomain">{{ appInstall.error.cert }}</div>
<div class="form-group" ng-class="{ 'has-error': !appInstallForm.certificate.$dirty && appInstall.error.cert }" ng-show="config.isCustomDomain">
<div class="input-group">
<input type="file" id="appInstallCertificateFileInput" style="display:none"/>
<input type="text" class="form-control" placeholder="Certificate" ng-model="appInstall.certificateFileName" id="appInstallCertificateInput" name="certificate" onclick="getElementById('appInstallCertificateFileInput').click();" style="cursor: pointer;" ng-required="appInstall.keyFileName">
<span class="input-group-addon">
<i class="fa fa-upload" onclick="getElementById('appInstallCertificateFileInput').click();"></i>
</span>
</div>
</div>
<div class="form-group" ng-class="{ 'has-error': !appInstallForm.key.$dirty && appInstall.error.cert }" ng-show="config.isCustomDomain">
<div class="input-group">
<input type="file" id="appInstallKeyFileInput" style="display:none"/>
<input type="text" class="form-control" placeholder="Key" ng-model="appInstall.keyFileName" id="appInstallKeyInput" name="key" onclick="getElementById('appInstallKeyFileInput').click();" style="cursor: pointer;" ng-required="appInstall.certificateFileName">
<span class="input-group-addon">
<i class="fa fa-upload" onclick="getElementById('appInstallKeyFileInput').click();"></i>
</span>
</div>
</div>
<input class="ng-hide" type="submit" ng-disabled="appInstallForm.$invalid || busy"/>
</form>
</div>
+56 -4
View File
@@ -19,7 +19,11 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
portBindings: {},
accessRestriction: null,
oauthProxy: false,
mediaLinks: []
mediaLinks: [],
certificateFile: null,
certificateFileName: '',
keyFile: null,
keyFileName: ''
};
$scope.appNotFound = {
@@ -142,6 +146,11 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
$scope.appInstall.installFormVisible = false;
$scope.appInstall.resourceConstraintVisible = false;
$scope.appInstall.mediaLinks = [];
$scope.appInstall.certificateFile = null;
$scope.appInstall.certificateFileName = '';
$scope.appInstall.keyFile = null;
$scope.appInstall.keyFileName = '';
$('#collapseInstallForm').collapse('hide');
$('#collapseResourceConstraint').collapse('hide');
$('#collapseMediaLinksCarousel').collapse('show');
@@ -163,6 +172,34 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
}
};
document.getElementById('appInstallCertificateFileInput').onchange = function (event) {
$scope.$apply(function () {
$scope.appInstall.certificateFile = null;
$scope.appInstall.certificateFileName = event.target.files[0].name;
var reader = new FileReader();
reader.onload = function (result) {
if (!result.target || !result.target.result) return console.error('Unable to read local file');
$scope.appInstall.certificateFile = result.target.result;
};
reader.readAsText(event.target.files[0]);
});
};
document.getElementById('appInstallKeyFileInput').onchange = function (event) {
$scope.$apply(function () {
$scope.appInstall.keyFile = null;
$scope.appInstall.keyFileName = event.target.files[0].name;
var reader = new FileReader();
reader.onload = function (result) {
if (!result.target || !result.target.result) return console.error('Unable to read local file');
$scope.appInstall.keyFile = result.target.result;
};
reader.readAsText(event.target.files[0]);
});
};
$scope.showInstall = function (app) {
$scope.reset();
@@ -175,7 +212,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
$scope.appInstall.portBindingsInfo = $scope.appInstall.app.manifest.tcpPorts || {}; // Portbinding map only for information
$scope.appInstall.portBindings = {}; // This is the actual model holding the env:port pair
$scope.appInstall.portBindingsEnabled = {}; // This is the actual model holding the enabled/disabled flag
$scope.appInstall.accessRestriction = app.accessRestriction ? app.accessRestriction.users[0] : null;
$scope.appInstall.accessRestriction = app.accessRestriction ? app.accessRestriction.users[0] : $scope.user;
$scope.appInstall.oauthProxy = false;
// set default ports
@@ -208,10 +245,19 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
// translate to accessRestriction object
var accessRestriction = $scope.appInstall.app.manifest.singleUser ? {
users: [ $scope.appInstall.accessRestriction ]
users: [ $scope.appInstall.accessRestriction.id ]
} : null;
Client.installApp($scope.appInstall.app.id, $scope.appInstall.app.manifest, $scope.appInstall.app.title, { location: $scope.appInstall.location || '', portBindings: finalPortBindings, accessRestriction: accessRestriction, oauthProxy: $scope.appInstall.oauthProxy }, function (error) {
var data = {
location: $scope.appInstall.location || '',
portBindings: finalPortBindings,
accessRestriction: accessRestriction,
oauthProxy: $scope.appInstall.oauthProxy,
cert: $scope.appInstall.certificateFile,
key: $scope.appInstall.keyFile,
};
Client.installApp($scope.appInstall.app.id, $scope.appInstall.app.manifest, $scope.appInstall.app.title, data, function (error) {
if (error) {
if (error.statusCode === 409 && (error.message.indexOf('is reserved') !== -1 || error.message.indexOf('is already in use') !== -1)) {
$scope.appInstall.error.port = error.message;
@@ -221,6 +267,12 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
$('#appInstallLocationInput').focus();
} else if (error.statusCode === 402) {
$scope.appInstall.error.other = 'Unable to purchase this app<br/>Please make sure your payment is setup <a href="' + $scope.config.webServerOrigin + '/console.html#/userprofile" target="_blank">here</a>';
} else if (error.statusCode === 400 && error.message.indexOf('cert') !== -1 ) {
$scope.appInstall.error.cert = error.message;
$scope.appInstall.certificateFileName = '';
$scope.appInstall.certificateFile = null;
$scope.appInstall.keyFileName = '';
$scope.appInstall.keyFile = null;
} else {
$scope.appInstall.error.other = error.message;
}
+125
View File
@@ -0,0 +1,125 @@
<div style="max-width: 600px; margin: 0 auto;">
<div class="text-left">
<h1>DNS & Certs</h1>
</div>
</div>
<div style="max-width: 600px; margin: 0 auto;" ng-show="user.admin && config.isCustomDomain">
<div class="text-left">
<h3>DNS Credentials</h3>
</div>
</div>
<div class="card" style="margin-bottom: 15px;" ng-show="user.admin && config.isCustomDomain">
<div class="row">
<div class="col-md-12">
<p>Currently only Amazon <a href="https://aws.amazon.com/route53/">Route53</a> is supported. Let us know if you require a different DNS provider <a href="#/support">here</a>.</p>
<table width="100%">
<tr>
<td class="text-muted" style="vertical-align: top;">Access Key Id</td>
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ dnsConfig.accessKeyId }}</td>
</tr>
<tr>
<td class="text-muted" style="vertical-align: top;">Secret Access Key</td>
<td class="text-right" style="vertical-align: top; white-space: nowrap;"><i>hidden</i></td>
</tr>
<tr>
<td class="text-muted" style="vertical-align: top;"></td>
<td class="text-right" style="vertical-align: top;"><span class="text-success" ng-show="dnsCredentials.success"><b>Done</b></span> &nbsp; &nbsp; <button class="btn btn-outline btn-xs btn-primary" ng-show="!dnsCredentials.formVisible" ng-click="showDnsCredentialsForm()">Change</button></td>
</tr>
</table>
<div class="collapse" id="collapseDnsCredentialsForm" data-toggle="false">
<p>The security credentials have to be valid for full Route53 access.</p>
<form name="dnsCredentialsForm" ng-submit="setDnsCredentials()">
<fieldset>
<div class="has-error text-center" ng-show="dnsCredentials.error">{{ dnsCredentials.error }}</div>
<div class="form-group" ng-class="{ 'has-error': false }">
<label class="control-label" for="dnsCredentialsAccessKeyId">Access Key Id</label>
<input type="text" class="form-control" ng-model="dnsCredentials.accessKeyId" id="dnsCredentialsAccessKeyId" name="accessKeyId" ng-disabled="dnsCredentials.busy" ng-minlength="16" ng-maxlength="32" required>
</div>
<div class="form-group" ng-class="{ 'has-error': false }">
<label class="control-label" for="dnsCredentialsSecretAccessKey">Secret Access Key</label>
<input type="text" class="form-control" ng-model="dnsCredentials.secretAccessKey" id="dnsCredentialsSecretAccessKey" name="secretAccessKey" ng-disabled="dnsCredentials.busy" required>
</div>
<button type="submit" class="btn btn-outline btn-success pull-right" ng-disabled="dnsCredentialsForm.$invalid || busy"><i class="fa fa-spinner fa-pulse" ng-show="dnsCredentials.busy"></i> Save</button>
</fieldset>
</form>
</div>
</div>
</div>
</div>
<div style="max-width: 600px; margin: 0 auto;" ng-show="user.admin && config.isCustomDomain">
<div class="text-left">
<h3>SSL Certificates</h3>
</div>
</div>
<div class="card" style="margin-bottom: 15px;" ng-show="user.admin && config.isCustomDomain">
<div class="row">
<div class="col-md-12">
<form name="defaultCertForm" ng-submit="setDefaultCert()">
<fieldset>
<label class="control-label" for="defaultCertInput">Fallback Certificate</label>
<p>This certificate has to be wildcard certificates and will be used for all apps, which were not configured to use a specific certificate.</p>
<div class="has-error text-center" ng-show="defaultCert.error">{{ defaultCert.error }}</div>
<div class="text-success text-center" ng-show="defaultCert.success"><b>Upload successful</b></div>
<div class="form-group" ng-class="{ 'has-error': (!defaultCert.cert.$dirty && defaultCert.error) }">
<div class="input-group">
<input type="file" id="defaultCertFileInput" style="display:none"/>
<input type="text" class="form-control" placeholder="Certificate" ng-model="defaultCert.certificateFileName" id="defaultCertInput" name="cert" onclick="getElementById('defaultCertFileInput').click();" style="cursor: pointer;" ng-disabled="defaultCert.busy" required>
<span class="input-group-addon">
<i class="fa fa-upload" onclick="getElementById('defaultCertFileInput').click();"></i>
</span>
</div>
</div>
<div class="form-group" ng-class="{ 'has-error': (!defaultCert.key.$dirty && defaultCert.error) }">
<div class="input-group">
<input type="file" id="defaultKeyFileInput" style="display:none"/>
<input type="text" class="form-control" placeholder="Key" ng-model="defaultCert.keyFileName" id="defaultKeyInput" name="key" onclick="getElementById('defaultKeyFileInput').click();" style="cursor: pointer;" ng-disabled="defaultCert.busy" required>
<span class="input-group-addon">
<i class="fa fa-upload" onclick="getElementById('defaultKeyFileInput').click();"></i>
</span>
</div>
</div>
<button type="submit" class="btn btn-outline btn-success pull-right" ng-disabled="defaultCertForm.$invalid || busy"><i class="fa fa-spinner fa-pulse" ng-show="defaultCert.busy"></i> Upload</button>
</fieldset>
</form>
</div>
</div>
<div class="row">
<div class="col-md-12">
<form name="adminCertForm" ng-submit="setAdminCert()">
<fieldset>
<label class="control-label" for="adminCertInput">Settings Certificate</label>
<p>This certificate will be used for this Settings application.</p>
<div class="has-error text-center" ng-show="adminCert.error">{{ adminCert.error }}</div>
<div class="text-success text-center" ng-show="adminCert.success"><b>Upload successful</b></div>
<div class="form-group" ng-class="{ 'has-error': (!adminCert.cert.$dirty && adminCert.error) }">
<div class="input-group">
<input type="file" id="adminCertFileInput" style="display:none"/>
<input type="text" class="form-control" placeholder="Certificate" ng-model="adminCert.certificateFileName" id="adminCertInput" name="cert" onclick="getElementById('adminCertFileInput').click();" style="cursor: pointer;" ng-disabled="adminCert.busy" required>
<span class="input-group-addon">
<i class="fa fa-upload" onclick="getElementById('adminCertFileInput').click();"></i>
</span>
</div>
</div>
<div class="form-group" ng-class="{ 'has-error': (!adminCert.key.$dirty && adminCert.error) }">
<div class="input-group">
<input type="file" id="adminKeyFileInput" style="display:none"/>
<input type="text" class="form-control" placeholder="Key" ng-model="adminCert.keyFileName" id="adminKeyInput" name="key" onclick="getElementById('adminKeyFileInput').click();" style="cursor: pointer;" ng-disabled="adminCert.busy" required>
<span class="input-group-addon">
<i class="fa fa-upload" onclick="getElementById('adminKeyFileInput').click();"></i>
</span>
</div>
</div>
<button type="submit" class="btn btn-outline btn-success pull-right" ng-disabled="adminCertForm.$invalid || busy"><i class="fa fa-spinner fa-pulse" ng-show="adminCert.busy"></i> Upload</button>
</fieldset>
</form>
</div>
</div>
</div>
+151
View File
@@ -0,0 +1,151 @@
'use strict';
angular.module('Application').controller('CertsController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); });
$scope.defaultCert = {
error: null,
success: false,
busy: false,
certificateFile: null,
certificateFileName: '',
keyFile: null,
keyFileName: ''
};
$scope.adminCert = {
error: null,
success: false,
busy: false,
certificateFile: null,
certificateFileName: '',
keyFile: null,
keyFileName: ''
};
$scope.dnsCredentials = {
error: null,
success: false,
busy: false,
formVisible: false,
accessKeyId: '',
secretAccessKey: '',
provider: 'route53'
};
function readFileLocally(obj, file, fileName) {
return function (event) {
$scope.$apply(function () {
obj[file] = null;
obj[fileName] = event.target.files[0].name;
var reader = new FileReader();
reader.onload = function (result) {
if (!result.target || !result.target.result) return console.error('Unable to read local file');
obj[file] = result.target.result;
};
reader.readAsText(event.target.files[0]);
});
};
}
document.getElementById('defaultCertFileInput').onchange = readFileLocally($scope.defaultCert, 'certificateFile', 'certificateFileName');
document.getElementById('defaultKeyFileInput').onchange = readFileLocally($scope.defaultCert, 'keyFile', 'keyFileName');
document.getElementById('adminCertFileInput').onchange = readFileLocally($scope.adminCert, 'certificateFile', 'certificateFileName');
document.getElementById('adminKeyFileInput').onchange = readFileLocally($scope.adminCert, 'keyFile', 'keyFileName');
$scope.setDefaultCert = function () {
$scope.defaultCert.busy = true;
$scope.defaultCert.error = null;
$scope.defaultCert.success = false;
Client.setCertificate($scope.defaultCert.certificateFile, $scope.defaultCert.keyFile, function (error) {
if (error) {
$scope.defaultCert.error = error.message;
} else {
$scope.defaultCert.success = true;
$scope.defaultCert.certificateFileName = '';
$scope.defaultCert.keyFileName = '';
}
$scope.defaultCert.busy = false;
});
};
$scope.setAdminCert = function () {
$scope.adminCert.busy = true;
$scope.adminCert.error = null;
$scope.adminCert.success = false;
Client.setAdminCertificate($scope.adminCert.certificateFile, $scope.adminCert.keyFile, function (error) {
if (error) {
$scope.adminCert.error = error.message;
} else {
$scope.adminCert.success = true;
$scope.adminCert.certificateFileName = '';
$scope.adminCert.keyFileName = '';
}
$scope.adminCert.busy = false;
// attempt to reload to make the browser get the new certs
window.location.reload(true);
});
};
$scope.setDnsCredentials = function () {
$scope.dnsCredentials.busy = true;
$scope.dnsCredentials.error = null;
$scope.dnsCredentials.success = false;
var data = {
provider: $scope.dnsCredentials.provider,
accessKeyId: $scope.dnsCredentials.accessKeyId,
secretAccessKey: $scope.dnsCredentials.secretAccessKey
};
Client.setDnsConfig(data, function (error) {
if (error) {
$scope.dnsCredentials.error = error.message;
} else {
$scope.dnsCredentials.success = true;
$scope.dnsConfig.accessKeyId = $scope.dnsCredentials.accessKeyId;
$scope.dnsConfig.secretAccessKey = $scope.dnsCredentials.secretAccessKey;
$scope.dnsCredentials.accessKeyId = '';
$scope.dnsCredentials.secretAccessKey = '';
$('#collapseDnsCredentialsForm').collapse('hide');
$scope.dnsCredentials.formVisible = false;
// attempt to reload to make the browser get the new certs
window.location.reload(true);
}
$scope.dnsCredentials.busy = false;
});
};
$scope.showDnsCredentialsForm = function () {
$scope.dnsCredentials.busy = false;
$scope.dnsCredentials.success = false;
$scope.dnsCredentials.error = null;
$scope.dnsCredentials.accessKeyId = '';
$scope.dnsCredentials.secretAccessKey = '';
$scope.dnsCredentialsForm.$setPristine();
$scope.dnsCredentialsForm.$setUntouched();
$scope.dnsCredentials.formVisible = true;
$('#collapseDnsCredentialsForm').collapse('show');
$('#dnsCredentialsAccessKeyId').focus();
};
Client.onReady(function () {
Client.getDnsConfig(function (error, result) {
if (error) return console.error(error);
$scope.dnsConfig = result;
});
});
}]);
-46
View File
@@ -1,46 +0,0 @@
<div class="row">
<div class="col-lg-12">
<h1>DNS Management</h1>
</div>
</div>
<div class="row">
<div class="col-md-6 grid-item">
<div class="grid-item-content">
<div class="grid-item-top">
<big>Certificate</big>
</div>
<div class="grid-item-bottom text-right">
<ul class="list-group">
<li class="list-group-item">
<input type="file" id="idCertificate" style="display:none"/>
<div class="input-group">
<span class="input-group-btn">
<button class="btn btn-default" type="button" onclick="getElementById('idCertificate').click();">Certificate</button>
</span>
<input type="text" class="form-control" ng-model="certificateFileName" onclick="getElementById('idCertificate').click();" style="cursor: pointer;"/>
<span class="input-group-addon">
<i class="fa fa-upload" onclick="getElementById('idCertificate').click();"></i>
</span>
</div>
</li>
<li class="list-group-item">
<input type="file" id="idKey" style="display:none"/>
<div class="input-group">
<span class="input-group-btn">
<button class="btn btn-default" type="button" onclick="getElementById('idKey').click();">Key</button>
</span>
<input type="text" class="form-control" ng-model="keyFileName" onclick="getElementById('idKey').click();" style="cursor: pointer;"/>
<span class="input-group-addon">
<i class="fa fa-upload" onclick="getElementById('idKey').click();"></i>
</span>
</div>
</li>
</ul>
<button class="btn btn-outline btn-success" ng-click="setCertificate()">Upload Certificate</button>
</div>
</div>
</div>
</div>
-35
View File
@@ -1,35 +0,0 @@
'use strict';
angular.module('Application').controller('DnsController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); });
$scope.certificateFile = null;
$scope.certificateFileName = '';
$scope.keyFile = null;
$scope.keyFileName = '';
document.getElementById('idCertificate').onchange = function (event) {
$scope.$apply(function () {
$scope.certificateFile = event.target.files[0];
$scope.certificateFileName = event.target.files[0].name;
});
};
document.getElementById('idKey').onchange = function (event) {
$scope.$apply(function () {
$scope.keyFile = event.target.files[0];
$scope.keyFileName = event.target.files[0].name;
});
};
$scope.setCertificate = function () {
if (!$scope.certificateFile) return console.log('Certificate not set');
if (!$scope.keyFile) return console.log('Key not set');
Client.setCertificate($scope.certificateFile, $scope.keyFile, function (error) {
if (error) return console.error(error);
window.setTimeout(window.location.reload.bind(window.location, true), 3000);
});
};
}]);
+28 -28
View File
@@ -85,6 +85,34 @@
</div>
</div>
<div style="max-width: 600px; margin: 0 auto;" ng-show="user.admin">
<div class="text-left">
<h3>About</h3>
</div>
</div>
<div class="card" style="margin-bottom: 15px;" ng-show="user.admin">
<div class="row">
<div class="col-xs-4" style="min-width: 150px;">
<div class="settings-avatar" ng-click="showChangeAvatar()" style="background-image: url('{{avatar.data || avatar.url}}');">
<div class="overlay"></div>
</div>
</div>
<div class="col-xs-8">
<table width="100%">
<tr>
<td class="text-muted" style="vertical-align: top;">Model</td>
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ config.size }} - {{ config.region }}</td>
</tr>
<tr>
<td class="text-muted" style="vertical-align: top;">Version</td>
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ config.version }}</td>
</tr>
</table>
</div>
</div>
</div>
<div style="max-width: 600px; margin: 0 auto;" ng-show="user.admin">
<div class="text-left">
<h3>Backups</h3>
@@ -115,34 +143,6 @@
</div>
</div>
<div style="max-width: 600px; margin: 0 auto;" ng-show="user.admin">
<div class="text-left">
<h3>About</h3>
</div>
</div>
<div class="card" style="margin-bottom: 15px;" ng-show="user.admin">
<div class="row">
<div class="col-xs-4" style="min-width: 150px;">
<div class="settings-avatar" ng-click="showChangeAvatar()" style="background-image: url('{{avatar.data || avatar.url}}');">
<div class="overlay"></div>
</div>
</div>
<div class="col-xs-8">
<table width="100%">
<tr>
<td class="text-muted" style="vertical-align: top;">Model</td>
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ config.size }} - {{ config.region }}</td>
</tr>
<tr>
<td class="text-muted" style="vertical-align: top;">Version</td>
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ config.version }}</td>
</tr>
</table>
</div>
</div>
</div>
<div style="max-width: 600px; margin: 0 auto;" ng-show="user.admin">
<div class="text-left">
<h3>Developer Mode</h3>
+2 -1
View File
@@ -5,6 +5,7 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
$scope.user = Client.getUserInfo();
$scope.config = Client.getConfig();
$scope.dnsConfig = {};
$scope.lastBackup = null;
$scope.backups = [];
@@ -262,7 +263,7 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
Client.onReady(function () {
fetchBackups();
$scope.avatar.url = '//my-' + $scope.config.fqdn + '/api/v1/cloudron/avatar';
$scope.avatar.url = ($scope.config.isCustomDomain ? '//my.' : '//my-') + $scope.config.fqdn + '/api/v1/cloudron/avatar';
});
// setup all the dialog focus handling
+1 -1
View File
@@ -38,6 +38,6 @@
<div class="row">
<div class="col-md-12 text-center">
<a class="btn btn-primary" href="#/step2">Next</a>
<button class="btn btn-primary" ng-click="next()">Next</button>
</div>
</div>
+2 -2
View File
@@ -16,12 +16,12 @@
</div>
<div class="form-group" ng-class="{ 'has-error': setup_form.password.$dirty && setup_form.password.$invalid }">
<!-- <label class="control-label" for="inputPassword">Password</label> -->
<input type="password" class="form-control" ng-model="wizard.password" id="inputPassword" name="password" placeholder="Password" ng-enter="next('/step3', setup_form.password.$invalid)" ng-maxlength="512" ng-minlength="5" required autocomplete="off">
<input type="password" class="form-control" ng-model="wizard.password" id="inputPassword" name="password" placeholder="Password" ng-enter="next(setup_form.password.$invalid)" ng-maxlength="512" ng-minlength="5" required autocomplete="off">
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 text-center">
<a class="btn btn-primary" href="#/step3" ng-disabled="setup_form.username.$invalid">Done</a>
<button class="btn btn-primary" ng-click="next(setup_form.username.$invalid || setup_form.password.$invalid)" ng-disabled="setup_form.username.$invalid || setup_form.password.$invalid">Done</button>
</div>
</div>
+27 -8
View File
@@ -1,8 +1,27 @@
<center>
<h1>All done!</h1>
<br/>
<br/>
<i class="fa fa-spinner fa-pulse fa-5x"></i>
<br/>
<br/>
</center>
<div class="row">
<div class="col-md-12 text-center">
<h1>Custom domain configuration</h1>
<h4 class="">
Provide <a href="https://aws.amazon.com/route53/">Route53</a> access keys here
</h4>
</div>
</div>
<br/>
<br/>
<div class="row">
<div class="col-md-4 col-md-offset-4 text-center">
<div class="form-group" ng-class="{ 'has-error': setup_form.accessKeyId.$dirty && setup_form.accessKeyId.$invalid }">
<!-- <label class="control-label" for="inputUsername">Username</label> -->
<input type="text" class="form-control" ng-model="wizard.dnsConfig.accessKeyId" id="inputAccessKeyId" name="accessKeyId" placeholder="Access Key Id" ng-enter="focusNext('inputSecretAccessKey', setup_form.accessKeyId.$invalid)" ng-maxlength="512" ng-minlength="3" autofocus required autocomplete="off">
</div>
<div class="form-group" ng-class="{ 'has-error': setup_form.secretAccessKey.$dirty && setup_form.secretAccessKey.$invalid }">
<!-- <label class="control-label" for="inputPassword">Password</label> -->
<input type="text" class="form-control" ng-model="wizard.dnsConfig.secretAccessKey" id="inputSecretAccessKey" name="secretAccessKey" placeholder="Secret Access Key" ng-enter="next(setup_form.secretAccessKey.$invalid)" ng-maxlength="512" ng-minlength="3" required autocomplete="off">
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 text-center">
<button class="btn btn-primary" ng-click="next(setup_form.accessKeyId.$invalid || setup_form.secretAccessKey.$invalid)" ng-disabled="setup_form.accessKeyId.$invalid || setup_form.secretAccessKey.$invalid">Done</button>
</div>
</div>
+8
View File
@@ -0,0 +1,8 @@
<center>
<h1>All done!</h1>
<br/>
<br/>
<i class="fa fa-spinner fa-pulse fa-5x"></i>
<br/>
<br/>
</center>