Compare commits

...

323 Commits

Author SHA1 Message Date
Girish Ramakrishnan 6bd9173a9d this docker registry keeps going down 2015-11-12 16:22:53 -08:00
Girish Ramakrishnan 0cef3e1090 do not trust the health state blindly 2015-11-12 16:16:05 -08:00
Girish Ramakrishnan 6bd68961d1 typo 2015-11-12 16:13:15 -08:00
Girish Ramakrishnan 7f8ad917d9 filter out non-healthy apps 2015-11-12 16:04:33 -08:00
Girish Ramakrishnan 7cd89accaf better pullImage debug output 2015-11-12 15:58:39 -08:00
Girish Ramakrishnan ffee084d2b new format of provisioning info 2015-11-12 14:22:43 -08:00
Girish Ramakrishnan 2bb657a733 rename variable for clarity 2015-11-12 12:40:41 -08:00
Girish Ramakrishnan bc48171626 use fallback cert from backup if it exists 2015-11-12 12:37:43 -08:00
Girish Ramakrishnan 50924b0cd3 use admin.cert and admin.key if present in backup dir 2015-11-12 12:33:52 -08:00
Girish Ramakrishnan 3d86950cc9 fix indentation 2015-11-12 12:28:05 -08:00
Girish Ramakrishnan db9ddf9969 backup fallback cert 2015-11-12 12:27:25 -08:00
Girish Ramakrishnan 1b507370dc Cannot use >= node 4.1.2
https://github.com/nodejs/node/issues/3803
2015-11-12 12:19:13 -08:00
Girish Ramakrishnan 3c5e221c39 change engine requirements 2015-11-11 15:55:59 -08:00
Girish Ramakrishnan 9c37f35d5a new shrinkwrap for 4.2.2 2015-11-11 15:55:24 -08:00
Girish Ramakrishnan 4044070d76 Add -app prefix for all app sources
so that this doesn't conflict with some user.
2015-11-11 13:27:45 -08:00
Girish Ramakrishnan 8f05917d97 delete container on network error 2015-11-10 21:56:17 -08:00
Girish Ramakrishnan 3766d67daa create new container from cloudron exec 2015-11-10 21:36:20 -08:00
Johannes Zellner b1290c073e log lines should be newline separated 2015-11-10 11:31:07 +01:00
Girish Ramakrishnan 36daf86ea2 send mail even if no related app was found (for addons) 2015-11-10 01:39:02 -08:00
Girish Ramakrishnan 4fb07a6ab3 make crashnotifier send mails again
mailer module waits for dns syncing. crashnotifier has no time for all that.
neither does it initialize the database. it simply wants to send mail.
(the crash itself could have happenned because of some db issue)

maybe it should simply use a shell script at some point.
2015-11-10 00:25:47 -08:00
Girish Ramakrishnan 8f2119272b print all missing images 2015-11-09 23:31:04 -08:00
Girish Ramakrishnan ee5bd456e0 set bucket and prefix to make migrate test pass 2015-11-09 22:45:07 -08:00
Girish Ramakrishnan 9c549ed4d8 wait for old apptask to finish before starting new one
kill() is async and takes no callback :/
2015-11-09 22:10:10 -08:00
Girish Ramakrishnan 61fc8b7968 better taskmanager debugs 2015-11-09 21:58:34 -08:00
Girish Ramakrishnan 6b30d65e05 add backupConfig in test 2015-11-09 21:08:23 -08:00
Girish Ramakrishnan 10c876ac75 make migrate test work 2015-11-09 20:34:27 -08:00
Girish Ramakrishnan 0966bd0bb1 use debug instead of console.error 2015-11-09 19:10:33 -08:00
Girish Ramakrishnan 294d1bfca4 remove ununsed require 2015-11-09 18:57:57 -08:00
Girish Ramakrishnan af1d1236ea expose api server origin to determine if the box is dev/staging/prod 2015-11-09 17:50:09 -08:00
Girish Ramakrishnan eaf9febdfd do not save aws and backupKey
it is now part of backupConfig
2015-11-09 08:39:01 -08:00
Johannes Zellner 8748226ef3 The controller already ensures we don't show the views here 2015-11-09 09:56:59 +01:00
Johannes Zellner 73568777c0 Only show dns and cert pages for custom domain cloudrons 2015-11-09 09:56:08 +01:00
Girish Ramakrishnan c64697dde7 cannot read propery provider of null 2015-11-09 00:42:25 -08:00
Girish Ramakrishnan 0701e38a04 do not set dnsConfig for caas on activation 2015-11-09 00:20:16 -08:00
Girish Ramakrishnan 2a27d96e08 pass dnsConfig in update 2015-11-08 23:57:42 -08:00
Girish Ramakrishnan ba42611701 token is not a function 2015-11-08 23:54:47 -08:00
Girish Ramakrishnan 54486138f0 pass dnsConfig to backend api 2015-11-08 23:21:55 -08:00
Girish Ramakrishnan 13d3f506b0 always add dns config in tests 2015-11-08 23:05:55 -08:00
Girish Ramakrishnan 32ca686e1f read dnsConfig from settings to choose api backend 2015-11-08 22:55:31 -08:00
Girish Ramakrishnan a5ef9ff372 Add getAllPaged to storage api 2015-11-08 22:41:13 -08:00
Girish Ramakrishnan 738bfa7601 remove unused variable 2015-11-08 11:04:39 -08:00
Girish Ramakrishnan 40cdd270b1 ensure correct token is used in tests 2015-11-07 22:19:23 -08:00
Girish Ramakrishnan 53a2a8015e set the backupConfig in backups test 2015-11-07 22:18:39 -08:00
Girish Ramakrishnan 15aaa440a2 Add test for settings.backupConfig 2015-11-07 22:17:08 -08:00
Girish Ramakrishnan d8a4014eff remove bad reference to config.aws 2015-11-07 22:12:03 -08:00
Girish Ramakrishnan d25d423ccd remove legacy update params
these are now part of settings
2015-11-07 22:07:25 -08:00
Girish Ramakrishnan 49b0fde18b remove config.aws and config.backupKey 2015-11-07 22:06:56 -08:00
Girish Ramakrishnan 8df7f17186 load backup config from settingsdb 2015-11-07 22:06:09 -08:00
Girish Ramakrishnan adc395f888 save backupConfig in db 2015-11-07 21:45:38 -08:00
Girish Ramakrishnan e770664365 Add backup config to settings 2015-11-07 18:02:45 -08:00
Girish Ramakrishnan 05d4ad3b5d read new format of restore keys 2015-11-07 17:53:54 -08:00
Girish Ramakrishnan cc6f726f71 change backup provider to caas 2015-11-07 09:17:58 -08:00
Girish Ramakrishnan a4923f894c prepare for new backupConfig 2015-11-07 00:26:12 -08:00
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
Johannes Zellner f72d89fa76 Replace the ugly oauth proxy checkbox 2015-10-22 13:18:58 +02:00
Johannes Zellner f9f4a8e7ad Check memory availability if an app can be installed or not 2015-10-22 11:16:55 +02:00
Johannes Zellner fd58e83da9 Provide the memory byte count with the cloudron config route 2015-10-22 11:16:55 +02:00
Johannes Zellner bfcedfdb2a Add node module bytes 2015-10-22 11:16:55 +02:00
Johannes Zellner d11e030150 Add resource constrait view on app installation attempt 2015-10-22 11:16:55 +02:00
Johannes Zellner 6103640b53 Remove leftover console.log() 2015-10-22 11:16:55 +02:00
Girish Ramakrishnan 259199897b update test image 2015-10-21 09:16:04 -07:00
Johannes Zellner ee498b9e2b A readable stream does not have .end() 2015-10-21 17:25:14 +02:00
Johannes Zellner 18a464b1d2 Make data/ directory writeable by yellowtent user 2015-10-21 17:18:45 +02:00
Johannes Zellner d1c8e34540 dns in sync file should be under data/ 2015-10-21 17:18:39 +02:00
Johannes Zellner a151846f1c Use config.(set)dnsInSync()
Fixes #520
2015-10-21 16:44:03 +02:00
Johannes Zellner 9f19b0bc9e Use a persistent file for dns sync flag 2015-10-21 16:42:17 +02:00
Johannes Zellner 289fe76adc Avoid network request for access token verification in oauth proxy 2015-10-21 16:23:15 +02:00
Johannes Zellner 1eb1c44926 Clear oauthproxy session in case the access token is invalid 2015-10-21 15:57:18 +02:00
Girish Ramakrishnan bc09e4204b use debug instead of console.error 2015-10-20 19:03:34 -07:00
Girish Ramakrishnan 1a2948df85 VolumesFrom is part of HostConfig 2015-10-20 17:34:47 -07:00
Girish Ramakrishnan 16df15cf55 containerId does not mean it is running 2015-10-20 16:56:57 -07:00
Girish Ramakrishnan 0566bad6d9 bump infra version 2015-10-20 15:07:35 -07:00
Girish Ramakrishnan edc90ccc00 bump test image 2015-10-20 14:40:27 -07:00
Girish Ramakrishnan 3688602d16 test the scheduler 2015-10-20 14:30:50 -07:00
Girish Ramakrishnan 0deadc5cf2 autodetect image id 2015-10-20 13:07:25 -07:00
Girish Ramakrishnan 10ac435d53 addons is mandatory 2015-10-20 12:57:00 -07:00
Girish Ramakrishnan 16f025181f ensure boolean 2015-10-20 12:49:02 -07:00
Girish Ramakrishnan 3808f60e69 appState can be null 2015-10-20 12:32:50 -07:00
Girish Ramakrishnan a00615bd4e manifest always has addons 2015-10-20 12:27:23 -07:00
Girish Ramakrishnan 14bc2c7232 rename isSubcontainer -> isAppContainer 2015-10-20 10:55:06 -07:00
Girish Ramakrishnan 76d286703c ignore portBindings and exportPorts for subcontainers 2015-10-20 10:42:35 -07:00
Girish Ramakrishnan c80a5b59ab do not dump containerOptions 2015-10-20 10:27:53 -07:00
Girish Ramakrishnan db6882e9f5 do not kill containers on restart 2015-10-20 10:22:42 -07:00
Girish Ramakrishnan 3fd9d9622b schedulerConfig cannot be null 2015-10-20 09:44:46 -07:00
Girish Ramakrishnan 5ae4c891de scheduler: sync more often to catch bugs sooner 2015-10-20 09:36:55 -07:00
Girish Ramakrishnan fb2e7cb009 scheduler: crash fixes 2015-10-20 09:36:30 -07:00
Johannes Zellner 8124f0ac7f Remove cloudron name from the setup wizard 2015-10-20 13:36:25 +02:00
Johannes Zellner 446f571bec The activate route does not take a cloudron name anymore 2015-10-20 13:12:37 +02:00
Johannes Zellner 142ae76542 Finally remove the cloudron name from the api wrapper and index page 2015-10-20 12:59:57 +02:00
Johannes Zellner ed1873f47e Remove cloudron name related UI in the settings view 2015-10-20 12:57:51 +02:00
Johannes Zellner 0ee04e6ef3 Remove cloudron name usage in error.html 2015-10-20 12:55:30 +02:00
Johannes Zellner 1e4475b275 Remove cloudron name usage in naked domain page 2015-10-20 12:46:45 +02:00
Johannes Zellner 9dd9743943 Do not use cloudron name in appstatus 2015-10-20 12:41:52 +02:00
Johannes Zellner 5fbcebf80b Stop using the cloudron name in the oauth views 2015-10-20 12:31:16 +02:00
Girish Ramakrishnan 852b016389 scheduler: do not save cronjob object in state
the cronjob object has lots of js stuff and stringify fails
2015-10-20 01:31:11 -07:00
Girish Ramakrishnan 73f28d7653 put back request 2015-10-20 00:20:21 -07:00
Girish Ramakrishnan 1f28678c27 scheduler: make it work 2015-10-20 00:05:19 -07:00
Girish Ramakrishnan daba68265c stop all containers of an app 2015-10-20 00:05:19 -07:00
Girish Ramakrishnan 6d04481c27 fix debug tag 2015-10-19 23:38:55 -07:00
Girish Ramakrishnan ed5d6f73bb scheduler: fix require 2015-10-19 22:42:13 -07:00
Girish Ramakrishnan d0360e9e68 scheduler: load/save state 2015-10-19 22:41:42 -07:00
Girish Ramakrishnan 32ddda404c explicitly specify all to 0 (this is the default) 2015-10-19 22:09:38 -07:00
Girish Ramakrishnan 41de667e3d do not set container name (we use labels instead) 2015-10-19 22:09:38 -07:00
Girish Ramakrishnan 8530e70af6 delete all containers of an app 2015-10-19 22:09:34 -07:00
Girish Ramakrishnan 7a840ad15f scheduler: make stopJobs async 2015-10-19 21:36:55 -07:00
Girish Ramakrishnan 682c2721d2 scheduler: kill existing tasks if they are still running 2015-10-19 21:36:23 -07:00
Girish Ramakrishnan fb56795cbd merge start options into hostconfig 2015-10-19 21:35:02 -07:00
Girish Ramakrishnan 15aa4ecc5d Add docker.createSubcontainer 2015-10-19 21:33:53 -07:00
Girish Ramakrishnan 351d7d22fb rename tasks to tasksConfig 2015-10-19 16:29:28 -07:00
Girish Ramakrishnan 79999887a9 job -> cronJob 2015-10-19 16:27:03 -07:00
Girish Ramakrishnan 25d74ed649 createContainer takes optional command 2015-10-19 16:22:35 -07:00
Girish Ramakrishnan 9346666b3e add labels to container 2015-10-19 16:01:04 -07:00
Girish Ramakrishnan 13453552b5 createContainer only takes app object 2015-10-19 16:00:40 -07:00
Girish Ramakrishnan ef38074b55 add asserts 2015-10-19 15:51:02 -07:00
Girish Ramakrishnan e5e8eea7ac make it work without app object 2015-10-19 15:45:43 -07:00
Girish Ramakrishnan 9be2efc4f2 downloadImage only requires manifest now 2015-10-19 15:37:57 -07:00
Girish Ramakrishnan 990b7a2d20 implement scheduler
- scan for apps every 10 minutes and schedules tasks
- uses docker.exec
    - there is no way to control exec container. docker developers
      feel exec is for debugging purposes primarily
- future version will be based on docker run instead

part of #519
2015-10-19 14:53:34 -07:00
Girish Ramakrishnan 8d6dd62ef4 refactor container code into docker.js 2015-10-19 14:44:01 -07:00
Girish Ramakrishnan 69d09e8133 use docker.connection 2015-10-19 14:09:20 -07:00
Girish Ramakrishnan 6671b211e0 export a connection property from docker.js 2015-10-19 11:24:21 -07:00
Girish Ramakrishnan 307e815e97 remove unused require 2015-10-19 11:18:50 -07:00
Girish Ramakrishnan d8e2bd6ff5 Refactor docker.js to not have mac stuff 2015-10-19 11:14:11 -07:00
Girish Ramakrishnan e74c2f686b remove unused require 2015-10-19 11:05:31 -07:00
Girish Ramakrishnan c7d5115a56 Remove vbox.js
... and all related mac code. It's totally untested at this point and
most likely doesn't work
2015-10-19 10:54:36 -07:00
Girish Ramakrishnan 774ba11a92 Move HostConfig to createContainer
Newer docker has obsoleted HostConfig in start container
2015-10-19 10:38:46 -07:00
Girish Ramakrishnan 322edbdc20 getByAppIdAndType 2015-10-19 08:58:07 -07:00
Johannes Zellner c1ba551e07 Cleanup some of the html form elements 2015-10-19 10:31:19 +02:00
Johannes Zellner 9917412329 Indicate during app installation and configuration if the app is a single user app 2015-10-19 10:29:51 +02:00
Girish Ramakrishnan 2f4adb4d5f keep addon listing alphabetical 2015-10-18 20:06:26 -07:00
Girish Ramakrishnan b61b864094 make callback noop 2015-10-17 13:57:19 -07:00
85 changed files with 5477 additions and 5157 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

-5
View File
@@ -36,12 +36,7 @@ function main() {
var processName = process.argv[2]; var processName = process.argv[2];
console.log('Started crash notifier for', processName); console.log('Started crash notifier for', processName);
mailer.initialize(function (error) {
if (error) return console.error(error);
sendCrashNotification(processName); sendCrashNotification(processName);
});
} }
main(); main();
+1778 -2997
View File
File diff suppressed because it is too large Load Diff
+6 -3
View File
@@ -10,13 +10,14 @@
"type": "git" "type": "git"
}, },
"engines": [ "engines": [
"node >= 0.12.0" "node >=4.0.0 <=4.1.1"
], ],
"dependencies": { "dependencies": {
"async": "^1.2.1", "async": "^1.2.1",
"aws-sdk": "^2.1.46", "aws-sdk": "^2.1.46",
"body-parser": "^1.13.1", "body-parser": "^1.13.1",
"cloudron-manifestformat": "^1.9.1", "bytes": "^2.1.0",
"cloudron-manifestformat": "^2.0.0",
"connect-ensure-login": "^0.1.1", "connect-ensure-login": "^0.1.1",
"connect-lastmile": "0.0.13", "connect-lastmile": "0.0.13",
"connect-timeout": "^1.5.0", "connect-timeout": "^1.5.0",
@@ -61,7 +62,8 @@
"tail-stream": "https://registry.npmjs.org/tail-stream/-/tail-stream-0.2.1.tgz", "tail-stream": "https://registry.npmjs.org/tail-stream/-/tail-stream-0.2.1.tgz",
"underscore": "^1.7.0", "underscore": "^1.7.0",
"valid-url": "^1.0.9", "valid-url": "^1.0.9",
"validator": "^3.30.0" "validator": "^3.30.0",
"x509": "^0.2.2"
}, },
"devDependencies": { "devDependencies": {
"apidoc": "*", "apidoc": "*",
@@ -84,6 +86,7 @@
"nock": "^2.6.0", "nock": "^2.6.0",
"node-sass": "^3.0.0-alpha.0", "node-sass": "^3.0.0-alpha.0",
"redis": "^0.12.1", "redis": "^0.12.1",
"request": "^2.65.0",
"sinon": "^1.12.2", "sinon": "^1.12.2",
"yargs": "^3.15.0" "yargs": "^3.15.0"
}, },
+2 -2
View File
@@ -3,7 +3,7 @@
# If you change the infra version, be sure to put a warning # If you change the infra version, be sure to put a warning
# in the change log # in the change log
INFRA_VERSION=17 INFRA_VERSION=20
# WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING # WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
# These constants are used in the installer script as well # 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 POSTGRESQL_IMAGE=cloudron/postgresql:0.7.0
MONGODB_IMAGE=cloudron/mongodb: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 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 GRAPHITE_IMAGE=cloudron/graphite:0.7.0
MYSQL_REPO=cloudron/mysql MYSQL_REPO=cloudron/mysql
+8 -8
View File
@@ -16,8 +16,8 @@ arg_tls_key=""
arg_token="" arg_token=""
arg_version="" arg_version=""
arg_web_server_origin="" arg_web_server_origin=""
arg_backup_key="" arg_backup_config=""
arg_aws="" arg_dns_config=""
args=$(getopt -o "" -l "data:,retire" -n "$0" -- "$@") args=$(getopt -o "" -l "data:,retire" -n "$0" -- "$@")
eval set -- "${args}" eval set -- "${args}"
@@ -37,17 +37,17 @@ EOF
arg_tls_cert=$(echo "$2" | $json tlsCert) arg_tls_cert=$(echo "$2" | $json tlsCert)
arg_tls_key=$(echo "$2" | $json tlsKey) arg_tls_key=$(echo "$2" | $json tlsKey)
arg_restore_url=$(echo "$2" | $json restoreUrl) arg_restore_url=$(echo "$2" | $json restore.url)
[[ "${arg_restore_url}" == "null" ]] && arg_restore_url="" [[ "${arg_restore_url}" == "null" ]] && arg_restore_url=""
arg_restore_key=$(echo "$2" | $json restoreKey) arg_restore_key=$(echo "$2" | $json restore.key)
[[ "${arg_restore_key}" == "null" ]] && arg_restore_key="" [[ "${arg_restore_key}" == "null" ]] && arg_restore_key=""
arg_backup_key=$(echo "$2" | $json backupKey) arg_backup_config=$(echo "$2" | $json backupConfig)
[[ "${arg_backup_key}" == "null" ]] && arg_backup_key="" [[ "${arg_backup_config}" == "null" ]] && arg_backup_config=""
arg_aws=$(echo "$2" | $json aws) arg_dns_config=$(echo "$2" | $json dnsConfig)
[[ "${arg_aws}" == "null" ]] && arg_aws="" [[ "${arg_dns_config}" == "null" ]] && arg_dns_config=""
shift 2 shift 2
;; ;;
+2 -2
View File
@@ -29,10 +29,10 @@ infra_version="none"
if [[ "${arg_retire}" == "true" || "${infra_version}" != "${INFRA_VERSION}" ]]; then if [[ "${arg_retire}" == "true" || "${infra_version}" != "${INFRA_VERSION}" ]]; then
rm -f ${DATA_DIR}/nginx/applications/* rm -f ${DATA_DIR}/nginx/applications/*
${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \ ${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 else
${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \ ${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 fi
echo '{ "update": { "percent": "10", "message": "Updating cloudron software" }, "backup": null }' > "${SETUP_WEBSITE_DIR}/progress.json" echo '{ "update": { "percent": "10", "message": "Updating cloudron software" }, "backup": null }' > "${SETUP_WEBSITE_DIR}/progress.json"
+33 -7
View File
@@ -38,6 +38,7 @@ set_progress "10" "Ensuring directories"
# keep these in sync with paths.js # keep these in sync with paths.js
[[ "${is_update}" == "false" ]] && btrfs subvolume create "${DATA_DIR}/box" [[ "${is_update}" == "false" ]] && btrfs subvolume create "${DATA_DIR}/box"
mkdir -p "${DATA_DIR}/box/appicons" mkdir -p "${DATA_DIR}/box/appicons"
mkdir -p "${DATA_DIR}/box/certs"
mkdir -p "${DATA_DIR}/box/mail" mkdir -p "${DATA_DIR}/box/mail"
mkdir -p "${DATA_DIR}/graphite" mkdir -p "${DATA_DIR}/graphite"
@@ -105,15 +106,27 @@ ${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/nginx.ejs
-O "{ \"sourceDir\": \"${BOX_SRC_DIR}\" }" > "${DATA_DIR}/nginx/nginx.conf" -O "{ \"sourceDir\": \"${BOX_SRC_DIR}\" }" > "${DATA_DIR}/nginx/nginx.conf"
# generate these for update code paths as well to overwrite splash # generate these for update code paths as well to overwrite splash
admin_cert_file="cert/host.cert"
admin_key_file="cert/host.key"
if [[ -f "${DATA_DIR}/box/certs/admin.cert" && -f "${DATA_DIR}/box/certs/admin.key" ]]; then
admin_cert_file="${DATA_DIR}/box/certs/admin.cert"
admin_key_file="${DATA_DIR}/box/certs/admin.key"
fi
${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \ ${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\": \"${admin_cert_file}\", \"keyFilePath\": \"${admin_key_file}\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
mkdir -p "${DATA_DIR}/nginx/cert" mkdir -p "${DATA_DIR}/nginx/cert"
echo "${arg_tls_cert}" > ${DATA_DIR}/nginx/cert/host.cert if [[ -f "${DATA_DIR}/box/certs/host.cert" && -f "${DATA_DIR}/box/certs/host.key" ]]; then
echo "${arg_tls_key}" > ${DATA_DIR}/nginx/cert/host.key cp "${DATA_DIR}/box/certs/host.cert" "${DATA_DIR}/nginx/cert/host.cert"
cp "${DATA_DIR}/box/certs/host.key" "${DATA_DIR}/nginx/cert/host.key"
else
echo "${arg_tls_cert}" > "${DATA_DIR}/nginx/cert/host.cert"
echo "${arg_tls_key}" > "${DATA_DIR}/nginx/cert/host.key"
fi
set_progress "33" "Changing ownership" set_progress "33" "Changing ownership"
chown "${USER}:${USER}" -R "${DATA_DIR}/box" "${DATA_DIR}/nginx" "${DATA_DIR}/collectd" "${DATA_DIR}/addons" chown "${USER}:${USER}" -R "${DATA_DIR}/box" "${DATA_DIR}/nginx" "${DATA_DIR}/collectd" "${DATA_DIR}/addons"
chown "${USER}:${USER}" "${DATA_DIR}"
set_progress "40" "Setting up infra" set_progress "40" "Setting up infra"
${script_dir}/start/setup_infra.sh "${arg_fqdn}" ${script_dir}/start/setup_infra.sh "${arg_fqdn}"
@@ -122,7 +135,6 @@ set_progress "65" "Creating cloudron.conf"
sudo -u yellowtent -H bash <<EOF sudo -u yellowtent -H bash <<EOF
set -eu set -eu
echo "Creating cloudron.conf" echo "Creating cloudron.conf"
# note that arg_aws is a javascript object and intentionally unquoted below
cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
{ {
"version": "${arg_version}", "version": "${arg_version}",
@@ -139,9 +151,7 @@ cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
"password": "${mysql_root_password}", "password": "${mysql_root_password}",
"port": 3306, "port": 3306,
"name": "box" "name": "box"
}, }
"backupKey": "${arg_backup_key}",
"aws": ${arg_aws}
} }
CONF_END CONF_END
@@ -153,6 +163,22 @@ cat > "${BOX_SRC_DIR}/webadmin/dist/config.json" <<CONF_END
CONF_END CONF_END
EOF EOF
# Add Backup Configuration
if [[ ! -z "${arg_backup_config}" ]]; then
echo "Add Backup Config"
mysql -u root -p${mysql_root_password} \
-e "REPLACE INTO settings (name, value) VALUES (\"backup_config\", '$arg_backup_config')" box
fi
# 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 # Add webadmin oauth client
# The domain might have changed, therefor we have to update the record # 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 # !!! This needs to be in sync with the webadmin, specifically login_callback.js
+2 -2
View File
@@ -10,8 +10,8 @@ server {
ssl on; ssl on;
# paths are relative to prefix and not to this file # paths are relative to prefix and not to this file
ssl_certificate cert/host.cert; ssl_certificate <%= certFilePath %>;
ssl_certificate_key cert/host.key; ssl_certificate_key <%= keyFilePath %>;
ssl_session_timeout 5m; ssl_session_timeout 5m;
ssl_session_cache shared:SSL:50m; ssl_session_cache shared:SSL:50m;
+69 -52
View File
@@ -9,6 +9,7 @@ exports = module.exports = {
getEnvironment: getEnvironment, getEnvironment: getEnvironment,
getLinksSync: getLinksSync, getLinksSync: getLinksSync,
getBindsSync: getBindsSync, getBindsSync: getBindsSync,
getContainerNamesSync: getContainerNamesSync,
// exported for testing // exported for testing
_setupOauth: setupOauth, _setupOauth: setupOauth,
@@ -23,62 +24,36 @@ var appdb = require('./appdb.js'),
config = require('./config.js'), config = require('./config.js'),
DatabaseError = require('./databaseerror.js'), DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:addons'), debug = require('debug')('box:addons'),
docker = require('./docker.js'), docker = require('./docker.js').connection,
fs = require('fs'), fs = require('fs'),
generatePassword = require('password-generator'), generatePassword = require('password-generator'),
hat = require('hat'), hat = require('hat'),
MemoryStream = require('memorystream'), MemoryStream = require('memorystream'),
once = require('once'), once = require('once'),
os = require('os'),
path = require('path'), path = require('path'),
paths = require('./paths.js'), paths = require('./paths.js'),
safe = require('safetydance'), safe = require('safetydance'),
shell = require('./shell.js'), shell = require('./shell.js'),
spawn = child_process.spawn, spawn = child_process.spawn,
util = require('util'), util = require('util'),
uuid = require('node-uuid'), uuid = require('node-uuid');
vbox = require('./vbox.js');
var NOOP = function (app, options, callback) { return callback(); }; var NOOP = function (app, options, callback) { return callback(); };
// setup can be called multiple times for the same app (configure crash restart) and existing data must not be lost // setup can be called multiple times for the same app (configure crash restart) and existing data must not be lost
// teardown is destructive. app data stored with the addon is lost // teardown is destructive. app data stored with the addon is lost
var KNOWN_ADDONS = { var KNOWN_ADDONS = {
oauth: {
setup: setupOauth,
teardown: teardownOauth,
backup: NOOP,
restore: setupOauth
},
simpleauth: {
setup: setupSimpleAuth,
teardown: teardownSimpleAuth,
backup: NOOP,
restore: setupSimpleAuth
},
ldap: { ldap: {
setup: setupLdap, setup: setupLdap,
teardown: teardownLdap, teardown: teardownLdap,
backup: NOOP, backup: NOOP,
restore: setupLdap restore: setupLdap
}, },
sendmail: { localstorage: {
setup: setupSendMail, setup: NOOP, // docker creates the directory for us
teardown: teardownSendMail, teardown: NOOP,
backup: NOOP, backup: NOOP, // no backup because it's already inside app data
restore: setupSendMail restore: NOOP
},
mysql: {
setup: setupMySql,
teardown: teardownMySql,
backup: backupMySql,
restore: restoreMySql,
},
postgresql: {
setup: setupPostgreSql,
teardown: teardownPostgreSql,
backup: backupPostgreSql,
restore: restorePostgreSql
}, },
mongodb: { mongodb: {
setup: setupMongoDb, setup: setupMongoDb,
@@ -86,18 +61,48 @@ var KNOWN_ADDONS = {
backup: backupMongoDb, backup: backupMongoDb,
restore: restoreMongoDb restore: restoreMongoDb
}, },
mysql: {
setup: setupMySql,
teardown: teardownMySql,
backup: backupMySql,
restore: restoreMySql,
},
oauth: {
setup: setupOauth,
teardown: teardownOauth,
backup: NOOP,
restore: setupOauth
},
postgresql: {
setup: setupPostgreSql,
teardown: teardownPostgreSql,
backup: backupPostgreSql,
restore: restorePostgreSql
},
redis: { redis: {
setup: setupRedis, setup: setupRedis,
teardown: teardownRedis, teardown: teardownRedis,
backup: backupRedis, backup: backupRedis,
restore: setupRedis // same thing restore: setupRedis // same thing
}, },
localstorage: { sendmail: {
setup: NOOP, // docker creates the directory for us setup: setupSendMail,
teardown: teardownSendMail,
backup: NOOP,
restore: setupSendMail
},
scheduler: {
setup: NOOP,
teardown: NOOP, teardown: NOOP,
backup: NOOP, // no backup because it's already inside app data backup: NOOP,
restore: NOOP restore: NOOP
}, },
simpleauth: {
setup: setupSimpleAuth,
teardown: teardownSimpleAuth,
backup: NOOP,
restore: setupSimpleAuth
},
_docker: { _docker: {
setup: NOOP, setup: NOOP,
teardown: NOOP, teardown: NOOP,
@@ -235,6 +240,27 @@ function getBindsSync(app, addons) {
return binds; 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) { function setupOauth(app, options, callback) {
assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof options, 'object');
@@ -365,7 +391,7 @@ function setupSendMail(app, options, callback) {
var env = [ var env = [
'MAIL_SMTP_SERVER=mail', 'MAIL_SMTP_SERVER=mail',
'MAIL_SMTP_PORT=2500', // if you change this, change the mail container 'MAIL_SMTP_PORT=2500', // if you change this, change the mail container
'MAIL_SMTP_USERNAME=' + (app.location || app.id), // use app.id for bare domains 'MAIL_SMTP_USERNAME=' + (app.location || app.id) + '-app', // use app.id for bare domains
'MAIL_DOMAIN=' + config.fqdn() 'MAIL_DOMAIN=' + config.fqdn()
]; ];
@@ -710,8 +736,6 @@ function forwardRedisPort(appId, callback) {
var redisPort = parseInt(safe.query(data, 'NetworkSettings.Ports.6379/tcp[0].HostPort'), 10); var redisPort = parseInt(safe.query(data, 'NetworkSettings.Ports.6379/tcp[0].HostPort'), 10);
if (!Number.isInteger(redisPort)) return callback(new Error('Unable to get container port mapping')); if (!Number.isInteger(redisPort)) return callback(new Error('Unable to get container port mapping'));
vbox.forwardFromHostToVirtualBox('redis-' + appId, redisPort);
return callback(null); return callback(null);
}); });
} }
@@ -752,7 +776,7 @@ function setupRedis(app, options, callback) {
var createOptions = { var createOptions = {
name: 'redis-' + app.id, name: 'redis-' + app.id,
Hostname: config.appFqdn(app.location), Hostname: 'redis-' + app.location,
Tty: true, Tty: true,
Image: 'cloudron/redis:0.7.0', // if you change this, fix setup/INFRA_VERSION as well Image: 'cloudron/redis:0.7.0', // if you change this, fix setup/INFRA_VERSION as well
Cmd: null, Cmd: null,
@@ -760,28 +784,23 @@ function setupRedis(app, options, callback) {
'/tmp': {}, '/tmp': {},
'/run': {} '/run': {}
}, },
VolumesFrom: [] VolumesFrom: [],
}; HostConfig: {
var isMac = os.platform() === 'darwin';
var startOptions = {
Binds: [ Binds: [
redisVarsFile + ':/etc/redis/redis_vars.sh:ro', redisVarsFile + ':/etc/redis/redis_vars.sh:ro',
redisDataDir + ':/var/lib/redis:rw' redisDataDir + ':/var/lib/redis:rw'
], ],
Memory: 1024 * 1024 * 75, // 100mb Memory: 1024 * 1024 * 75, // 100mb
MemorySwap: 1024 * 1024 * 75 * 2, // 150mb MemorySwap: 1024 * 1024 * 75 * 2, // 150mb
// On Mac (boot2docker), we have to export the port to external world for port forwarding from Mac to work
// On linux, export to localhost only for testing purposes and not for the app itself
PortBindings: { PortBindings: {
'6379/tcp': [{ HostPort: '0', HostIp: isMac ? '0.0.0.0' : '127.0.0.1' }] '6379/tcp': [{ HostPort: '0', HostIp: '127.0.0.1' }]
}, },
ReadonlyRootfs: true, ReadonlyRootfs: true,
RestartPolicy: { RestartPolicy: {
'Name': 'always', 'Name': 'always',
'MaximumRetryCount': 0 'MaximumRetryCount': 0
} }
}
}; };
var env = [ var env = [
@@ -796,7 +815,7 @@ function setupRedis(app, options, callback) {
docker.createContainer(createOptions, function (error) { docker.createContainer(createOptions, function (error) {
if (error && error.statusCode !== 409) return callback(error); // if not already created if (error && error.statusCode !== 409) return callback(error); // if not already created
redisContainer.start(startOptions, function (error) { redisContainer.start(function (error) {
if (error && error.statusCode !== 304) return callback(error); // if not already running if (error && error.statusCode !== 304) return callback(error); // if not already running
appdb.setAddonConfig(app.id, 'redis', env, function (error) { appdb.setAddonConfig(app.id, 'redis', env, function (error) {
@@ -824,8 +843,6 @@ function teardownRedis(app, options, callback) {
container.remove(removeOptions, function (error) { container.remove(removeOptions, function (error) {
if (error && error.statusCode !== 404) return callback(new Error('Error removing container:' + error)); if (error && error.statusCode !== 404) return callback(new Error('Error removing container:' + error));
vbox.unforwardFromHostToVirtualBox('redis-' + app.id);
safe.fs.unlinkSync(paths.ADDON_CONFIG_DIR, 'redis-' + app.id + '_vars.sh'); safe.fs.unlinkSync(paths.ADDON_CONFIG_DIR, 'redis-' + app.id + '_vars.sh');
shell.sudo('teardownRedis', [ RMAPPDIR_CMD, app.id + '/redis' ], function (error, stdout, stderr) { shell.sudo('teardownRedis', [ RMAPPDIR_CMD, app.id + '/redis' ], function (error, stdout, stderr) {
+10 -5
View File
@@ -5,7 +5,7 @@ var appdb = require('./appdb.js'),
async = require('async'), async = require('async'),
DatabaseError = require('./databaseerror.js'), DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:apphealthmonitor'), debug = require('debug')('box:apphealthmonitor'),
docker = require('./docker.js'), docker = require('./docker.js').connection,
mailer = require('./mailer.js'), mailer = require('./mailer.js'),
superagent = require('superagent'), superagent = require('superagent'),
util = require('util'); util = require('util');
@@ -97,7 +97,6 @@ function checkAppHealth(app, callback) {
debugApp(app, 'not alive : %s', error || res.status); debugApp(app, 'not alive : %s', error || res.status);
setHealth(app, appdb.HEALTH_UNHEALTHY, callback); setHealth(app, appdb.HEALTH_UNHEALTHY, callback);
} else { } else {
debugApp(app, 'alive');
setHealth(app, appdb.HEALTH_HEALTHY, callback); setHealth(app, appdb.HEALTH_HEALTHY, callback);
} }
}); });
@@ -110,6 +109,13 @@ function processApps(callback) {
async.each(apps, checkAppHealth, function (error) { async.each(apps, checkAppHealth, function (error) {
if (error) console.error(error); if (error) console.error(error);
var alive =apps
.filter(function (a) { return a.installationState === appdb.ISTATE_INSTALLED && a.runState === appdb.RSTATE_RUNNING && a.health === appdb.HEALTH_HEALTHY; })
.map(function (a) { return a.location; }).join(', ');
debug('apps alive: [%s]', alive);
callback(null); callback(null);
}); });
}); });
@@ -149,7 +155,7 @@ function processDockerEvents() {
debug('OOM Context: %s', context); debug('OOM Context: %s', context);
// do not send mails for dev apps // do not send mails for dev apps
if (app.appStoreId !== '') mailer.sendCrashNotification(program, context); // app can be null if it's an addon crash if (error || app.appStoreId !== '') mailer.sendCrashNotification(program, context); // app can be null if it's an addon crash
}); });
}); });
@@ -159,9 +165,8 @@ function processDockerEvents() {
}); });
stream.on('end', function () { stream.on('end', function () {
console.error('Docke event stream ended'); console.error('Docker event stream ended');
gDockerEventStream = null; // TODO: reconnect? gDockerEventStream = null; // TODO: reconnect?
stream.end();
}); });
}); });
} }
+84 -54
View File
@@ -23,7 +23,6 @@ exports = module.exports = {
backup: backup, backup: backup,
backupApp: backupApp, backupApp: backupApp,
getLogStream: getLogStream,
getLogs: getLogs, getLogs: getLogs,
start: start, start: start,
@@ -56,11 +55,14 @@ var addons = require('./addons.js'),
docker = require('./docker.js'), docker = require('./docker.js'),
fs = require('fs'), fs = require('fs'),
manifestFormat = require('cloudron-manifestformat'), manifestFormat = require('cloudron-manifestformat'),
once = require('once'),
path = require('path'), path = require('path'),
paths = require('./paths.js'), paths = require('./paths.js'),
safe = require('safetydance'), safe = require('safetydance'),
settings = require('./settings.js'),
semver = require('semver'), semver = require('semver'),
shell = require('./shell.js'), shell = require('./shell.js'),
spawn = require('child_process').spawn,
split = require('split'), split = require('split'),
superagent = require('superagent'), superagent = require('superagent'),
taskmanager = require('./taskmanager.js'), taskmanager = require('./taskmanager.js'),
@@ -71,6 +73,8 @@ var BACKUP_APP_CMD = path.join(__dirname, 'scripts/backupapp.sh'),
RESTORE_APP_CMD = path.join(__dirname, 'scripts/restoreapp.sh'), RESTORE_APP_CMD = path.join(__dirname, 'scripts/restoreapp.sh'),
BACKUP_SWAP_CMD = path.join(__dirname, 'scripts/backupswap.sh'); BACKUP_SWAP_CMD = path.join(__dirname, 'scripts/backupswap.sh');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function debugApp(app, args) { function debugApp(app, args) {
assert(!app || typeof app === 'object'); assert(!app || typeof app === 'object');
@@ -119,6 +123,7 @@ AppsError.PORT_CONFLICT = 'Port Conflict';
AppsError.BILLING_REQUIRED = 'Billing Required'; AppsError.BILLING_REQUIRED = 'Billing Required';
AppsError.ACCESS_DENIED = 'Access denied'; AppsError.ACCESS_DENIED = 'Access denied';
AppsError.USER_REQUIRED = 'User required'; AppsError.USER_REQUIRED = 'User required';
AppsError.BAD_CERTIFICATE = 'Invalid certificate';
// Hostname validation comes from RFC 1123 (section 2.1) // Hostname validation comes from RFC 1123 (section 2.1)
// Domain name validation comes from RFC 2181 (Name syntax) // Domain name validation comes from RFC 2181 (Name syntax)
@@ -294,7 +299,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 appId, 'string');
assert.strictEqual(typeof appStoreId, 'string'); assert.strictEqual(typeof appStoreId, 'string');
assert(manifest && typeof manifest === 'object'); assert(manifest && typeof manifest === 'object');
@@ -303,6 +308,8 @@ function install(appId, appStoreId, manifest, location, portBindings, accessRest
assert.strictEqual(typeof accessRestriction, 'object'); assert.strictEqual(typeof accessRestriction, 'object');
assert.strictEqual(typeof oauthProxy, 'boolean'); assert.strictEqual(typeof oauthProxy, 'boolean');
assert(!icon || typeof icon === 'string'); assert(!icon || typeof icon === 'string');
assert(cert === null || typeof cert === 'string');
assert(key === null || typeof key === 'string');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
var error = manifestFormat.parse(manifest); var error = manifestFormat.parse(manifest);
@@ -332,6 +339,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); debug('Will install app with id : ' + appId);
purchase(appStoreId, function (error) { purchase(appStoreId, function (error) {
@@ -341,6 +351,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 && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location.toLowerCase(), portBindings, error));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 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); taskmanager.restartAppTask(appId);
callback(null); callback(null);
@@ -348,12 +364,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 appId, 'string');
assert.strictEqual(typeof location, 'string'); assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof portBindings, 'object'); assert.strictEqual(typeof portBindings, 'object');
assert.strictEqual(typeof accessRestriction, 'object'); assert.strictEqual(typeof accessRestriction, 'object');
assert.strictEqual(typeof oauthProxy, 'boolean'); assert.strictEqual(typeof oauthProxy, 'boolean');
assert(cert === null || typeof cert === 'string');
assert(key === null || typeof key === 'string');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
var error = validateHostname(location, config.fqdn()); var error = validateHostname(location, config.fqdn());
@@ -362,6 +380,9 @@ function configure(appId, location, portBindings, accessRestriction, oauthProxy,
error = validateAccessRestriction(accessRestriction); error = validateAccessRestriction(accessRestriction);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message)); 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) { 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 && 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)); if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
@@ -369,6 +390,12 @@ function configure(appId, location, portBindings, accessRestriction, oauthProxy,
error = validatePortBindings(portBindings, app.manifest.tcpPorts); error = validatePortBindings(portBindings, app.manifest.tcpPorts);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message)); 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 = { var values = {
location: location.toLowerCase(), location: location.toLowerCase(),
accessRestriction: accessRestriction, accessRestriction: accessRestriction,
@@ -451,58 +478,50 @@ function update(appId, force, manifest, portBindings, icon, callback) {
}); });
} }
function getLogStream(appId, fromLine, callback) { function appLogFilter(app) {
assert.strictEqual(typeof appId, 'string'); var names = [ app.id ].concat(addons.getContainerNamesSync(app, app.manifest.addons));
assert.strictEqual(typeof fromLine, 'number'); // behaves like tail -n
assert.strictEqual(typeof callback, 'function');
debug('Getting logs for %s', appId); return names.map(function (name) { return 'CONTAINER_NAME=' + name; });
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);
});
});
} }
function getLogs(appId, callback) { function getLogs(appId, lines, follow, callback) {
assert.strictEqual(typeof appId, 'string'); assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof lines, 'number');
assert.strictEqual(typeof follow, 'boolean');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
debug('Getting logs for %s', appId); debug('Getting logs for %s', appId);
appdb.get(appId, function (error, app) { appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND)); 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 (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))); 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 args = [ '--output=json', '--no-pager', '--lines=' + lines ];
// note: cannot access docker file directly because it needs root access if (follow) args.push('--follow');
container.logs({ stdout: true, stderr: true, follow: false, timestamps: true, tail: 'all' }, function (error, logStream) { args = args.concat(appLogFilter(app));
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));
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'
}) + '\n';
}); });
transformStream.close = cp.kill.bind(cp, 'SIGKILL'); // closing stream kills the child process
cp.stdout.pipe(transformStream);
return callback(null, transformStream);
}); });
} }
@@ -627,31 +646,42 @@ function exec(appId, options, callback) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such 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)); if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
var container = docker.getContainer(app.containerId); var createOptions = {
var execOptions = {
AttachStdin: true, AttachStdin: true,
AttachStdout: true, AttachStdout: true,
AttachStderr: true, AttachStderr: true,
Tty: true, OpenStdin: true,
Cmd: cmd StdinOnce: false,
Tty: true
}; };
container.exec(execOptions, function (error, exec) { docker.createSubcontainer(app, app.id + '-exec-' + Date.now(), cmd, createOptions, function (error, container) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
var startOptions = {
Detach: false, container.attach({ stream: true, stdin: true, stdout: true, stderr: true }, function (error, stream) {
Tty: true, if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
stdin: true // this is a dockerode option that enabled openStdin in the modem
}; docker.startContainer(container.id, function (error) {
exec.start(startOptions, function(error, stream) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (options.rows && options.columns) { if (options.rows && options.columns) {
exec.resize({ h: options.rows, w: options.columns }, function (error) { if (error) debug('Error resizing console', error); }); container.resize({ h: options.rows, w: options.columns }, NOOP_CALLBACK);
} }
return callback(null, stream); var deleteContainer = once(docker.deleteContainer.bind(null, container.id, NOOP_CALLBACK));
container.wait(function (error) {
if (error) debug('Error waiting on container', error);
debug('exec: container finished', container.id);
deleteContainer();
});
stream.close = deleteContainer;
callback(null, stream);
});
}); });
}); });
}); });
+42 -263
View File
@@ -47,19 +47,16 @@ var addons = require('./addons.js'),
hat = require('hat'), hat = require('hat'),
manifestFormat = require('cloudron-manifestformat'), manifestFormat = require('cloudron-manifestformat'),
net = require('net'), net = require('net'),
os = require('os'),
path = require('path'), path = require('path'),
paths = require('./paths.js'), paths = require('./paths.js'),
safe = require('safetydance'), safe = require('safetydance'),
semver = require('semver'),
shell = require('./shell.js'), shell = require('./shell.js'),
SubdomainError = require('./subdomainerror.js'), SubdomainError = require('./subdomains.js').SubdomainError,
subdomains = require('./subdomains.js'), subdomains = require('./subdomains.js'),
superagent = require('superagent'), superagent = require('superagent'),
sysinfo = require('./sysinfo.js'), sysinfo = require('./sysinfo.js'),
util = require('util'), util = require('util'),
uuid = require('node-uuid'), uuid = require('node-uuid'),
vbox = require('./vbox.js'),
_ = require('underscore'); _ = require('underscore');
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/../setup/start/nginx/appconfig.ejs', { encoding: 'utf8' }), var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/../setup/start/nginx/appconfig.ejs', { encoding: 'utf8' }),
@@ -80,14 +77,6 @@ function debugApp(app, args) {
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1))); 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) // 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 // https://tools.ietf.org/html/rfc6056#section-3.5 says linux uses random ephemeral port allocation
function getFreePort(callback) { function getFreePort(callback) {
@@ -110,7 +99,20 @@ function configureNginx(app, callback) {
var sourceDir = path.resolve(__dirname, '..'); var sourceDir = path.resolve(__dirname, '..');
var endpoint = app.oauthProxy ? 'oauthproxy' : 'app'; var endpoint = app.oauthProxy ? 'oauthproxy' : 'app';
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, { sourceDir: sourceDir, adminOrigin: config.adminOrigin(), vhost: config.appFqdn(app.location), port: freePort, endpoint: endpoint }); var 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'); var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
debugApp(app, 'writing config to %s', nginxConfigFilename); debugApp(app, 'writing config to %s', nginxConfigFilename);
@@ -124,8 +126,6 @@ function configureNginx(app, callback) {
exports._reloadNginx, exports._reloadNginx,
updateApp.bind(null, app, { httpPort: freePort }) updateApp.bind(null, app, { httpPort: freePort })
], callback); ], callback);
vbox.forwardFromHostToVirtualBox(app.id + '-http', freePort);
}); });
} }
@@ -137,163 +137,27 @@ function unconfigureNginx(app, callback) {
} }
exports._reloadNginx(callback); exports._reloadNginx(callback);
vbox.unforwardFromHostToVirtualBox(app.id + '-http');
}
function pullImage(app, callback) {
docker.pull(app.manifest.dockerImage, function (err, stream) {
if (err) return callback(new Error('Error connecting to docker. statusCode: %s' + err.statusCode));
// https://github.com/dotcloud/docker/issues/1074 says each status message
// is emitted as a chunk
stream.on('data', function (chunk) {
var data = safe.JSON.parse(chunk) || { };
debugApp(app, 'pullImage data: %j', data);
// The information here is useless because this is per layer as opposed to per image
if (data.status) {
// debugApp(app, 'progress: %s', data.status); // progressDetail { current, total }
} else if (data.error) {
debugApp(app, 'pullImage error detail: %s', data.errorDetail.message);
}
});
stream.on('end', function () {
debugApp(app, 'download image successfully');
var image = docker.getImage(app.manifest.dockerImage);
image.inspect(function (err, data) {
if (err) return callback(new Error('Error inspecting image:' + err.message));
if (!data || !data.Config) return callback(new Error('Missing Config in image:' + JSON.stringify(data, null, 4)));
if (!data.Config.Entrypoint && !data.Config.Cmd) return callback(new Error('Only images with entry point are allowed'));
debugApp(app, 'This image exposes ports: %j', data.Config.ExposedPorts);
callback(null);
});
});
stream.on('error', function (error) {
debugApp(app, 'pullImage error : %j', error);
callback(error);
});
});
}
function downloadImage(app, callback) {
debugApp(app, 'downloadImage %s', app.manifest.dockerImage);
var attempt = 1;
async.retry({ times: 5, interval: 15000 }, function (retryCallback) {
debugApp(app, 'Downloading image. attempt: %s', attempt++);
pullImage(app, function (error) {
if (error) console.error(error);
retryCallback(error);
});
}, callback);
} }
function createContainer(app, callback) { function createContainer(app, callback) {
appdb.getPortBindings(app.id, function (error, portBindings) { assert(!app.containerId); // otherwise, it will trigger volumeFrom
if (error) return callback(error);
var manifest = app.manifest; debugApp(app, 'creating container');
var exposedPorts = {};
var env = [];
// docker portBindings requires ports to be exposed docker.createContainer(app, function (error, container) {
exposedPorts[manifest.httpPort + '/tcp'] = {};
for (var e in portBindings) {
var hostPort = portBindings[e];
var containerPort = manifest.tcpPorts[e].containerPort || hostPort;
exposedPorts[containerPort + '/tcp'] = {};
env.push(e + '=' + hostPort);
}
env.push('CLOUDRON=1');
env.push('WEBADMIN_ORIGIN' + '=' + config.adminOrigin());
env.push('API_ORIGIN' + '=' + config.adminOrigin());
addons.getEnvironment(app, function (error, addonEnv) {
if (error) return callback(new Error('Error getting addon env: ' + error));
var containerOptions = {
name: app.id,
Hostname: config.appFqdn(app.location),
Tty: true,
Image: app.manifest.dockerImage,
Cmd: null,
Env: env.concat(addonEnv),
ExposedPorts: exposedPorts,
Volumes: { // see also ReadonlyRootfs
'/tmp': {},
'/run': {}
}
};
// older versions wanted a writable /var/log
if (semver.lte(targetBoxVersion(app.manifest), '0.0.71')) containerOptions.Volumes['/var/log'] = {};
debugApp(app, 'Creating container for %s', app.manifest.dockerImage);
docker.createContainer(containerOptions, function (error, container) {
if (error) return callback(new Error('Error creating container: ' + error)); if (error) return callback(new Error('Error creating container: ' + error));
updateApp(app, { containerId: container.id }, callback); updateApp(app, { containerId: container.id }, callback);
}); });
});
});
} }
function deleteContainer(app, callback) { function deleteContainers(app, callback) {
if (app.containerId === null) return callback(null); debugApp(app, 'deleting containers');
var container = docker.getContainer(app.containerId); docker.deleteContainers(app.id, function (error) {
if (error) return callback(new Error('Error deleting container: ' + error));
var removeOptions = { updateApp(app, { containerId: null }, callback);
force: true, // kill container if it's running
v: true // removes volumes associated with the container (but not host mounts)
};
container.remove(removeOptions, function (error) {
if (error && error.statusCode === 404) return updateApp(app, { containerId: null }, callback);
if (error) debugApp(app, 'Error removing container', error);
callback(error);
});
}
function deleteImage(app, manifest, callback) {
var dockerImage = manifest ? manifest.dockerImage : null;
if (!dockerImage) return callback(null);
docker.getImage(dockerImage).inspect(function (error, result) {
if (error && error.statusCode === 404) return callback(null);
if (error) return callback(error);
var removeOptions = {
force: true,
noprune: false
};
// delete image by id because 'docker pull' pulls down all the tags and this is the only way to delete all tags
docker.getImage(result.Id).remove(removeOptions, function (error) {
if (error && error.statusCode === 404) return callback(null);
if (error && error.statusCode === 409) return callback(null); // another container using the image
if (error) debugApp(app, 'Error removing image', error);
callback(error);
});
}); });
} }
@@ -348,87 +212,6 @@ function removeCollectdProfile(app, callback) {
}); });
} }
function startContainer(app, callback) {
appdb.getPortBindings(app.id, function (error, portBindings) {
if (error) return callback(error);
var manifest = app.manifest;
var dockerPortBindings = { };
var isMac = os.platform() === 'darwin';
// On Mac (boot2docker), we have to export the port to external world for port forwarding from Mac to work
dockerPortBindings[manifest.httpPort + '/tcp'] = [ { HostIp: isMac ? '0.0.0.0' : '127.0.0.1', HostPort: app.httpPort + '' } ];
for (var env in portBindings) {
var hostPort = portBindings[env];
var containerPort = manifest.tcpPorts[env].containerPort || hostPort;
dockerPortBindings[containerPort + '/tcp'] = [ { HostIp: '0.0.0.0', HostPort: hostPort + '' } ];
vbox.forwardFromHostToVirtualBox(app.id + '-tcp' + containerPort, hostPort);
}
var memoryLimit = manifest.memoryLimit || 1024 * 1024 * 200; // 200mb by default
var startOptions = {
Binds: addons.getBindsSync(app, app.manifest.addons),
Memory: memoryLimit / 2,
MemorySwap: memoryLimit, // Memory + Swap
PortBindings: dockerPortBindings,
PublishAllPorts: false,
ReadonlyRootfs: semver.gte(targetBoxVersion(app.manifest), '0.0.66'), // see also Volumes in startContainer
Links: addons.getLinksSync(app, app.manifest.addons),
RestartPolicy: {
"Name": "always",
"MaximumRetryCount": 0
},
CpuShares: 512, // relative to 1024 for system processes
SecurityOpt: config.CLOUDRON ? [ "apparmor:docker-cloudron-app" ] : null // profile available only on cloudron
};
var container = docker.getContainer(app.containerId);
debugApp(app, 'Starting container %s with options: %j', container.id, JSON.stringify(startOptions));
container.start(startOptions, function (error, data) {
if (error && error.statusCode !== 304) return callback(new Error('Error starting container:' + error));
return callback(null);
});
});
}
function stopContainer(app, callback) {
if (!app.containerId) {
debugApp(app, 'No previous container to stop');
return callback();
}
var container = docker.getContainer(app.containerId);
debugApp(app, 'Stopping container %s', container.id);
var options = {
t: 10 // wait for 10 seconds before killing it
};
container.stop(options, function (error) {
if (error && (error.statusCode !== 304 && error.statusCode !== 404)) return callback(new Error('Error stopping container:' + error));
var tcpPorts = safe.query(app, 'manifest.tcpPorts', { });
for (var containerPort in tcpPorts) {
vbox.unforwardFromHostToVirtualBox(app.id + '-tcp' + containerPort);
}
debugApp(app, 'Waiting for container ' + container.id);
container.wait(function (error, data) {
if (error && (error.statusCode !== 304 && error.statusCode !== 404)) return callback(new Error('Error waiting on container:' + error));
debugApp(app, 'Container stopped with status code [%s]', data ? String(data.StatusCode) : '');
return callback(null);
});
});
}
function verifyManifest(app, callback) { function verifyManifest(app, callback) {
debugApp(app, 'Verifying manifest'); debugApp(app, 'Verifying manifest');
@@ -463,12 +246,10 @@ function downloadIcon(app, callback) {
function registerSubdomain(app, callback) { function registerSubdomain(app, callback) {
// even though the bare domain is already registered in the appstore, we still // 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 // 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) { async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Registering subdomain location [%s]', app.location); 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 if (error && (error.reason === SubdomainError.STILL_BUSY || error.reason === SubdomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
retryCallback(null, error || changeId); retryCallback(null, error || changeId);
@@ -487,18 +268,16 @@ function unregisterSubdomain(app, location, callback) {
return callback(null); return callback(null);
} }
var record = { subdomain: location, type: 'A', value: sysinfo.getIp() };
async.retry({ times: 30, interval: 5000 }, function (retryCallback) { async.retry({ times: 30, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Unregistering subdomain: %s', location); debugApp(app, 'Unregistering subdomain: %s', location);
subdomains.remove(record, function (error) { 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 if (error && (error.reason === SubdomainError.STILL_BUSY || error.reason === SubdomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
retryCallback(error); retryCallback(null, error);
}); });
}, function (error) { }, function (error, result) {
if (error) debugApp(app, 'Error unregistering subdomain: %s', error); if (error || result instanceof Error) return callback(error || result);
updateApp(app, { dnsRecordId: null }, callback); updateApp(app, { dnsRecordId: null }, callback);
}); });
@@ -566,7 +345,7 @@ function install(app, callback) {
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }), updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
removeCollectdProfile.bind(null, app), removeCollectdProfile.bind(null, app),
stopApp.bind(null, app), stopApp.bind(null, app),
deleteContainer.bind(null, app), deleteContainers.bind(null, app),
addons.teardownAddons.bind(null, app, app.manifest.addons), addons.teardownAddons.bind(null, app, app.manifest.addons),
deleteVolume.bind(null, app), deleteVolume.bind(null, app),
unregisterSubdomain.bind(null, app, app.location), unregisterSubdomain.bind(null, app, app.location),
@@ -587,7 +366,7 @@ function install(app, callback) {
registerSubdomain.bind(null, app), registerSubdomain.bind(null, app),
updateApp.bind(null, app, { installationProgress: '40, Downloading image' }), updateApp.bind(null, app, { installationProgress: '40, Downloading image' }),
downloadImage.bind(null, app), docker.downloadImage.bind(null, app.manifest),
updateApp.bind(null, app, { installationProgress: '50, Creating volume' }), updateApp.bind(null, app, { installationProgress: '50, Creating volume' }),
createVolume.bind(null, app), createVolume.bind(null, app),
@@ -652,14 +431,14 @@ function restore(app, callback) {
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }), updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
removeCollectdProfile.bind(null, app), removeCollectdProfile.bind(null, app),
stopApp.bind(null, app), stopApp.bind(null, app),
deleteContainer.bind(null, app), deleteContainers.bind(null, app),
// oldConfig can be null during upgrades // oldConfig can be null during upgrades
addons.teardownAddons.bind(null, app, app.oldConfig ? app.oldConfig.manifest.addons : null), addons.teardownAddons.bind(null, app, app.oldConfig ? app.oldConfig.manifest.addons : null),
deleteVolume.bind(null, app), deleteVolume.bind(null, app),
function deleteImageIfChanged(done) { function deleteImageIfChanged(done) {
if (!app.oldConfig || (app.oldConfig.manifest.dockerImage === app.manifest.dockerImage)) return done(); if (!app.oldConfig || (app.oldConfig.manifest.dockerImage === app.manifest.dockerImage)) return done();
deleteImage(app, app.oldConfig.manifest, done); docker.deleteImage(app.oldConfig.manifest, done);
}, },
removeOAuthProxyCredentials.bind(null, app), removeOAuthProxyCredentials.bind(null, app),
removeIcon.bind(null, app), removeIcon.bind(null, app),
@@ -678,7 +457,7 @@ function restore(app, callback) {
registerSubdomain.bind(null, app), registerSubdomain.bind(null, app),
updateApp.bind(null, app, { installationProgress: '60, Downloading image' }), updateApp.bind(null, app, { installationProgress: '60, Downloading image' }),
downloadImage.bind(null, app), docker.downloadImage.bind(null, app.manifest),
updateApp.bind(null, app, { installationProgress: '65, Creating volume' }), updateApp.bind(null, app, { installationProgress: '65, Creating volume' }),
createVolume.bind(null, app), createVolume.bind(null, app),
@@ -718,7 +497,7 @@ function configure(app, callback) {
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }), updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
removeCollectdProfile.bind(null, app), removeCollectdProfile.bind(null, app),
stopApp.bind(null, app), stopApp.bind(null, app),
deleteContainer.bind(null, app), deleteContainers.bind(null, app),
function (next) { function (next) {
// oldConfig can be null during an infra update // oldConfig can be null during an infra update
if (!app.oldConfig || app.oldConfig.location === app.location) return next(); if (!app.oldConfig || app.oldConfig.location === app.location) return next();
@@ -781,12 +560,12 @@ function update(app, callback) {
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }), updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
removeCollectdProfile.bind(null, app), removeCollectdProfile.bind(null, app),
stopApp.bind(null, app), stopApp.bind(null, app),
deleteContainer.bind(null, app), deleteContainers.bind(null, app),
addons.teardownAddons.bind(null, app, unusedAddons), addons.teardownAddons.bind(null, app, unusedAddons),
function deleteImageIfChanged(done) { function deleteImageIfChanged(done) {
if (app.oldConfig.manifest.dockerImage === app.manifest.dockerImage) return done(); if (app.oldConfig.manifest.dockerImage === app.manifest.dockerImage) return done();
deleteImage(app, app.oldConfig.manifest, done); docker.deleteImage(app.oldConfig.manifest, done);
}, },
// removeIcon.bind(null, app), // do not remove icon, otherwise the UI breaks for a short time... // removeIcon.bind(null, app), // do not remove icon, otherwise the UI breaks for a short time...
@@ -803,7 +582,7 @@ function update(app, callback) {
downloadIcon.bind(null, app), downloadIcon.bind(null, app),
updateApp.bind(null, app, { installationProgress: '45, Downloading image' }), updateApp.bind(null, app, { installationProgress: '45, Downloading image' }),
downloadImage.bind(null, app), docker.downloadImage.bind(null, app.manifest),
updateApp.bind(null, app, { installationProgress: '70, Updating addons' }), updateApp.bind(null, app, { installationProgress: '70, Updating addons' }),
addons.setupAddons.bind(null, app, app.manifest.addons), addons.setupAddons.bind(null, app, app.manifest.addons),
@@ -841,7 +620,7 @@ function uninstall(app, callback) {
stopApp.bind(null, app), stopApp.bind(null, app),
updateApp.bind(null, app, { installationProgress: '20, Deleting container' }), updateApp.bind(null, app, { installationProgress: '20, Deleting container' }),
deleteContainer.bind(null, app), deleteContainers.bind(null, app),
updateApp.bind(null, app, { installationProgress: '30, Teardown addons' }), updateApp.bind(null, app, { installationProgress: '30, Teardown addons' }),
addons.teardownAddons.bind(null, app, app.manifest.addons), addons.teardownAddons.bind(null, app, app.manifest.addons),
@@ -850,7 +629,7 @@ function uninstall(app, callback) {
deleteVolume.bind(null, app), deleteVolume.bind(null, app),
updateApp.bind(null, app, { installationProgress: '50, Deleting image' }), updateApp.bind(null, app, { installationProgress: '50, Deleting image' }),
deleteImage.bind(null, app, app.manifest), docker.deleteImage.bind(null, app.manifest),
updateApp.bind(null, app, { installationProgress: '60, Unregistering subdomain' }), updateApp.bind(null, app, { installationProgress: '60, Unregistering subdomain' }),
unregisterSubdomain.bind(null, app, app.location), unregisterSubdomain.bind(null, app, app.location),
@@ -870,7 +649,7 @@ function uninstall(app, callback) {
} }
function runApp(app, callback) { function runApp(app, callback) {
startContainer(app, function (error) { docker.startContainer(app.containerId, function (error) {
if (error) return callback(error); if (error) return callback(error);
updateApp(app, { runState: appdb.RSTATE_RUNNING }, callback); updateApp(app, { runState: appdb.RSTATE_RUNNING }, callback);
@@ -878,7 +657,7 @@ function runApp(app, callback) {
} }
function stopApp(app, callback) { function stopApp(app, callback) {
stopContainer(app, function (error) { docker.stopContainers(app.id, function (error) {
if (error) return callback(error); if (error) return callback(error);
updateApp(app, { runState: appdb.RSTATE_STOPPED }, callback); updateApp(app, { runState: appdb.RSTATE_STOPPED }, callback);
@@ -935,7 +714,7 @@ if (require.main === module) {
if (error) throw error; if (error) throw error;
startTask(process.argv[2], function (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]); 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 // https://nodejs.org/api/process.html are exit codes used by node. apps.js uses the value below
-282
View File
@@ -1,282 +0,0 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
getSignedUploadUrl: getSignedUploadUrl,
getSignedDownloadUrl: getSignedDownloadUrl,
addSubdomain: addSubdomain,
delSubdomain: delSubdomain,
getChangeStatus: getChangeStatus,
copyObject: copyObject
};
var assert = require('assert'),
AWS = require('aws-sdk'),
config = require('./config.js'),
debug = require('debug')('box:aws'),
SubdomainError = require('./subdomainerror.js'),
superagent = require('superagent');
function getAWSCredentials(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));
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');
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 });
});
}
function getZoneByName(zoneName, callback) {
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof callback, 'function');
debug('getZoneByName: %s', zoneName);
getAWSCredentials(function (error, credentials) {
if (error) return callback(error);
var route53 = new AWS.Route53(credentials);
route53.listHostedZones({}, function (error, result) {
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
var zone = result.HostedZones.filter(function (zone) {
return zone.Name.slice(0, -1) === zoneName; // aws zone name contains a '.' at the end
})[0];
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) {
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('addSubdomain: ' + subdomain + ' for domain ' + zoneName + ' with value ' + value);
getZoneByName(zoneName, function (error, zone) {
if (error) return callback(error);
var fqdn = config.appFqdn(subdomain);
var params = {
ChangeBatch: {
Changes: [{
Action: 'UPSERT',
ResourceRecordSet: {
Type: type,
Name: fqdn,
ResourceRecords: [{
Value: value
}],
Weight: 0,
SetIdentifier: fqdn,
TTL: 1
}
}]
},
HostedZoneId: zone.Id
};
getAWSCredentials(function (error, credentials) {
if (error) return callback(error);
var route53 = new AWS.Route53(credentials);
route53.changeResourceRecordSets(params, function(error, result) {
if (error && error.code === 'PriorRequestNotComplete') {
return callback(new SubdomainError(SubdomainError.STILL_BUSY, error.message));
} else if (error) {
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) {
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);
getZoneByName(zoneName, function (error, zone) {
if (error) return callback(error);
var fqdn = config.appFqdn(subdomain);
var resourceRecordSet = {
Name: fqdn,
Type: type,
ResourceRecords: [{
Value: value
}],
Weight: 0,
SetIdentifier: fqdn,
TTL: 1
};
var params = {
ChangeBatch: {
Changes: [{
Action: 'DELETE',
ResourceRecordSet: resourceRecordSet
}]
},
HostedZoneId: zone.Id
};
getAWSCredentials(function (error, credentials) {
if (error) return callback(error);
var route53 = new AWS.Route53(credentials);
route53.changeResourceRecordSets(params, function(error, result) {
if (error && error.message && error.message.indexOf('it was not found') !== -1) {
debug('delSubdomain: resource record set not found.', error);
return callback(new SubdomainError(SubdomainError.NOT_FOUND, new Error(error)));
} else if (error && error.code === 'NoSuchHostedZone') {
debug('delSubdomain: hosted zone not found.', error);
return callback(new SubdomainError(SubdomainError.NOT_FOUND, new Error(error)));
} else if (error && error.code === 'PriorRequestNotComplete') {
debug('delSubdomain: resource is still busy', error);
return callback(new SubdomainError(SubdomainError.STILL_BUSY, new Error(error)));
} else if (error && error.code === 'InvalidChangeBatch') {
debug('delSubdomain: invalid change batch. No such record to be deleted.');
return callback(new SubdomainError(SubdomainError.NOT_FOUND, new Error(error)));
} else if (error) {
debug('delSubdomain: error', error);
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
}
debug('delSubdomain: success');
callback(null);
});
});
});
}
function getChangeStatus(changeId, callback) {
assert.strictEqual(typeof changeId, 'string');
assert.strictEqual(typeof callback, 'function');
if (changeId === '') return callback(null, 'INSYNC');
getAWSCredentials(function (error, credentials) {
if (error) return callback(error);
var route53 = new AWS.Route53(credentials);
route53.getChange({ Id: changeId }, function (error, result) {
if (error) return callback(error);
callback(null, result.ChangeInfo.Status);
});
});
}
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);
});
}
+36 -13
View File
@@ -12,10 +12,11 @@ exports = module.exports = {
}; };
var assert = require('assert'), var assert = require('assert'),
aws = require('./aws.js'), caas = require('./storage/caas.js'),
config = require('./config.js'), config = require('./config.js'),
debug = require('debug')('box:backups'), debug = require('debug')('box:backups'),
superagent = require('superagent'), s3 = require('./storage/s3.js'),
settings = require('./settings.js'),
util = require('util'); util = require('util');
function BackupsError(reason, errorOrMessage) { function BackupsError(reason, errorOrMessage) {
@@ -39,21 +40,30 @@ function BackupsError(reason, errorOrMessage) {
util.inherits(BackupsError, Error); util.inherits(BackupsError, Error);
BackupsError.EXTERNAL_ERROR = 'external error'; BackupsError.EXTERNAL_ERROR = 'external error';
BackupsError.INTERNAL_ERROR = 'internal 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(provider) {
switch (provider) {
case 'caas': return caas;
case 's3': return s3;
default: return null;
}
}
function getAllPaged(page, perPage, callback) { function getAllPaged(page, perPage, callback) {
assert.strictEqual(typeof page, 'number'); assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number'); assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/backups'; settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
superagent.get(url).query({ token: config.token() }).end(function (error, result) { api(backupConfig.provider).getAllPaged(backupConfig, page, perPage, function (error, backups) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error)); if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
if (result.statusCode !== 200) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, result.text));
if (!result.body || !util.isArray(result.body.backups)) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Unexpected response'));
// [ { creationTime, boxVersion, restoreKey, dependsOn: [ ] } ] sorted by time (latest first) return callback(null, backups); // [ { creationTime, boxVersion, restoreKey, dependsOn: [ ] } ] sorted by time (latest first
return callback(null, result.body.backups); });
}); });
} }
@@ -68,20 +78,24 @@ function getBackupUrl(app, callback) {
filename = util.format('backup_%s-v%s.tar.gz', (new Date()).toISOString(), config.version()); filename = util.format('backup_%s-v%s.tar.gz', (new Date()).toISOString(), config.version());
} }
aws.getSignedUploadUrl(filename, function (error, result) { settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
api(backupConfig.provider).getSignedUploadUrl(backupConfig, filename, function (error, result) {
if (error) return callback(error); if (error) return callback(error);
var obj = { var obj = {
id: filename, id: filename,
url: result.url, url: result.url,
sessionToken: result.sessionToken, sessionToken: result.sessionToken,
backupKey: config.backupKey() backupKey: backupConfig.key
}; };
debug('getBackupUrl: id:%s url:%s sessionToken:%s backupKey:%s', obj.id, obj.url, obj.sessionToken, obj.backupKey); debug('getBackupUrl: id:%s url:%s sessionToken:%s backupKey:%s', obj.id, obj.url, obj.sessionToken, obj.backupKey);
callback(null, obj); callback(null, obj);
}); });
});
} }
// backupId is the s3 filename. appbackup_%s_%s-v%s.tar.gz // backupId is the s3 filename. appbackup_%s_%s-v%s.tar.gz
@@ -89,20 +103,24 @@ function getRestoreUrl(backupId, callback) {
assert.strictEqual(typeof backupId, 'string'); assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
aws.getSignedDownloadUrl(backupId, function (error, result) { settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
api(backupConfig.provider).getSignedDownloadUrl(backupConfig, backupId, function (error, result) {
if (error) return callback(error); if (error) return callback(error);
var obj = { var obj = {
id: backupId, id: backupId,
url: result.url, url: result.url,
sessionToken: result.sessionToken, sessionToken: result.sessionToken,
backupKey: config.backupKey() backupKey: backupConfig.key
}; };
debug('getRestoreUrl: id:%s url:%s sessionToken:%s backupKey:%s', obj.id, obj.url, obj.sessionToken, obj.backupKey); debug('getRestoreUrl: id:%s url:%s sessionToken:%s backupKey:%s', obj.id, obj.url, obj.sessionToken, obj.backupKey);
callback(null, obj); callback(null, obj);
}); });
});
} }
function copyLastBackup(app, callback) { function copyLastBackup(app, callback) {
@@ -111,9 +129,14 @@ function copyLastBackup(app, callback) {
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
var toFilename = util.format('appbackup_%s_%s-v%s.tar.gz', app.id, (new Date()).toISOString(), app.manifest.version); 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) {
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
api(backupConfig.provider).copyObject(backupConfig, app.lastBackupId, toFilename, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error)); if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
return callback(null, toFilename); return callback(null, toFilename);
}); });
});
} }
-91
View File
@@ -1,91 +0,0 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
addSubdomain: addSubdomain,
delSubdomain: delSubdomain,
getChangeStatus: getChangeStatus
};
var assert = require('assert'),
config = require('./config.js'),
debug = require('debug')('box:caas'),
SubdomainError = require('./subdomainerror.js'),
superagent = require('superagent'),
util = require('util');
function addSubdomain(zoneName, subdomain, type, value, callback) {
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert.strictEqual(typeof callback, 'function');
var fqdn = subdomain !== '' && type === 'TXT' ? subdomain + '.' + config.fqdn() : config.appFqdn(subdomain);
debug('addSubdomain: zoneName: %s subdomain: %s type: %s value: %s fqdn: %s', zoneName, subdomain, type, value, fqdn);
var data = {
type: type,
value: value
};
superagent
.post(config.apiServerOrigin() + '/api/v1/domains/' + fqdn)
.query({ token: config.token() })
.send(data)
.end(function (error, result) {
if (error) return callback(error);
if (result.status === 420) return callback(new SubdomainError(SubdomainError.STILL_BUSY));
if (result.status !== 201) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
return callback(null, result.body.changeId);
});
}
function delSubdomain(zoneName, subdomain, type, value, callback) {
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert.strictEqual(typeof callback, 'function');
debug('delSubdomain: %s for domain %s.', subdomain, zoneName);
var data = {
type: type,
value: value
};
superagent
.del(config.apiServerOrigin() + '/api/v1/domains/' + config.appFqdn(subdomain))
.query({ token: config.token() })
.send(data)
.end(function (error, result) {
if (error) return callback(error);
if (result.status === 420) return callback(new SubdomainError(SubdomainError.STILL_BUSY));
if (result.status === 404) return callback(new SubdomainError(SubdomainError.NOT_FOUND));
if (result.status !== 204) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
return callback(null);
});
}
function getChangeStatus(changeId, callback) {
assert.strictEqual(typeof changeId, 'string');
assert.strictEqual(typeof callback, 'function');
if (changeId === '') return callback(null, 'INSYNC');
superagent
.get(config.apiServerOrigin() + '/api/v1/domains/' + config.fqdn() + '/status/' + changeId)
.query({ token: config.token() })
.end(function (error, result) {
if (error) return callback(error);
if (result.status !== 200) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
return callback(null, result.body.status);
});
}
+177 -105
View File
@@ -11,15 +11,20 @@ exports = module.exports = {
getConfig: getConfig, getConfig: getConfig,
getStatus: getStatus, getStatus: getStatus,
setCertificate: setCertificate,
sendHeartbeat: sendHeartbeat, sendHeartbeat: sendHeartbeat,
update: update, update: update,
reboot: reboot, reboot: reboot,
migrate: migrate, migrate: migrate,
backup: backup, backup: backup,
ensureBackup: ensureBackup ensureBackup: ensureBackup,
isConfiguredSync: isConfiguredSync,
events: new (require('events').EventEmitter)(),
EVENT_ACTIVATED: 'activated',
EVENT_CONFIGURED: 'configured'
}; };
var apps = require('./apps.js'), var apps = require('./apps.js'),
@@ -28,6 +33,7 @@ var apps = require('./apps.js'),
async = require('async'), async = require('async'),
backups = require('./backups.js'), backups = require('./backups.js'),
BackupsError = require('./backups.js').BackupsError, BackupsError = require('./backups.js').BackupsError,
bytes = require('bytes'),
clientdb = require('./clientdb.js'), clientdb = require('./clientdb.js'),
config = require('./config.js'), config = require('./config.js'),
debug = require('debug')('box:cloudron'), debug = require('debug')('box:cloudron'),
@@ -38,7 +44,6 @@ var apps = require('./apps.js'),
progress = require('./progress.js'), progress = require('./progress.js'),
safe = require('safetydance'), safe = require('safetydance'),
settings = require('./settings.js'), settings = require('./settings.js'),
SettingsError = settings.SettingsError,
shell = require('./shell.js'), shell = require('./shell.js'),
subdomains = require('./subdomains.js'), subdomains = require('./subdomains.js'),
superagent = require('superagent'), superagent = require('superagent'),
@@ -51,14 +56,16 @@ var apps = require('./apps.js'),
util = require('util'), util = require('util'),
webhooks = require('./webhooks.js'); webhooks = require('./webhooks.js');
var RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh'), var REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'),
REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'),
BACKUP_BOX_CMD = path.join(__dirname, 'scripts/backupbox.sh'), BACKUP_BOX_CMD = path.join(__dirname, 'scripts/backupbox.sh'),
BACKUP_SWAP_CMD = path.join(__dirname, 'scripts/backupswap.sh'), BACKUP_SWAP_CMD = path.join(__dirname, 'scripts/backupswap.sh'),
INSTALLER_UPDATE_URL = 'http://127.0.0.1:2020/api/v1/installer/update'; INSTALLER_UPDATE_URL = 'http://127.0.0.1:2020/api/v1/installer/update';
var gAddDnsRecordsTimerId = null, var NOOP_CALLBACK = function (error) { if (error) debug(error); };
gCloudronDetails = null; // cached cloudron details like region,size...
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) { function debugApp(app, args) {
assert(!app || typeof app === 'object'); assert(!app || typeof app === 'object');
@@ -76,7 +83,6 @@ function ignoreError(func) {
}; };
} }
function CloudronError(reason, errorOrMessage) { function CloudronError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string'); assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined'); assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
@@ -110,22 +116,61 @@ CloudronError.NOT_FOUND = 'Not found';
function initialize(callback) { function initialize(callback) {
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
if (process.env.BOX_ENV !== 'test') { exports.events.on(exports.EVENT_CONFIGURED, addDnsRecords);
addDnsRecords();
}
callback(null); syncConfigState(callback);
} }
function uninitialize(callback) { function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
clearTimeout(gAddDnsRecordsTimerId); exports.events.removeListener(exports.EVENT_CONFIGURED, addDnsRecords);
gAddDnsRecordsTimerId = null;
callback(null); 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) { function setTimeZone(ip, callback) {
assert.strictEqual(typeof ip, 'string'); assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
@@ -149,24 +194,17 @@ function setTimeZone(ip, callback) {
}); });
} }
function activate(username, password, email, name, ip, callback) { function activate(username, password, email, ip, callback) {
assert.strictEqual(typeof username, 'string'); assert.strictEqual(typeof username, 'string');
assert.strictEqual(typeof password, 'string'); assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof email, 'string'); assert.strictEqual(typeof email, 'string');
assert.strictEqual(typeof ip, 'string'); assert.strictEqual(typeof ip, 'string');
assert(!name || typeof name, 'string');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
debug('activating user:%s email:%s', username, email); debug('activating user:%s email:%s', username, email);
setTimeZone(ip, function () { }); // TODO: get this from user. note that timezone is detected based on the browser location and not the cloudron region setTimeZone(ip, function () { }); // TODO: get this from user. note that timezone is detected based on the browser location and not the cloudron region
if (!name) name = settings.getDefaultSync(settings.CLOUDRON_NAME_KEY);
settings.setCloudronName(name, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_NAME));
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
user.createOwner(username, password, email, function (error, userObject) { user.createOwner(username, password, email, function (error, userObject) {
if (error && error.reason === UserError.ALREADY_EXISTS) return callback(new CloudronError(CloudronError.ALREADY_PROVISIONED)); if (error && error.reason === UserError.ALREADY_EXISTS) return callback(new CloudronError(CloudronError.ALREADY_PROVISIONED));
if (error && error.reason === UserError.BAD_USERNAME) return callback(new CloudronError(CloudronError.BAD_USERNAME)); if (error && error.reason === UserError.BAD_USERNAME) return callback(new CloudronError(CloudronError.BAD_USERNAME));
@@ -184,11 +222,13 @@ function activate(username, password, email, name, ip, callback) {
tokendb.add(token, tokendb.PREFIX_USER + userObject.id, result.id, expires, '*', function (error) { tokendb.add(token, tokendb.PREFIX_USER + userObject.id, result.id, expires, '*', function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, 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 }); callback(null, { token: token, expires: expires });
}); });
}); });
}); });
});
} }
function getStatus(callback) { function getStatus(callback) {
@@ -203,6 +243,8 @@ function getStatus(callback) {
callback(null, { callback(null, {
activated: count !== 0, activated: count !== 0,
version: config.version(), version: config.version(),
boxVersionsUrl: config.get('boxVersionsUrl'),
apiServerOrigin: config.apiServerOrigin(), // used by CaaS tool
cloudronName: cloudronName cloudronName: cloudronName
}); });
}); });
@@ -230,10 +272,9 @@ function getCloudronDetails(callback) {
function getConfig(callback) { function getConfig(callback) {
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
// TODO avoid pyramid of awesomeness with async
getCloudronDetails(function (error, result) { getCloudronDetails(function (error, result) {
if (error) { if (error) {
console.error('Failed to fetch cloudron details.', error); debug('Failed to fetch cloudron details.', error);
// set fallback values to avoid dependency on appstore // set fallback values to avoid dependency on appstore
result = { result = {
@@ -242,6 +283,10 @@ function getConfig(callback) {
}; };
} }
// We rely at the moment on the size being specified in 512mb,1gb,...
// TODO provide that number from the appstore
var memory = bytes(result.size) || 0;
settings.getCloudronName(function (error, cloudronName) { settings.getCloudronName(function (error, cloudronName) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error)); if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
@@ -261,6 +306,7 @@ function getConfig(callback) {
developerMode: developerMode, developerMode: developerMode,
region: result.region, region: result.region,
size: result.size, size: result.size,
memory: memory,
cloudronName: cloudronName cloudronName: cloudronName
}); });
}); });
@@ -269,9 +315,6 @@ function getConfig(callback) {
} }
function sendHeartbeat() { function sendHeartbeat() {
// Only send heartbeats after the admin dns record is synced to give appstore a chance to know that fact
if (!config.get('dnsInSync')) return;
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/heartbeat'; 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) { superagent.post(url).query({ token: config.token(), version: config.version() }).timeout(10000).end(function (error, result) {
@@ -281,92 +324,109 @@ function sendHeartbeat() {
}); });
} }
function addDnsRecords() { function readDkimPublicKeySync() {
if (config.get('dnsInSync')) return sendHeartbeat(); // already registered send heartbeat
var DKIM_SELECTOR = 'mail';
var DMARC_REPORT_EMAIL = 'dmarc-report@cloudron.io';
var dkimPublicKeyFile = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn() + '/public'); var dkimPublicKeyFile = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn() + '/public');
var publicKey = safe.fs.readFileSync(dkimPublicKeyFile, 'utf8'); var publicKey = safe.fs.readFileSync(dkimPublicKeyFile, 'utf8');
if (publicKey === null) { if (publicKey === null) {
console.error('Error reading dkim public key. Stop DNS setup.'); debug('Error reading dkim public key.', safe.error);
return; return null;
} }
// remove header, footer and new lines // remove header, footer and new lines
publicKey = publicKey.split('\n').slice(1, -2).join(''); publicKey = publicKey.split('\n').slice(1, -2).join('');
// note that dmarc requires special DNS records for external RUF and RUA return publicKey;
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 + '"' }
];
debug('addDnsRecords:', records);
subdomains.addMany(records, function (error, changeIds) {
if (error) {
console.error('Admin DNS record addition failed', error);
gAddDnsRecordsTimerId = setTimeout(addDnsRecords, 10000);
return;
} }
function checkIfInSync() { function txtRecordsWithSpf(callback) {
debug('addDnsRecords: Check if admin DNS record is in sync.');
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) {
console.error(error);
gAddDnsRecordsTimerId = setTimeout(checkIfInSync, 5000);
return;
}
debug('addDnsRecords: done');
config.set('dnsInSync', true);
sendHeartbeat(); // send heartbeat after the dns records are done
});
}
checkIfInSync();
});
}
function setCertificate(certificate, key, callback) {
assert.strictEqual(typeof certificate, 'string');
assert.strictEqual(typeof key, 'string');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
debug('Updating certificates'); subdomains.get('', 'TXT', function (error, txtRecords) {
if (error) return callback(error);
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), certificate)) { debug('txtRecordsWithSpf: current txt records - %j', txtRecords);
return callback(new CloudronError(CloudronError.INTERNAL_ERROR, safe.error.message));
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;
} }
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), key)) { if (validSpf) return callback(null, null);
return callback(new CloudronError(CloudronError.INTERNAL_ERROR, safe.error.message));
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);
} }
shell.sudo('setCertificate', [ RELOAD_NGINX_CMD ], function (error) { return callback(null, txtRecords);
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error)); });
}
return callback(null); function addDnsRecords() {
var callback = NOOP_CALLBACK;
if (process.env.BOX_ENV === 'test') return callback();
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);
}
debug('addDnsRecords: %j', records);
async.retry({ times: 10, interval: 20000 }, function (retryCallback) {
txtRecordsWithSpf(function (error, txtRecords) {
if (error) return retryCallback(error);
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);
}); });
} }
@@ -504,19 +564,31 @@ function doUpdate(boxUpdateInfo, callback) {
// this data is opaque to the installer // this data is opaque to the installer
data: { data: {
apiServerOrigin: config.apiServerOrigin(),
aws: config.aws(),
backupKey: config.backupKey(),
boxVersionsUrl: config.get('boxVersionsUrl'),
fqdn: config.fqdn(),
isCustomDomain: config.isCustomDomain(),
restoreUrl: null,
restoreKey: null,
token: config.token(), token: config.token(),
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin(),
fqdn: config.fqdn(),
tlsCert: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), 'utf8'), tlsCert: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), 'utf8'),
tlsKey: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), 'utf8'), tlsKey: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), 'utf8'),
version: boxUpdateInfo.version, isCustomDomain: config.isCustomDomain(),
appstore: {
token: config.token(),
apiServerOrigin: config.apiServerOrigin()
},
caas: {
token: config.token(),
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin() webServerOrigin: config.webServerOrigin()
},
tlsConfig: {
provider: 'caas',
cert: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), 'utf8'),
key: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), 'utf8'),
},
version: boxUpdateInfo.version,
boxVersionsUrl: config.get('boxVersionsUrl')
} }
}; };
+10 -24
View File
@@ -4,6 +4,8 @@
exports = module.exports = { exports = module.exports = {
baseDir: baseDir, baseDir: baseDir,
dnsInSync: dnsInSync,
setDnsInSync: setDnsInSync,
// values set here will be lost after a upgrade/update. use the sqlite database // values set here will be lost after a upgrade/update. use the sqlite database
// for persistent values that need to be backed up // for persistent values that need to be backed up
@@ -31,9 +33,6 @@ exports = module.exports = {
isDev: isDev, isDev: isDev,
backupKey: backupKey,
aws: aws,
// for testing resets to defaults // for testing resets to defaults
_reset: initConfig _reset: initConfig
}; };
@@ -56,6 +55,14 @@ function baseDir() {
var cloudronConfigFileName = path.join(baseDir(), 'configs/cloudron.conf'); var cloudronConfigFileName = path.join(baseDir(), 'configs/cloudron.conf');
function dnsInSync() {
return !!safe.fs.statSync(require('./paths.js').DNS_IN_SYNC_FILE);
}
function setDnsInSync(content) {
safe.fs.writeFileSync(require('./paths.js').DNS_IN_SYNC_FILE, content || 'if this file exists, dns is in sync');
}
function saveSync() { function saveSync() {
fs.writeFileSync(cloudronConfigFileName, JSON.stringify(data, null, 4)); // functions are ignored by JSON.stringify fs.writeFileSync(cloudronConfigFileName, JSON.stringify(data, null, 4)); // functions are ignored by JSON.stringify
} }
@@ -65,9 +72,7 @@ function initConfig() {
data.fqdn = 'localhost'; data.fqdn = 'localhost';
data.token = null; data.token = null;
data.mailServer = null;
data.adminEmail = null; data.adminEmail = null;
data.mailDnsRecordIds = [ ];
data.boxVersionsUrl = null; data.boxVersionsUrl = null;
data.version = null; data.version = null;
data.isCustomDomain = false; data.isCustomDomain = false;
@@ -76,14 +81,6 @@ function initConfig() {
data.ldapPort = 3002; data.ldapPort = 3002;
data.oauthProxyPort = 3003; data.oauthProxyPort = 3003;
data.simpleAuthPort = 3004; data.simpleAuthPort = 3004;
data.backupKey = 'backupKey';
data.aws = {
backupBucket: null,
backupPrefix: null,
accessKeyId: null, // selfhosting only
secretAccessKey: null // selfhosting only
};
data.dnsInSync = false;
if (exports.CLOUDRON) { if (exports.CLOUDRON) {
data.port = 3000; data.port = 3000;
@@ -100,9 +97,6 @@ function initConfig() {
name: 'boxtest' name: 'boxtest'
}; };
data.token = 'APPSTORE_TOKEN'; data.token = 'APPSTORE_TOKEN';
data.aws.backupBucket = 'testbucket';
data.aws.backupPrefix = 'testprefix';
data.aws.endpoint = 'http://localhost:5353';
} else { } else {
assert(false, 'Unknown environment. This should not happen!'); assert(false, 'Unknown environment. This should not happen!');
} }
@@ -195,11 +189,3 @@ function database() {
function isDev() { function isDev() {
return /dev/i.test(get('boxVersionsUrl')); return /dev/i.test(get('boxVersionsUrl'));
} }
function backupKey() {
return get('backupKey');
}
function aws() {
return get('aws');
}
+41 -26
View File
@@ -8,9 +8,11 @@ exports = module.exports = {
var apps = require('./apps.js'), var apps = require('./apps.js'),
assert = require('assert'), assert = require('assert'),
cloudron = require('./cloudron.js'), cloudron = require('./cloudron.js'),
config = require('./config.js'),
CronJob = require('cron').CronJob, CronJob = require('cron').CronJob,
debug = require('debug')('box:cron'), debug = require('debug')('box:cron'),
janitor = require('./janitor.js'), janitor = require('./janitor.js'),
scheduler = require('./scheduler.js'),
settings = require('./settings.js'), settings = require('./settings.js'),
updateChecker = require('./updatechecker.js'); updateChecker = require('./updatechecker.js');
@@ -20,9 +22,8 @@ var gAutoupdaterJob = null,
gHeartbeatJob = null, gHeartbeatJob = null,
gBackupJob = null, gBackupJob = null,
gCleanupTokensJob = null, gCleanupTokensJob = null,
gDockerVolumeCleanerJob = null; gDockerVolumeCleanerJob = null,
gSchedulerSyncJob = null;
var gInitialized = false;
var NOOP_CALLBACK = function (error) { if (error) console.error(error); }; var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
@@ -37,14 +38,19 @@ var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
function initialize(callback) { function initialize(callback) {
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
if (gInitialized) return callback(); gHeartbeatJob = new CronJob({
cronTime: '00 */1 * * * *', // every minute
settings.events.on(settings.TIME_ZONE_KEY, recreateJobs); onTick: cloudron.sendHeartbeat,
settings.events.on(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged); start: true
});
gInitialized = true; cloudron.sendHeartbeat(); // latest unpublished version of CronJob has runOnInit
if (cloudron.isConfiguredSync()) {
recreateJobs(callback); recreateJobs(callback);
} else {
cloudron.events.on(cloudron.EVENT_ACTIVATED, recreateJobs);
callback();
}
} }
function recreateJobs(unusedTimeZone, callback) { function recreateJobs(unusedTimeZone, callback) {
@@ -53,14 +59,6 @@ function recreateJobs(unusedTimeZone, callback) {
settings.getAll(function (error, allSettings) { settings.getAll(function (error, allSettings) {
debug('Creating jobs with timezone %s', allSettings[settings.TIME_ZONE_KEY]); 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(); if (gBackupJob) gBackupJob.stop();
gBackupJob = new CronJob({ gBackupJob = new CronJob({
cronTime: '00 00 */4 * * *', // every 4 hours cronTime: '00 00 */4 * * *', // every 4 hours
@@ -101,14 +99,28 @@ function recreateJobs(unusedTimeZone, callback) {
timeZone: allSettings[settings.TIME_ZONE_KEY] timeZone: allSettings[settings.TIME_ZONE_KEY]
}); });
if (gSchedulerSyncJob) gSchedulerSyncJob.stop();
gSchedulerSyncJob = new CronJob({
cronTime: config.TEST ? '*/10 * * * * *' : '00 */1 * * * *', // every minute
onTick: scheduler.sync,
start: true,
timeZone: allSettings[settings.TIME_ZONE_KEY]
});
settings.events.removeListener(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
settings.events.on(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
autoupdatePatternChanged(allSettings[settings.AUTOUPDATE_PATTERN_KEY]); 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(); if (callback) callback();
}); });
} }
function autoupdatePatternChanged(pattern) { function autoupdatePatternChanged(pattern) {
assert.strictEqual(typeof pattern, 'string'); assert.strictEqual(typeof pattern, 'string');
assert(gBoxUpdateCheckerJob);
debug('Auto update pattern changed to %s', pattern); debug('Auto update pattern changed to %s', pattern);
@@ -138,31 +150,34 @@ function autoupdatePatternChanged(pattern) {
function uninitialize(callback) { function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function'); 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(); if (gAutoupdaterJob) gAutoupdaterJob.stop();
gAutoupdaterJob = null; gAutoupdaterJob = null;
gBoxUpdateCheckerJob.stop(); if (gBoxUpdateCheckerJob) gBoxUpdateCheckerJob.stop();
gBoxUpdateCheckerJob = null; gBoxUpdateCheckerJob = null;
gAppUpdateCheckerJob.stop(); if (gAppUpdateCheckerJob) gAppUpdateCheckerJob.stop();
gAppUpdateCheckerJob = null; gAppUpdateCheckerJob = null;
gHeartbeatJob.stop(); if (gHeartbeatJob) gHeartbeatJob.stop();
gHeartbeatJob = null; gHeartbeatJob = null;
gBackupJob.stop(); if (gBackupJob) gBackupJob.stop();
gBackupJob = null; gBackupJob = null;
gCleanupTokensJob.stop(); if (gCleanupTokensJob) gCleanupTokensJob.stop();
gCleanupTokensJob = null; gCleanupTokensJob = null;
gDockerVolumeCleanerJob.stop(); if (gDockerVolumeCleanerJob) gDockerVolumeCleanerJob.stop();
gDockerVolumeCleanerJob = null; gDockerVolumeCleanerJob = null;
gInitialized = false; if (gSchedulerSyncJob) gSchedulerSyncJob.stop();
gSchedulerSyncJob = null;
callback(); 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();
});
}
+136
View File
@@ -0,0 +1,136 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
add: add,
del: del,
update: update,
getChangeStatus: getChangeStatus,
get: get
};
var assert = require('assert'),
config = require('../config.js'),
debug = require('debug')('box:dns/caas'),
SubdomainError = require('../subdomains.js').SubdomainError,
superagent = require('superagent'),
util = require('util'),
_ = require('underscore');
function add(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
var fqdn = subdomain !== '' && type === 'TXT' ? subdomain + '.' + config.fqdn() : config.appFqdn(subdomain);
debug('add: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
var data = {
type: type,
values: values
};
superagent
.post(config.apiServerOrigin() + '/api/v1/domains/' + fqdn)
.query({ token: dnsConfig.token })
.send(data)
.end(function (error, result) {
if (error) return callback(error);
if (result.status === 420) return callback(new SubdomainError(SubdomainError.STILL_BUSY));
if (result.status !== 201) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
return callback(null, result.body.changeId);
});
}
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
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: dnsConfig.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(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
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(dnsConfig, zoneName, subdomain, type, function (error, result) {
if (error) return callback(error);
if (_.isEqual(values, result)) return callback();
add(dnsConfig, zoneName, subdomain, type, values, callback);
});
}
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
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,
values: values
};
superagent
.del(config.apiServerOrigin() + '/api/v1/domains/' + config.appFqdn(subdomain))
.query({ token: dnsConfig.token })
.send(data)
.end(function (error, result) {
if (error) return callback(error);
if (result.status === 420) return callback(new SubdomainError(SubdomainError.STILL_BUSY));
if (result.status === 404) return callback(new SubdomainError(SubdomainError.NOT_FOUND));
if (result.status !== 204) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
return callback(null);
});
}
function getChangeStatus(dnsConfig, changeId, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof changeId, 'string');
assert.strictEqual(typeof callback, 'function');
if (changeId === '') return callback(null, 'INSYNC');
superagent
.get(config.apiServerOrigin() + '/api/v1/domains/' + config.fqdn() + '/status/' + changeId)
.query({ token: dnsConfig.token })
.end(function (error, result) {
if (error) return callback(error);
if (result.status !== 200) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
return callback(null, result.body.status);
});
}
+214
View File
@@ -0,0 +1,214 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
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:dns/route53'),
SubdomainError = require('../subdomains.js').SubdomainError,
util = require('util'),
_ = require('underscore');
function getDnsCredentials(dnsConfig) {
assert.strictEqual(typeof dnsConfig, 'object');
var credentials = {
accessKeyId: dnsConfig.accessKeyId,
secretAccessKey: dnsConfig.secretAccessKey,
region: dnsConfig.region
};
if (dnsConfig.endpoint) credentials.endpoint = new AWS.Endpoint(dnsConfig.endpoint);
return credentials;
}
function getZoneByName(dnsConfig, zoneName, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof callback, 'function');
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
route53.listHostedZones({}, function (error, result) {
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
var zone = result.HostedZones.filter(function (zone) {
return zone.Name.slice(0, -1) === zoneName; // aws zone name contains a '.' at the end
})[0];
if (!zone) return callback(new SubdomainError(SubdomainError.NOT_FOUND, 'no such zone'));
callback(null, zone);
});
}
function add(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
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);
getZoneByName(dnsConfig, 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: [{
Action: 'UPSERT',
ResourceRecordSet: {
Type: type,
Name: fqdn,
ResourceRecords: records,
TTL: 1
}
}]
},
HostedZoneId: zone.Id
};
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
route53.changeResourceRecordSets(params, function(error, result) {
if (error && error.code === 'PriorRequestNotComplete') {
return callback(new SubdomainError(SubdomainError.STILL_BUSY, error.message));
} else if (error) {
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
}
callback(null, result.ChangeInfo.Id);
});
});
}
function update(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
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(dnsConfig, zoneName, subdomain, type, function (error, result) {
if (error) return callback(error);
if (_.isEqual(values, result)) return callback();
add(dnsConfig, zoneName, subdomain, type, values, callback);
});
}
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
getZoneByName(dnsConfig, zoneName, function (error, zone) {
if (error) return callback(error);
var params = {
HostedZoneId: zone.Id,
MaxItems: '1',
StartRecordName: (subdomain ? subdomain + '.' : '') + zoneName + '.',
StartRecordType: type
};
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
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(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
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(dnsConfig, 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: records,
TTL: 1
};
var params = {
ChangeBatch: {
Changes: [{
Action: 'DELETE',
ResourceRecordSet: resourceRecordSet
}]
},
HostedZoneId: zone.Id
};
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
route53.changeResourceRecordSets(params, function(error, result) {
if (error && error.message && error.message.indexOf('it was not found') !== -1) {
debug('delSubdomain: resource record set not found.', error);
return callback(new SubdomainError(SubdomainError.NOT_FOUND, new Error(error)));
} else if (error && error.code === 'NoSuchHostedZone') {
debug('delSubdomain: hosted zone not found.', error);
return callback(new SubdomainError(SubdomainError.NOT_FOUND, new Error(error)));
} else if (error && error.code === 'PriorRequestNotComplete') {
debug('delSubdomain: resource is still busy', error);
return callback(new SubdomainError(SubdomainError.STILL_BUSY, new Error(error)));
} else if (error && error.code === 'InvalidChangeBatch') {
debug('delSubdomain: invalid change batch. No such record to be deleted.');
return callback(new SubdomainError(SubdomainError.NOT_FOUND, new Error(error)));
} else if (error) {
debug('delSubdomain: error', error);
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
}
callback(null);
});
});
}
function getChangeStatus(dnsConfig, changeId, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof changeId, 'string');
assert.strictEqual(typeof callback, 'function');
if (changeId === '') return callback(null, 'INSYNC');
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
route53.getChange({ Id: changeId }, function (error, result) {
if (error) return callback(error);
callback(null, result.ChangeInfo.Status);
});
}
+335 -30
View File
@@ -1,42 +1,347 @@
'use strict'; 'use strict';
var Docker = require('dockerode'), var addons = require('./addons.js'),
fs = require('fs'), async = require('async'),
os = require('os'), assert = require('assert'),
path = require('path'), config = require('./config.js'),
url = require('url'); debug = require('debug')('box:src/docker.js'),
Docker = require('dockerode'),
safe = require('safetydance'),
semver = require('semver'),
util = require('util'),
_ = require('underscore');
exports = module.exports = (function () { exports = module.exports = {
connection: connectionInstance(),
downloadImage: downloadImage,
createContainer: createContainer,
startContainer: startContainer,
stopContainer: stopContainer,
stopContainers: stopContainers,
deleteContainer: deleteContainer,
deleteImage: deleteImage,
deleteContainers: deleteContainers,
createSubcontainer: createSubcontainer
};
function connectionInstance() {
var docker; var docker;
var options = connectOptions(); // the real docker
if (process.env.BOX_ENV === 'test') { if (process.env.BOX_ENV === 'test') {
// test code runs a docker proxy on this port // test code runs a docker proxy on this port
docker = new Docker({ host: 'http://localhost', port: 5687 }); docker = new Docker({ host: 'http://localhost', port: 5687 });
} else {
docker = new Docker(options);
}
// proxy code uses this to route to the real docker // proxy code uses this to route to the real docker
docker.options = options; docker.options = { socketPath: '/var/run/docker.sock' };
} else {
return docker; docker = new Docker({ socketPath: '/var/run/docker.sock' });
})();
function connectOptions() {
if (os.platform() === 'linux') return { socketPath: '/var/run/docker.sock' };
// boot2docker configuration
var DOCKER_CERT_PATH = process.env.DOCKER_CERT_PATH || path.join(process.env.HOME, '.boot2docker/certs/boot2docker-vm');
var DOCKER_HOST = process.env.DOCKER_HOST || 'tcp://192.168.59.103:2376';
return {
protocol: 'https',
host: url.parse(DOCKER_HOST).hostname,
port: url.parse(DOCKER_HOST).port,
ca: fs.readFileSync(path.join(DOCKER_CERT_PATH, 'ca.pem')),
cert: fs.readFileSync(path.join(DOCKER_CERT_PATH, 'cert.pem')),
key: fs.readFileSync(path.join(DOCKER_CERT_PATH, 'key.pem'))
};
} }
return docker;
}
function debugApp(app, args) {
assert(!app || typeof app === 'object');
var prefix = app ? (app.location || '(bare)') : '(no app)';
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
function targetBoxVersion(manifest) {
if ('targetBoxVersion' in manifest) return manifest.targetBoxVersion;
if ('minBoxVersion' in manifest) return manifest.minBoxVersion;
return '0.0.1';
}
function pullImage(manifest, callback) {
var docker = exports.connection;
docker.pull(manifest.dockerImage, function (err, stream) {
if (err) return callback(new Error('Error connecting to docker. statusCode: %s' + err.statusCode));
// https://github.com/dotcloud/docker/issues/1074 says each status message
// is emitted as a chunk
stream.on('data', function (chunk) {
var data = safe.JSON.parse(chunk) || { };
debug('pullImage %s: %j', manifest.id, data);
// The information here is useless because this is per layer as opposed to per image
if (data.status) {
} else if (data.error) {
debug('pullImage error %s: %s', manifest.id, data.errorDetail.message);
}
});
stream.on('end', function () {
debug('downloaded image %s of %s successfully', manifest.dockerImage, manifest.id);
var image = docker.getImage(manifest.dockerImage);
image.inspect(function (err, data) {
if (err) return callback(new Error('Error inspecting image:' + err.message));
if (!data || !data.Config) return callback(new Error('Missing Config in image:' + JSON.stringify(data, null, 4)));
if (!data.Config.Entrypoint && !data.Config.Cmd) return callback(new Error('Only images with entry point are allowed'));
debug('This image of %s exposes ports: %j', manifest.id, data.Config.ExposedPorts);
callback(null);
});
});
stream.on('error', function (error) {
debug('error pulling image %s of %s: %j', manifest.dockerImage, manifest.id, error);
callback(error);
});
});
}
function downloadImage(manifest, callback) {
assert.strictEqual(typeof manifest, 'object');
assert.strictEqual(typeof callback, 'function');
debug('downloadImage %s %s', manifest.id, manifest.dockerImage);
var attempt = 1;
async.retry({ times: 10, interval: 15000 }, function (retryCallback) {
debug('Downloading image %s %s. attempt: %s', manifest.id, manifest.dockerImage, attempt++);
pullImage(manifest, function (error) {
if (error) console.error(error);
retryCallback(error);
});
}, callback);
}
function createSubcontainer(app, name, cmd, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof name, 'string');
assert(!cmd || util.isArray(cmd));
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var docker = exports.connection,
isAppContainer = !cmd;
var manifest = app.manifest;
var exposedPorts = {}, dockerPortBindings = { };
var stdEnv = [
'CLOUDRON=1',
'WEBADMIN_ORIGIN=' + config.adminOrigin(),
'API_ORIGIN=' + config.adminOrigin(),
'APP_ORIGIN=https://' + config.appFqdn(app.location),
'APP_DOMAIN=' + config.appFqdn(app.location)
];
// docker portBindings requires ports to be exposed
exposedPorts[manifest.httpPort + '/tcp'] = {};
dockerPortBindings[manifest.httpPort + '/tcp'] = [ { HostIp: '127.0.0.1', HostPort: app.httpPort + '' } ];
var portEnv = [];
for (var e in app.portBindings) {
var hostPort = app.portBindings[e];
var containerPort = manifest.tcpPorts[e].containerPort || hostPort;
exposedPorts[containerPort + '/tcp'] = {};
portEnv.push(e + '=' + hostPort);
dockerPortBindings[containerPort + '/tcp'] = [ { HostIp: '0.0.0.0', HostPort: hostPort + '' } ];
}
var memoryLimit = manifest.memoryLimit || 1024 * 1024 * 200; // 200mb by default
addons.getEnvironment(app, function (error, addonEnv) {
if (error) return callback(new Error('Error getting addon environment : ' + error));
var containerOptions = {
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,
Env: stdEnv.concat(addonEnv).concat(portEnv),
ExposedPorts: isAppContainer ? exposedPorts : { },
Volumes: { // see also ReadonlyRootfs
'/tmp': {},
'/run': {}
},
Labels: {
"location": app.location,
"appId": app.id,
"isSubcontainer": String(!isAppContainer)
},
HostConfig: {
Binds: addons.getBindsSync(app, app.manifest.addons),
Memory: memoryLimit / 2,
MemorySwap: memoryLimit, // Memory + Swap
PortBindings: isAppContainer ? dockerPortBindings : { },
PublishAllPorts: false,
ReadonlyRootfs: semver.gte(targetBoxVersion(app.manifest), '0.0.66'), // see also Volumes in startContainer
Links: addons.getLinksSync(app, app.manifest.addons),
RestartPolicy: {
"Name": isAppContainer ? "always" : "no",
"MaximumRetryCount": 0
},
CpuShares: 512, // relative to 1024 for system processes
VolumesFrom: isAppContainer ? null : [ app.containerId + ":rw" ],
SecurityOpt: config.CLOUDRON ? [ "apparmor:docker-cloudron-app" ] : null // profile available only on cloudron
}
};
containerOptions = _.extend(containerOptions, options);
// older versions wanted a writable /var/log
if (semver.lte(targetBoxVersion(app.manifest), '0.0.71')) containerOptions.Volumes['/var/log'] = {};
debugApp(app, 'Creating container for %s with options %j', app.manifest.dockerImage, containerOptions);
docker.createContainer(containerOptions, callback);
});
}
function createContainer(app, callback) {
createSubcontainer(app, app.id /* name */, null /* cmd */, { } /* options */, callback);
}
function startContainer(containerId, callback) {
assert.strictEqual(typeof containerId, 'string');
assert.strictEqual(typeof callback, 'function');
var docker = exports.connection;
var container = docker.getContainer(containerId);
debug('Starting container %s', containerId);
container.start(function (error) {
if (error && error.statusCode !== 304) return callback(new Error('Error starting container :' + error));
return callback(null);
});
}
function stopContainer(containerId, callback) {
assert(!containerId || typeof containerId === 'string');
assert.strictEqual(typeof callback, 'function');
if (!containerId) {
debug('No previous container to stop');
return callback();
}
var docker = exports.connection;
var container = docker.getContainer(containerId);
debug('Stopping container %s', containerId);
var options = {
t: 10 // wait for 10 seconds before killing it
};
container.stop(options, function (error) {
if (error && (error.statusCode !== 304 && error.statusCode !== 404)) return callback(new Error('Error stopping container:' + error));
debug('Waiting for container ' + containerId);
container.wait(function (error, data) {
if (error && (error.statusCode !== 304 && error.statusCode !== 404)) return callback(new Error('Error waiting on container:' + error));
debug('Container %s stopped with status code [%s]', containerId, data ? String(data.StatusCode) : '');
return callback(null);
});
});
}
function deleteContainer(containerId, callback) {
assert(!containerId || typeof containerId === 'string');
assert.strictEqual(typeof callback, 'function');
debug('deleting container %s', containerId);
if (containerId === null) return callback(null);
var docker = exports.connection;
var container = docker.getContainer(containerId);
var removeOptions = {
force: true, // kill container if it's running
v: true // removes volumes associated with the container (but not host mounts)
};
container.remove(removeOptions, function (error) {
if (error && error.statusCode === 404) return callback(null);
if (error) debug('Error removing container %s : %j', containerId, error);
callback(error);
});
}
function deleteContainers(appId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
var docker = exports.connection;
debug('deleting containers of %s', appId);
docker.listContainers({ all: 1, filters: JSON.stringify({ label: [ 'appId=' + appId ] }) }, function (error, containers) {
if (error) return callback(error);
async.eachSeries(containers, function (container, iteratorDone) {
deleteContainer(container.Id, iteratorDone);
}, callback);
});
}
function stopContainers(appId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
var docker = exports.connection;
debug('stopping containers of %s', appId);
docker.listContainers({ all: 1, filters: JSON.stringify({ label: [ 'appId=' + appId ] }) }, function (error, containers) {
if (error) return callback(error);
async.eachSeries(containers, function (container, iteratorDone) {
stopContainer(container.Id, iteratorDone);
}, callback);
});
}
function deleteImage(manifest, callback) {
assert(!manifest || typeof manifest === 'object');
assert.strictEqual(typeof callback, 'function');
var dockerImage = manifest ? manifest.dockerImage : null;
if (!dockerImage) return callback(null);
var docker = exports.connection;
docker.getImage(dockerImage).inspect(function (error, result) {
if (error && error.statusCode === 404) return callback(null);
if (error) return callback(error);
var removeOptions = {
force: true,
noprune: false
};
// delete image by id because 'docker pull' pulls down all the tags and this is the only way to delete all tags
docker.getImage(result.Id).remove(removeOptions, function (error) {
if (error && error.statusCode === 404) return callback(null);
if (error && error.statusCode === 409) return callback(null); // another container using the image
if (error) debug('Error removing image %s : %j', dockerImage, error);
callback(error);
});
});
}
+6 -2
View File
@@ -4,7 +4,7 @@ var assert = require('assert'),
async = require('async'), async = require('async'),
authcodedb = require('./authcodedb.js'), authcodedb = require('./authcodedb.js'),
debug = require('debug')('box:src/janitor'), debug = require('debug')('box:src/janitor'),
docker = require('./docker.js'), docker = require('./docker.js').connection,
tokendb = require('./tokendb.js'); tokendb = require('./tokendb.js');
exports = module.exports = { exports = module.exports = {
@@ -12,6 +12,8 @@ exports = module.exports = {
cleanupDockerVolumes: cleanupDockerVolumes cleanupDockerVolumes: cleanupDockerVolumes
}; };
var NOOP_CALLBACK = function () { };
function ignoreError(func) { function ignoreError(func) {
return function (callback) { return function (callback) {
func(function (error) { func(function (error) {
@@ -83,9 +85,11 @@ function cleanupTmpVolume(containerInfo, callback) {
function cleanupDockerVolumes(callback) { function cleanupDockerVolumes(callback) {
assert(!callback || typeof callback === 'function'); // callback is null when called from cronjob assert(!callback || typeof callback === 'function'); // callback is null when called from cronjob
callback = callback || NOOP_CALLBACK;
debug('Cleaning up docker volumes'); debug('Cleaning up docker volumes');
docker.listContainers(function (error, containers) { docker.listContainers({ all: 0 }, function (error, containers) {
if (error) return callback(error); if (error) return callback(error);
async.eachSeries(containers, function (container, iteratorDone) { async.eachSeries(containers, function (container, iteratorDone) {
+2 -2
View File
@@ -2,9 +2,9 @@
Dear <%= user.username %>, 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. at https://www.cloudron.io.
You username is '<%= user.username %>' You username is '<%= user.username %>'
+75 -13
View File
@@ -25,16 +25,16 @@ exports = module.exports = {
var assert = require('assert'), var assert = require('assert'),
async = require('async'), async = require('async'),
cloudron = require('./cloudron.js'),
config = require('./config.js'), config = require('./config.js'),
debug = require('debug')('box:mailer'), debug = require('debug')('box:mailer'),
digitalocean = require('./digitalocean.js'), dns = require('native-dns'),
docker = require('./docker.js'), docker = require('./docker.js').connection,
ejs = require('ejs'), ejs = require('ejs'),
nodemailer = require('nodemailer'), nodemailer = require('nodemailer'),
path = require('path'), path = require('path'),
safe = require('safetydance'), safe = require('safetydance'),
smtpTransport = require('nodemailer-smtp-transport'), smtpTransport = require('nodemailer-smtp-transport'),
sysinfo = require('./sysinfo.js'),
userdb = require('./userdb.js'), userdb = require('./userdb.js'),
util = require('util'), util = require('util'),
_ = require('underscore'); _ = require('underscore');
@@ -48,13 +48,20 @@ var gMailQueue = [ ],
function initialize(callback) { function initialize(callback) {
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
if (cloudron.isConfiguredSync()) {
checkDns(); checkDns();
} else {
cloudron.events.on(cloudron.EVENT_CONFIGURED, checkDns);
}
callback(null); callback(null);
} }
function uninitialize(callback) { function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
cloudron.events.removeListener(cloudron.EVENT_CONFIGURED, checkDns);
// TODO: interrupt processQueue as well // TODO: interrupt processQueue as well
clearTimeout(gCheckDnsTimerId); clearTimeout(gCheckDnsTimerId);
gCheckDnsTimerId = null; gCheckDnsTimerId = null;
@@ -65,20 +72,76 @@ function uninitialize(callback) {
callback(null); 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() { function checkDns() {
digitalocean.checkPtrRecord(sysinfo.getIp(), config.fqdn(), function (error, ok) { getTxtRecords(function (error, records) {
if (error || !ok) { if (error || !records) {
debug('PTR record not setup yet'); debug('checkDns: DNS error or no records looking up TXT records for %s %s', config.fqdn(), error, records);
gCheckDnsTimerId = setTimeout(checkDns, 10000); gCheckDnsTimerId = setTimeout(checkDns, 60000);
return; 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; gDnsReady = true;
processQueue(); processQueue();
}); });
} }
function processQueue() { function processQueue() {
assert(gDnsReady);
sendMails(gMailQueue);
gMailQueue = [ ];
}
// note : this function should NOT access the database. it is called by the crashnotifier
// which does not initialize mailer or the databse
function sendMails(queue) {
assert(util.isArray(queue));
docker.getContainer('mail').inspect(function (error, data) { docker.getContainer('mail').inspect(function (error, data) {
if (error) return console.error(error); if (error) return console.error(error);
@@ -90,12 +153,9 @@ function processQueue() {
port: 2500 // this value comes from mail container port: 2500 // this value comes from mail container
})); }));
var mailQueueCopy = gMailQueue; debug('Processing mail queue of size %d (through %s:2500)', queue.length, mailServerIp);
gMailQueue = [ ];
debug('Processing mail queue of size %d (through %s:2500)', mailQueueCopy.length, mailServerIp); async.mapSeries(queue, function iterator(mailOptions, callback) {
async.mapSeries(mailQueueCopy, function iterator(mailOptions, callback) {
transport.sendMail(mailOptions, function (error) { transport.sendMail(mailOptions, function (error) {
if (error) return console.error(error); // TODO: requeue? if (error) return console.error(error); // TODO: requeue?
debug('Email sent to ' + mailOptions.to); debug('Email sent to ' + mailOptions.to);
@@ -269,6 +329,8 @@ function appUpdateAvailable(app, updateInfo) {
}); });
} }
// this function bypasses the queue intentionally. it is also expected to work without the mailer module initialized
// crashnotifier should be able to send mail when there is no db
function sendCrashNotification(program, context) { function sendCrashNotification(program, context) {
assert.strictEqual(typeof program, 'string'); assert.strictEqual(typeof program, 'string');
assert.strictEqual(typeof context, 'string'); assert.strictEqual(typeof context, 'string');
@@ -280,7 +342,7 @@ function sendCrashNotification(program, context) {
text: render('crash_notification.ejs', { fqdn: config.fqdn(), program: program, context: context, format: 'text' }) text: render('crash_notification.ejs', { fqdn: config.fqdn(), program: program, context: context, format: 'text' })
}; };
enqueue(mailOptions); sendMails([ mailOptions ]);
} }
function sendFeedback(user, type, subject, description) { function sendFeedback(user, type, subject, description) {
+3 -1
View File
@@ -6,6 +6,8 @@
<title> Cloudron Login </title> <title> Cloudron Login </title>
<link href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
<!-- Custom Fonts --> <!-- Custom Fonts -->
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" type="text/css"> <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"> <link href="https://fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet" type="text/css">
@@ -32,7 +34,7 @@
<div class="container-fluid"> <div class="container-fluid">
<div class="navbar-header"> <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 navbar-brand-icon"><img src="/api/v1/cloudron/avatar" width="40" height="40"/></span>
<span class="navbar-brand"><%= cloudronName %></span> <span class="navbar-brand">Cloudron</span>
</div> </div>
</div> </div>
</nav> </nav>
+17 -14
View File
@@ -9,12 +9,14 @@ var appdb = require('./appdb.js'),
assert = require('assert'), assert = require('assert'),
clientdb = require('./clientdb.js'), clientdb = require('./clientdb.js'),
config = require('./config.js'), config = require('./config.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:proxy'), debug = require('debug')('box:proxy'),
express = require('express'), express = require('express'),
http = require('http'), http = require('http'),
proxy = require('proxy-middleware'), proxy = require('proxy-middleware'),
session = require('cookie-session'), session = require('cookie-session'),
superagent = require('superagent'), superagent = require('superagent'),
tokendb = require('./tokendb.js'),
url = require('url'), url = require('url'),
uuid = require('node-uuid'); uuid = require('node-uuid');
@@ -24,13 +26,20 @@ var gHttpServer = null;
var CALLBACK_URI = '/callback'; var CALLBACK_URI = '/callback';
function clearSession(req) {
delete gSessions[req.session.id];
req.session.id = uuid.v4();
gSessions[req.session.id] = {};
req.sessionData = gSessions[req.session.id];
}
function attachSessionData(req, res, next) { function attachSessionData(req, res, next) {
assert.strictEqual(typeof req.session, 'object'); assert.strictEqual(typeof req.session, 'object');
if (!req.session.id || !gSessions[req.session.id]) { if (!req.session.id || !gSessions[req.session.id]) clearSession(req);
req.session.id = uuid.v4();
gSessions[req.session.id] = {};
}
// attach the session data to the requeset // attach the session data to the requeset
req.sessionData = gSessions[req.session.id]; req.sessionData = gSessions[req.session.id];
@@ -46,16 +55,10 @@ function verifySession(req, res, next) {
return next(); return next();
} }
// use http admin origin so that it works with self-signed certs tokendb.get(req.sessionData.accessToken, function (error, token) {
superagent
.get(config.internalAdminOrigin() + '/api/v1/profile')
.query({ access_token: req.sessionData.accessToken})
.end(function (error, result) {
if (error) { if (error) {
console.error(error); if (error.reason !== DatabaseError.NOT_FOUND) console.error(error);
req.authenticated = false; clearSession(req);
} else if (result.statusCode !== 200) {
req.sessionData.accessToken = null;
req.authenticated = false; req.authenticated = false;
} else { } else {
req.authenticated = true; req.authenticated = true;
@@ -121,7 +124,7 @@ function authenticate(req, res, next) {
return res.send(500, 'Unknown app.'); return res.send(500, 'Unknown app.');
} }
clientdb.getByAppId(result.id, clientdb.TYPE_PROXY, function (error, result) { clientdb.getByAppIdAndType(result.id, clientdb.TYPE_PROXY, function (error, result) {
if (error) { if (error) {
console.error('Unkonwn OAuth client.', error); console.error('Unkonwn OAuth client.', error);
return res.send(500, 'Unknown OAuth client.'); return res.send(500, 'Unknown OAuth client.');
+4 -2
View File
@@ -12,6 +12,9 @@ exports = module.exports = {
NGINX_CERT_DIR: path.join(config.baseDir(), 'data/nginx/cert'), NGINX_CERT_DIR: path.join(config.baseDir(), 'data/nginx/cert'),
ADDON_CONFIG_DIR: path.join(config.baseDir(), 'data/addons'), ADDON_CONFIG_DIR: path.join(config.baseDir(), 'data/addons'),
SCHEDULER_FILE: path.join(config.baseDir(), 'data/addons/scheduler.json'),
DNS_IN_SYNC_FILE: path.join(config.baseDir(), 'data/dns_in_sync'),
COLLECTD_APPCONFIG_DIR: path.join(config.baseDir(), 'data/collectd/collectd.conf.d'), COLLECTD_APPCONFIG_DIR: path.join(config.baseDir(), 'data/collectd/collectd.conf.d'),
@@ -19,12 +22,11 @@ exports = module.exports = {
BOX_DATA_DIR: path.join(config.baseDir(), 'data/box'), BOX_DATA_DIR: path.join(config.baseDir(), 'data/box'),
// this is not part of appdata because an icon may be set before install // this is not part of appdata because an icon may be set before install
APPICONS_DIR: path.join(config.baseDir(), 'data/box/appicons'), 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'), MAIL_DATA_DIR: path.join(config.baseDir(), 'data/box/mail'),
CLOUDRON_AVATAR_FILE: path.join(config.baseDir(), 'data/box/avatar.png'), CLOUDRON_AVATAR_FILE: path.join(config.baseDir(), 'data/box/avatar.png'),
CLOUDRON_DEFAULT_AVATAR_FILE: path.join(__dirname + '/../assets/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') UPDATE_CHECKER_FILE: path.join(config.baseDir(), 'data/box/updatechecker.json')
}; };
+22 -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.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 (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 ('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 // allow tests to provide an appId for testing
var appId = (process.env.BOX_ENV === 'test' && typeof data.appId === 'string') ? data.appId : uuid.v4(); 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); 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.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_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.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.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.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 && error.reason === AppsError.USER_REQUIRED) return next(new HttpError(400, 'accessRestriction must specify one user'));
if (error) return next(new HttpError(500, error)); 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 (('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.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 (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); 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.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_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.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.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_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_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)); if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, { })); next(new HttpSuccess(202, { }));
@@ -276,14 +286,14 @@ function getLogStream(req, res, next) {
debug('Getting logstream of app id:%s', req.params.id); 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 var lines = req.query.lines ? parseInt(req.query.lines, 10) : -10; // we ignore last-event-id
if (isNaN(fromLine)) return next(new HttpError(400, 'fromLine must be a valid number')); 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'; } 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')); 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.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 && error.reason === AppsError.BAD_STATE) return next(new HttpError(412, error.message));
if (error) return next(new HttpError(500, error)); if (error) return next(new HttpError(500, error));
@@ -299,7 +309,7 @@ function getLogStream(req, res, next) {
res.on('close', logStream.close); res.on('close', logStream.close);
logStream.on('data', function (data) { logStream.on('data', function (data) {
var obj = JSON.parse(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('end', res.end.bind(res));
logStream.on('error', res.end.bind(res, null)); logStream.on('error', res.end.bind(res, null));
@@ -309,9 +319,12 @@ function getLogStream(req, res, next) {
function getLogs(req, res, next) { function getLogs(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string'); 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); 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.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 && error.reason === AppsError.BAD_STATE) return next(new HttpError(412, error.message));
if (error) return next(new HttpError(500, error)); if (error) return next(new HttpError(500, error));
@@ -355,6 +368,8 @@ function exec(req, res, next) {
duplexStream.pipe(res.socket); duplexStream.pipe(res.socket);
res.socket.pipe(duplexStream); res.socket.pipe(duplexStream);
res.on('close', duplexStream.close);
}); });
} }
+1 -23
View File
@@ -11,7 +11,6 @@ exports = module.exports = {
getConfig: getConfig, getConfig: getConfig,
update: update, update: update,
migrate: migrate, migrate: migrate,
setCertificate: setCertificate,
feedback: feedback feedback: feedback
}; };
@@ -25,7 +24,6 @@ var assert = require('assert'),
HttpError = require('connect-lastmile').HttpError, HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess, HttpSuccess = require('connect-lastmile').HttpSuccess,
superagent = require('superagent'), superagent = require('superagent'),
safe = require('safetydance'),
updateChecker = require('../updatechecker.js'); updateChecker = require('../updatechecker.js');
/** /**
@@ -44,22 +42,19 @@ function activate(req, res, next) {
if (typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be string')); if (typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be string'));
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be string')); if (typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be string'));
if (typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string')); if (typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string'));
if ('name' in req.body && typeof req.body.name !== 'string') return next(new HttpError(400, 'name must be a string'));
var username = req.body.username; var username = req.body.username;
var password = req.body.password; var password = req.body.password;
var email = req.body.email; var email = req.body.email;
var name = req.body.name || null;
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress; var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
debug('activate: username:%s ip:%s', username, ip); debug('activate: username:%s ip:%s', username, ip);
cloudron.activate(username, password, email, name, ip, function (error, info) { cloudron.activate(username, password, email, ip, function (error, info) {
if (error && error.reason === CloudronError.ALREADY_PROVISIONED) return next(new HttpError(409, 'Already setup')); if (error && error.reason === CloudronError.ALREADY_PROVISIONED) return next(new HttpError(409, 'Already setup'));
if (error && error.reason === CloudronError.BAD_USERNAME) return next(new HttpError(400, 'Bad username')); if (error && error.reason === CloudronError.BAD_USERNAME) return next(new HttpError(400, 'Bad username'));
if (error && error.reason === CloudronError.BAD_PASSWORD) return next(new HttpError(400, 'Bad password')); if (error && error.reason === CloudronError.BAD_PASSWORD) return next(new HttpError(400, 'Bad password'));
if (error && error.reason === CloudronError.BAD_EMAIL) return next(new HttpError(400, 'Bad email')); if (error && error.reason === CloudronError.BAD_EMAIL) return next(new HttpError(400, 'Bad email'));
if (error && error.reason === CloudronError.BAD_NAME) return next(new HttpError(400, 'Bad name'));
if (error) return next(new HttpError(500, error)); if (error) return next(new HttpError(500, error));
// Now let the api server know we got activated // Now let the api server know we got activated
@@ -143,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) { function feedback(req, res, next) {
assert.strictEqual(typeof req.user, 'object'); assert.strictEqual(typeof req.user, 'object');
-7
View File
@@ -137,14 +137,7 @@ function renderTemplate(res, template, data) {
assert.strictEqual(typeof template, 'string'); assert.strictEqual(typeof template, 'string');
assert.strictEqual(typeof data, 'object'); assert.strictEqual(typeof data, 'object');
settings.getCloudronName(function (error, cloudronName) {
if (error) console.error(error);
// amend details which the header expects
data.cloudronName = cloudronName || 'Cloudron';
res.render(template, data); res.render(template, data);
});
} }
function sendErrorPageOrRedirect(req, res, message) { function sendErrorPageOrRedirect(req, res, message) {
+82 -1
View File
@@ -10,7 +10,16 @@ exports = module.exports = {
setCloudronName: setCloudronName, setCloudronName: setCloudronName,
getCloudronAvatar: getCloudronAvatar, getCloudronAvatar: getCloudronAvatar,
setCloudronAvatar: setCloudronAvatar setCloudronAvatar: setCloudronAvatar,
getDnsConfig: getDnsConfig,
setDnsConfig: setDnsConfig,
getBackupConfig: getBackupConfig,
setBackupConfig: setBackupConfig,
setCertificate: setCertificate,
setAdminCertificate: setAdminCertificate
}; };
var assert = require('assert'), var assert = require('assert'),
@@ -83,3 +92,75 @@ function getCloudronAvatar(req, res, next) {
res.status(200).send(avatar); 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));
});
}
function getBackupConfig(req, res, next) {
settings.getBackupConfig(function (error, config) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, config));
});
}
function setBackupConfig(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required'));
settings.setBackupConfig(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, {}));
});
}
+100 -33
View File
@@ -16,7 +16,7 @@ var appdb = require('../../appdb.js'),
config = require('../../config.js'), config = require('../../config.js'),
constants = require('../../constants.js'), constants = require('../../constants.js'),
database = require('../../database.js'), database = require('../../database.js'),
docker = require('../../docker.js'), docker = require('../../docker.js').connection,
expect = require('expect.js'), expect = require('expect.js'),
fs = require('fs'), fs = require('fs'),
hock = require('hock'), hock = require('hock'),
@@ -25,7 +25,6 @@ var appdb = require('../../appdb.js'),
js2xml = require('js2xmlparser'), js2xml = require('js2xmlparser'),
net = require('net'), net = require('net'),
nock = require('nock'), nock = require('nock'),
os = require('os'),
paths = require('../../paths.js'), paths = require('../../paths.js'),
redis = require('redis'), redis = require('redis'),
request = require('superagent'), request = require('superagent'),
@@ -42,8 +41,8 @@ var SERVER_URL = 'http://localhost:' + config.get('port');
// Test image information // Test image information
var TEST_IMAGE_REPO = 'cloudron/test'; var TEST_IMAGE_REPO = 'cloudron/test';
var TEST_IMAGE_TAG = '8.0.0'; var TEST_IMAGE_TAG = '10.0.0';
var TEST_IMAGE_ID = '07096aedb19464ae3df950e7c1dd1a5b8b5c3835e2734876a7a0e1c4de3731c8'; var TEST_IMAGE_ID = child_process.execSync('docker inspect --format={{.Id}} ' + TEST_IMAGE_REPO + ':' + TEST_IMAGE_TAG).toString('utf8').trim();
var APP_STORE_ID = 'test', APP_ID; var APP_STORE_ID = 'test', APP_ID;
var APP_LOCATION = 'appslocation'; var APP_LOCATION = 'appslocation';
@@ -147,12 +146,17 @@ function setup(done) {
callback(null); callback(null);
}); });
}, function (callback) { },
function (callback) {
token_1 = tokendb.generateToken(); token_1 = tokendb.generateToken();
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...) // HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_1, tokendb.PREFIX_USER + USERNAME_1, 'test-client-id', Date.now() + 100000, '*', callback); tokendb.add(token_1, tokendb.PREFIX_USER + USERNAME_1, 'test-client-id', Date.now() + 100000, '*', callback);
} },
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }),
settings.setBackupConfig.bind(null, { provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' })
], done); ], done);
} }
@@ -585,10 +589,7 @@ describe('App installation', function () {
function (callback) { function (callback) {
apiHockInstance apiHockInstance
.get('/api/v1/apps/' + APP_STORE_ID + '/versions/' + APP_MANIFEST.version + '/icon') .get('/api/v1/apps/' + APP_STORE_ID + '/versions/' + APP_MANIFEST.version + '/icon')
.replyWithFile(200, path.resolve(__dirname, '../../../webadmin/src/img/appicon_fallback.png')) .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' });
var port = parseInt(url.parse(config.apiServerOrigin()).port, 10); var port = parseInt(url.parse(config.apiServerOrigin()).port, 10);
apiHockServer = http.createServer(apiHockInstance.handler).listen(port, callback); apiHockServer = http.createServer(apiHockInstance.handler).listen(port, callback);
@@ -604,8 +605,7 @@ describe('App installation', function () {
.max(Infinity) .max(Infinity)
.reply(200, js2xml('ChangeResourceRecordSetsResponse', { ChangeInfo: { Id: 'dnsrecordid', Status: 'INSYNC' } }), { 'Content-Type': 'application/xml' }); .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(5353, callback);
awsHockServer = http.createServer(awsHockInstance.handler).listen(port, callback);
} }
], done); ], done);
}); });
@@ -673,6 +673,9 @@ describe('App installation', function () {
expect(data.Config.Env).to.contain('WEBADMIN_ORIGIN=' + config.adminOrigin()); 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('API_ORIGIN=' + config.adminOrigin());
expect(data.Config.Env).to.contain('CLOUDRON=1'); 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) { clientdb.getByAppIdAndType(appResult.id, clientdb.TYPE_OAUTH, function (error, client) {
expect(error).to.not.be.ok(); expect(error).to.not.be.ok();
expect(client.id.length).to.be(40); // cid- + 32 hex chars (128 bits) + 4 hyphens expect(client.id.length).to.be(40); // cid- + 32 hex chars (128 bits) + 4 hyphens
@@ -758,10 +761,7 @@ describe('App installation', function () {
expect(urlp.hostname).to.be('redis-' + APP_ID); expect(urlp.hostname).to.be('redis-' + APP_ID);
var isMac = os.platform() === 'darwin'; var client = redis.createClient(parseInt(urlp.port, 10), redisIp, { auth_pass: password });
var client =
isMac ? redis.createClient(parseInt(exportedRedisPort, 10), '127.0.0.1', { auth_pass: password })
: redis.createClient(parseInt(urlp.port, 10), redisIp, { auth_pass: password });
client.on('error', done); client.on('error', done);
client.set('key', 'value'); client.set('key', 'value');
client.get('key', function (err, reply) { client.get('key', function (err, reply) {
@@ -833,7 +833,15 @@ describe('App installation', function () {
}); });
}); });
it('logs - stdout and stderr', function (done) { it('installation - scheduler', function (done) {
async.retry({ times: 100, interval: 1000 }, function (retryCallback) {
if (fs.existsSync(paths.DATA_DIR + '/' + APP_ID + '/data/every_minute.env')) return retryCallback();
retryCallback(new Error('not run yet'));
}, done);
});
xit('logs - stdout and stderr', function (done) {
request.get(SERVER_URL + '/api/v1/apps/' + APP_ID + '/logs') request.get(SERVER_URL + '/api/v1/apps/' + APP_ID + '/logs')
.query({ access_token: token }) .query({ access_token: token })
.end(function (err, res) { .end(function (err, res) {
@@ -847,7 +855,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') request.get(SERVER_URL + '/api/v1/apps/' + APP_ID + '/logstream')
.query({ access_token: token, fromLine: 0 }) .query({ access_token: token, fromLine: 0 })
.end(function (err, res) { .end(function (err, res) {
@@ -857,7 +865,7 @@ describe('App installation', function () {
}); });
it('logStream - stream logs', function (done) { xit('logStream - stream logs', function (done) {
var options = { var options = {
port: config.get('port'), host: 'localhost', path: '/api/v1/apps/' + APP_ID + '/logstream?access_token=' + token, port: config.get('port'), host: 'localhost', path: '/api/v1/apps/' + APP_ID + '/logstream?access_token=' + token,
headers: { 'Accept': 'text/event-stream', 'Connection': 'keep-alive' } headers: { 'Accept': 'text/event-stream', 'Connection': 'keep-alive' }
@@ -1016,8 +1024,15 @@ describe('App installation - port bindings', function () {
var awsHockInstance = hock.createHock({ throwOnUnmatched: false }), awsHockServer; var awsHockInstance = hock.createHock({ throwOnUnmatched: false }), awsHockServer;
var imageDeleted = false, imageCreated = false; 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) { before(function (done) {
config.set('fqdn', 'test.foobar.com');
APP_ID = uuid.v4(); APP_ID = uuid.v4();
async.series([ async.series([
function (callback) { function (callback) {
dockerProxy = startDockerProxy(function interceptor(req, res) { dockerProxy = startDockerProxy(function interceptor(req, res) {
@@ -1041,15 +1056,14 @@ describe('App installation - port bindings', function () {
function (callback) { function (callback) {
apiHockInstance apiHockInstance
.get('/api/v1/apps/' + APP_STORE_ID + '/versions/' + APP_MANIFEST.version + '/icon') .get('/api/v1/apps/' + APP_STORE_ID + '/versions/' + APP_MANIFEST.version + '/icon')
.replyWithFile(200, path.resolve(__dirname, '../../../webadmin/src/img/appicon_fallback.png')) .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' });
var port = parseInt(url.parse(config.apiServerOrigin()).port, 10); var port = parseInt(url.parse(config.apiServerOrigin()).port, 10);
apiHockServer = http.createServer(apiHockInstance.handler).listen(port, callback); apiHockServer = http.createServer(apiHockInstance.handler).listen(port, callback);
}, },
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }),
function (callback) { function (callback) {
awsHockInstance awsHockInstance
.get('/2013-04-01/hostedzone') .get('/2013-04-01/hostedzone')
@@ -1060,8 +1074,7 @@ describe('App installation - port bindings', function () {
.max(Infinity) .max(Infinity)
.reply(200, js2xml('ChangeResourceRecordSetsResponse', { ChangeInfo: { Id: 'dnsrecordid', Status: 'INSYNC' } }), { 'Content-Type': 'application/xml' }); .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(5353, callback);
awsHockServer = http.createServer(awsHockInstance.handler).listen(port, callback);
} }
], done); ], done);
}); });
@@ -1221,10 +1234,7 @@ describe('App installation - port bindings', function () {
expect(data.Config.Env).to.contain('REDIS_PASSWORD=' + password); expect(data.Config.Env).to.contain('REDIS_PASSWORD=' + password);
function checkRedis() { function checkRedis() {
var isMac = os.platform() === 'darwin'; var client = redis.createClient(parseInt(urlp.port, 10), redisIp, { auth_pass: password });
var client =
isMac ? redis.createClient(parseInt(exportedRedisPort, 10), '127.0.0.1', { auth_pass: password })
: redis.createClient(parseInt(urlp.port, 10), redisIp, { auth_pass: password });
client.on('error', done); client.on('error', done);
client.set('key', 'value'); client.set('key', 'value');
client.get('key', function (err, reply) { client.get('key', function (err, reply) {
@@ -1284,6 +1294,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) { it('non admin cannot reconfigure app', function (done) {
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure') request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token_1 }) .query({ access_token: token_1 })
@@ -1356,10 +1406,7 @@ describe('App installation - port bindings', function () {
expect(data.Config.Env).to.contain('REDIS_HOST=redis-' + APP_ID); expect(data.Config.Env).to.contain('REDIS_HOST=redis-' + APP_ID);
expect(data.Config.Env).to.contain('REDIS_PASSWORD=' + password); expect(data.Config.Env).to.contain('REDIS_PASSWORD=' + password);
var isMac = os.platform() === 'darwin'; var client = redis.createClient(parseInt(urlp.port, 10), redisIp, { auth_pass: password });
var client =
isMac ? redis.createClient(parseInt(exportedRedisPort, 10), '127.0.0.1', { auth_pass: password })
: redis.createClient(parseInt(urlp.port, 10), redisIp, { auth_pass: password });
client.on('error', done); client.on('error', done);
client.set('key', 'value'); client.set('key', 'value');
client.get('key', function (err, reply) { client.get('key', function (err, reply) {
@@ -1371,6 +1418,26 @@ describe('App installation - port bindings', function () {
}); });
}); });
it('scheduler works after reconfiguration', function (done) {
async.retry({ times: 100, interval: 1000 }, function (callback) {
var data = safe.fs.readFileSync(paths.DATA_DIR + '/' + APP_ID + '/data/every_minute.env', 'utf8');
if (data && data.indexOf('ECHO_SERVER_PORT=7172') !== -1) return callback();
callback(new Error('not run yet'));
}, done);
});
it('can 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) { it('can stop app', function (done) {
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/stop') request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/stop')
.query({ access_token: token }) .query({ access_token: token })
+8 -4
View File
@@ -13,6 +13,7 @@ var appdb = require('../../appdb.js'),
expect = require('expect.js'), expect = require('expect.js'),
request = require('superagent'), request = require('superagent'),
server = require('../../server.js'), server = require('../../server.js'),
settings = require('../../settings.js'),
nock = require('nock'), nock = require('nock'),
userdb = require('../../userdb.js'); userdb = require('../../userdb.js');
@@ -52,6 +53,10 @@ function setup(done) {
function addApp(callback) { function addApp(callback) {
var manifest = { version: '0.0.1', manifestVersion: 1, dockerImage: 'foo', healthCheckPath: '/', httpPort: 3, title: 'ok', addons: { } }; var manifest = { version: '0.0.1', manifestVersion: 1, dockerImage: 'foo', healthCheckPath: '/', httpPort: 3, title: 'ok', addons: { } };
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, null /* accessRestriction */, false /* oauthProxy */, callback); appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, null /* accessRestriction */, false /* oauthProxy */, callback);
},
function createSettings(callback) {
settings.setBackupConfig({ provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' }, callback);
} }
], done); ], done);
} }
@@ -70,7 +75,7 @@ describe('Backups API', function () {
describe('get', function () { describe('get', function () {
it('cannot get backups with appstore request failing', function (done) { it('cannot get backups with appstore request failing', function (done) {
var req = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/backups?token=APPSTORE_TOKEN').reply(401, {}); var req = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/backups?token=BACKUP_TOKEN').reply(401, {});
request.get(SERVER_URL + '/api/v1/backups') request.get(SERVER_URL + '/api/v1/backups')
.query({ access_token: token }) .query({ access_token: token })
@@ -82,7 +87,7 @@ describe('Backups API', function () {
}); });
it('can get backups', function (done) { it('can get backups', function (done) {
var req = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/backups?token=APPSTORE_TOKEN').reply(200, { backups: ['foo', 'bar']}); var req = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/backups?token=BACKUP_TOKEN').reply(200, { backups: ['foo', 'bar']});
request.get(SERVER_URL + '/api/v1/backups') request.get(SERVER_URL + '/api/v1/backups')
.query({ access_token: token }) .query({ access_token: token })
@@ -119,7 +124,7 @@ describe('Backups API', function () {
it('succeeds', function (done) { it('succeeds', function (done) {
var scope = nock(config.apiServerOrigin()) var scope = nock(config.apiServerOrigin())
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=APPSTORE_TOKEN') .post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN')
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } }); .reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
request.post(SERVER_URL + '/api/v1/backups') request.post(SERVER_URL + '/api/v1/backups')
@@ -141,4 +146,3 @@ describe('Backups API', function () {
}); });
}); });
}); });
+24 -109
View File
@@ -10,11 +10,7 @@ var async = require('async'),
config = require('../../config.js'), config = require('../../config.js'),
database = require('../../database.js'), database = require('../../database.js'),
expect = require('expect.js'), expect = require('expect.js'),
fs = require('fs'),
os = require('os'),
path = require('path'),
nock = require('nock'), nock = require('nock'),
paths = require('../../paths.js'),
request = require('superagent'), request = require('superagent'),
server = require('../../server.js'), server = require('../../server.js'),
shell = require('../../shell.js'); shell = require('../../shell.js');
@@ -28,6 +24,7 @@ var server;
function setup(done) { function setup(done) {
nock.cleanAll(); nock.cleanAll();
config.set('version', '0.5.0'); config.set('version', '0.5.0');
config.set('fqdn', 'localhost');
server.start(done); 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 () { describe('get config', function () {
before(function (done) { before(function (done) {
async.series([ async.series([
@@ -310,14 +212,15 @@ describe('Cloudron', function () {
expect(result.statusCode).to.equal(200); expect(result.statusCode).to.equal(200);
expect(result.body.apiServerOrigin).to.eql('http://localhost:6060'); expect(result.body.apiServerOrigin).to.eql('http://localhost:6060');
expect(result.body.webServerOrigin).to.eql(null); 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.isCustomDomain).to.eql(false);
expect(result.body.progress).to.be.an('object'); expect(result.body.progress).to.be.an('object');
expect(result.body.update).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.developerMode).to.be.a('boolean');
expect(result.body.size).to.eql(null); expect(result.body.size).to.eql(null);
expect(result.body.region).to.eql(null); expect(result.body.region).to.eql(null);
expect(result.body.memory).to.eql(0);
expect(result.body.cloudronName).to.be.a('string'); expect(result.body.cloudronName).to.be.a('string');
done(); done();
@@ -325,7 +228,7 @@ describe('Cloudron', function () {
}); });
it('succeeds', function (done) { it('succeeds', function (done) {
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/localhost?token=' + config.token()).reply(200, { box: { region: 'sfo', size: 'small' }}); var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/localhost?token=' + config.token()).reply(200, { box: { region: 'sfo', size: '1gb' }});
request.get(SERVER_URL + '/api/v1/cloudron/config') request.get(SERVER_URL + '/api/v1/cloudron/config')
.query({ access_token: token }) .query({ access_token: token })
@@ -334,14 +237,15 @@ describe('Cloudron', function () {
expect(result.statusCode).to.equal(200); expect(result.statusCode).to.equal(200);
expect(result.body.apiServerOrigin).to.eql('http://localhost:6060'); expect(result.body.apiServerOrigin).to.eql('http://localhost:6060');
expect(result.body.webServerOrigin).to.eql(null); 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.isCustomDomain).to.eql(false);
expect(result.body.progress).to.be.an('object'); expect(result.body.progress).to.be.an('object');
expect(result.body.update).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.developerMode).to.be.a('boolean');
expect(result.body.size).to.eql('small'); expect(result.body.size).to.eql('1gb');
expect(result.body.region).to.eql('sfo'); expect(result.body.region).to.eql('sfo');
expect(result.body.memory).to.eql(1073741824);
expect(result.body.cloudronName).to.be.a('string'); expect(result.body.cloudronName).to.be.a('string');
expect(scope.isDone()).to.be.ok(); expect(scope.isDone()).to.be.ok();
@@ -378,6 +282,19 @@ describe('Cloudron', function () {
callback(); callback();
}); });
}, },
function setupBackupConfig(callback) {
request.post(SERVER_URL + '/api/v1/settings/backup_config')
.send({ provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' })
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(200);
callback();
});
}
], done); ], done);
}); });
@@ -437,7 +354,6 @@ describe('Cloudron', function () {
}); });
}); });
it('fails with wrong region type', function (done) { it('fails with wrong region type', function (done) {
request.post(SERVER_URL + '/api/v1/cloudron/migrate') request.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 4, password: PASSWORD }) .send({ size: 'small', region: 4, password: PASSWORD })
@@ -451,7 +367,7 @@ describe('Cloudron', function () {
it('fails when in wrong state', function (done) { it('fails when in wrong state', function (done) {
var scope2 = nock(config.apiServerOrigin()) var scope2 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=APPSTORE_TOKEN') .post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN')
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } }); .reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
var scope3 = nock(config.apiServerOrigin()) var scope3 = nock(config.apiServerOrigin())
@@ -487,7 +403,6 @@ describe('Cloudron', function () {
}); });
}); });
it('succeeds', function (done) { it('succeeds', function (done) {
var scope1 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/migrate?token=APPSTORE_TOKEN', function (body) { var scope1 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/migrate?token=APPSTORE_TOKEN', function (body) {
return body.size && body.region && body.restoreKey; return body.size && body.region && body.restoreKey;
@@ -500,7 +415,7 @@ describe('Cloudron', function () {
.reply(200, { id: 'someid' }); .reply(200, { id: 'someid' });
var scope3 = nock(config.apiServerOrigin()) var scope3 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=APPSTORE_TOKEN') .post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN')
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } }); .reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
injectShellMock(); injectShellMock();
+3 -3
View File
@@ -151,7 +151,7 @@ describe('OAuth2', function () {
var APP_0 = { var APP_0 = {
id: 'app0', id: 'app0',
appStoreId: '', appStoreId: '',
manifest: { version: '0.1.0' }, manifest: { version: '0.1.0', addons: { } },
location: 'test', location: 'test',
portBindings: {}, portBindings: {},
accessRestriction: null, accessRestriction: null,
@@ -161,7 +161,7 @@ describe('OAuth2', function () {
var APP_1 = { var APP_1 = {
id: 'app1', id: 'app1',
appStoreId: '', appStoreId: '',
manifest: { version: '0.1.0' }, manifest: { version: '0.1.0', addons: { } },
location: 'test1', location: 'test1',
portBindings: {}, portBindings: {},
accessRestriction: { users: [ 'foobar' ] }, accessRestriction: { users: [ 'foobar' ] },
@@ -171,7 +171,7 @@ describe('OAuth2', function () {
var APP_2 = { var APP_2 = {
id: 'app2', id: 'app2',
appStoreId: '', appStoreId: '',
manifest: { version: '0.1.0' }, manifest: { version: '0.1.0', addons: { } },
location: 'test2', location: 'test2',
portBindings: {}, portBindings: {},
accessRestriction: { users: [ USER_0.id ] }, accessRestriction: { users: [ USER_0.id ] },
+141 -2
View File
@@ -11,6 +11,7 @@ var appdb = require('../../appdb.js'),
config = require('../../config.js'), config = require('../../config.js'),
database = require('../../database.js'), database = require('../../database.js'),
expect = require('expect.js'), expect = require('expect.js'),
path = require('path'),
paths = require('../../paths.js'), paths = require('../../paths.js'),
request = require('superagent'), request = require('superagent'),
server = require('../../server.js'), server = require('../../server.js'),
@@ -26,6 +27,8 @@ var token = null;
var server; var server;
function setup(done) { function setup(done) {
config.set('fqdn', 'foobar.com');
async.series([ async.series([
server.start.bind(server), server.start.bind(server),
@@ -212,7 +215,7 @@ describe('Settings API', function () {
it('set succeeds', function (done) { it('set succeeds', function (done) {
request.post(SERVER_URL + '/api/v1/settings/cloudron_avatar') request.post(SERVER_URL + '/api/v1/settings/cloudron_avatar')
.query({ access_token: token }) .query({ access_token: token })
.attach('avatar', paths.FAVICON_FILE) .attach('avatar', paths.CLOUDRON_DEFAULT_AVATAR_FILE)
.end(function (err, res) { .end(function (err, res) {
expect(res.statusCode).to.equal(202); expect(res.statusCode).to.equal(202);
done(); done();
@@ -224,10 +227,146 @@ describe('Settings API', function () {
.query({ access_token: token }) .query({ access_token: token })
.end(function (err, res) { .end(function (err, res) {
expect(res.statusCode).to.equal(200); 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); 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 -3
View File
@@ -26,7 +26,7 @@ describe('SimpleAuth API', function () {
var APP_0 = { var APP_0 = {
id: 'app0', id: 'app0',
appStoreId: '', appStoreId: '',
manifest: { version: '0.1.0' }, manifest: { version: '0.1.0', addons: { } },
location: 'test0', location: 'test0',
portBindings: {}, portBindings: {},
accessRestriction: { users: [ 'foobar', 'someone'] }, accessRestriction: { users: [ 'foobar', 'someone'] },
@@ -36,7 +36,7 @@ describe('SimpleAuth API', function () {
var APP_1 = { var APP_1 = {
id: 'app1', id: 'app1',
appStoreId: '', appStoreId: '',
manifest: { version: '0.1.0' }, manifest: { version: '0.1.0', addons: { } },
location: 'test1', location: 'test1',
portBindings: {}, portBindings: {},
accessRestriction: { users: [ 'foobar', USERNAME, 'someone' ] }, accessRestriction: { users: [ 'foobar', USERNAME, 'someone' ] },
@@ -46,7 +46,7 @@ describe('SimpleAuth API', function () {
var APP_2 = { var APP_2 = {
id: 'app2', id: 'app2',
appStoreId: '', appStoreId: '',
manifest: { version: '0.1.0' }, manifest: { version: '0.1.0', addons: { } },
location: 'test2', location: 'test2',
portBindings: {}, portBindings: {},
accessRestriction: null, accessRestriction: null,
-2
View File
@@ -66,8 +66,6 @@ start_mongodb() {
} }
start_mail() { start_mail() {
local mongodb_vars="MONGODB_ROOT_PASSWORD=${root_password}"
docker rm -f mail 2>/dev/null 1>&2 || true docker rm -f mail 2>/dev/null 1>&2 || true
docker run -dP --name=mail -e DOMAIN_NAME="localhost" \ docker run -dP --name=mail -e DOMAIN_NAME="localhost" \
+193
View File
@@ -0,0 +1,193 @@
'use strict';
exports = module.exports = {
sync: sync
};
var appdb = require('./appdb.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
config = require('./config.js'),
CronJob = require('cron').CronJob,
debug = require('debug')('box:src/scheduler'),
docker = require('./docker.js'),
paths = require('./paths.js'),
safe = require('safetydance'),
_ = require('underscore');
var NOOP_CALLBACK = function (error) { if (error) debug('Unhandled error: ', error); };
// appId -> { schedulerConfig (manifest), cronjobs, containerIds }
var gState = (function loadState() {
var state = safe.JSON.parse(safe.fs.readFileSync(paths.SCHEDULER_FILE, 'utf8'));
return state || { };
})();
function saveState(state) {
// do not save cronJobs
var safeState = { };
for (var appId in state) {
safeState[appId] = {
schedulerConfig: state[appId].schedulerConfig,
containerIds: state[appId].containerIds
};
}
safe.fs.writeFileSync(paths.SCHEDULER_FILE, JSON.stringify(safeState, null, 4), 'utf8');
}
function sync(callback) {
assert(!callback || typeof callback === 'function');
callback = callback || NOOP_CALLBACK;
debug('Syncing');
apps.getAll(function (error, allApps) {
if (error) return callback(error);
// stop tasks of apps that went away
var allAppIds = allApps.map(function (app) { return app.id; });
var removedAppIds = _.difference(Object.keys(gState), allAppIds);
async.eachSeries(removedAppIds, function (appId, iteratorDone) {
stopJobs(appId, gState[appId], true /* killContainers */, iteratorDone);
}, function (error) {
if (error) debug('Error stopping jobs : %j', error);
gState = _.omit(gState, removedAppIds);
// start tasks of new apps
async.eachSeries(allApps, function (app, iteratorDone) {
var appState = gState[app.id] || null;
var schedulerConfig = app.manifest.addons.scheduler || null;
if (!appState && !schedulerConfig) return iteratorDone(); // nothing changed
if (appState && _.isEqual(appState.schedulerConfig, schedulerConfig) && appState.cronJobs) {
return iteratorDone(); // nothing changed
}
var killContainers = appState && !appState.cronJobs ? true : false; // keep the old containers on 'startup'
stopJobs(app.id, appState, killContainers, function (error) {
if (error) debug('Error stopping jobs for %s : %s', app.id, error.message);
if (!schedulerConfig) {
delete gState[app.id];
return iteratorDone();
}
gState[app.id] = {
schedulerConfig: schedulerConfig,
cronJobs: createCronJobs(app.id, schedulerConfig),
containerIds: { }
};
saveState(gState);
iteratorDone();
});
});
debug('Done syncing');
});
});
}
function killContainer(containerId, callback) {
if (!containerId) return callback();
async.series([
docker.stopContainer.bind(null, containerId),
docker.deleteContainer.bind(null, containerId)
], function (error) {
if (error) debug('Failed to kill task with containerId %s : %s', containerId, error.message);
callback(error);
});
}
function stopJobs(appId, appState, killContainers, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof appState, 'object');
assert.strictEqual(typeof killContainers, 'boolean');
assert.strictEqual(typeof callback, 'function');
debug('stopJobs for %s', appId);
if (!appState) return callback();
async.eachSeries(Object.keys(appState.schedulerConfig), function (taskName, iteratorDone) {
if (appState.cronJobs && appState.cronJobs[taskName]) { // could be null across restarts
appState.cronJobs[taskName].stop();
}
if (!killContainers) return iteratorDone();
killContainer(appState.containerIds[taskName], iteratorDone);
}, callback);
}
function createCronJobs(appId, schedulerConfig) {
assert.strictEqual(typeof appId, 'string');
assert(schedulerConfig && typeof schedulerConfig === 'object');
debug('creating cron jobs for app %s', appId);
var jobs = { };
Object.keys(schedulerConfig).forEach(function (taskName) {
var task = schedulerConfig[taskName];
var cronTime = (config.TEST ? '*/5 ' : '00 ') + task.schedule; // time ticks faster in tests
debug('scheduling task for %s/%s @ %s : %s', appId, taskName, cronTime, task.command);
var cronJob = new CronJob({
cronTime: cronTime, // at this point, the pattern has been validated
onTick: doTask.bind(null, appId, taskName),
start: true
});
jobs[taskName] = cronJob;
});
return jobs;
}
function doTask(appId, taskName, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof taskName, 'string');
assert(!callback || typeof callback === 'function');
callback = callback || NOOP_CALLBACK;
var appState = gState[appId];
debug('Executing task %s/%s', appId, taskName);
apps.get(appId, function (error, app) {
if (error) return callback(error);
if (app.installationState !== appdb.ISTATE_INSTALLED || app.runState !== appdb.RSTATE_RUNNING) {
debug('task %s skipped. app %s is not installed/running', taskName, app.id);
return callback();
}
if (appState.containerIds[taskName]) debug('task %s/%s has existing container %s. killing it', appId, taskName, appState.containerIds[taskName]);
killContainer(appState.containerIds[taskName], function (error) {
if (error) return callback(error);
debug('Creating createSubcontainer for %s/%s : %s', app.id, taskName, gState[appId].schedulerConfig[taskName].command);
// 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 ], { } /* options */, function (error, container) {
appState.containerIds[taskName] = container.id;
saveState(gState);
docker.startContainer(container.id, callback);
});
});
});
}
+7 -4
View File
@@ -20,7 +20,6 @@ var assert = require('assert'),
middleware = require('./middleware'), middleware = require('./middleware'),
passport = require('passport'), passport = require('passport'),
path = require('path'), path = require('path'),
paths = require('./paths.js'),
routes = require('./routes/index.js'), routes = require('./routes/index.js'),
taskmanager = require('./taskmanager.js'); taskmanager = require('./taskmanager.js');
@@ -53,7 +52,6 @@ function initializeExpressSync() {
.use(json) .use(json)
.use(urlencoded) .use(urlencoded)
.use(middleware.cookieParser()) .use(middleware.cookieParser())
.use(middleware.favicon(paths.FAVICON_FILE)) // used when serving oauth login page
.use(middleware.cors({ origins: [ '*' ], allowCredentials: true })) .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(middleware.session({ secret: 'yellow is blue', resave: true, saveUninitialized: true, cookie: { path: '/', httpOnly: true, secure: false, maxAge: 600000 } }))
.use(passport.initialize()) .use(passport.initialize())
@@ -95,7 +93,6 @@ function initializeExpressSync() {
router.post('/api/v1/cloudron/update', rootScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.cloudron.update); 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/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/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); router.get ('/api/v1/cloudron/graphs', rootScope, routes.graphs.getGraphs);
// feedback // feedback
@@ -161,6 +158,12 @@ function initializeExpressSync() {
router.post('/api/v1/settings/cloudron_name', settingsScope, routes.settings.setCloudronName); router.post('/api/v1/settings/cloudron_name', settingsScope, routes.settings.setCloudronName);
router.get ('/api/v1/settings/cloudron_avatar', settingsScope, routes.settings.getCloudronAvatar); router.get ('/api/v1/settings/cloudron_avatar', settingsScope, routes.settings.getCloudronAvatar);
router.post('/api/v1/settings/cloudron_avatar', settingsScope, multipart, routes.settings.setCloudronAvatar); router.post('/api/v1/settings/cloudron_avatar', settingsScope, multipart, routes.settings.setCloudronAvatar);
router.get ('/api/v1/settings/dns_config', settingsScope, routes.settings.getDnsConfig);
router.post('/api/v1/settings/dns_config', settingsScope, routes.settings.setDnsConfig);
router.get ('/api/v1/settings/backup_config', settingsScope, routes.settings.getBackupConfig);
router.post('/api/v1/settings/backup_config', settingsScope, routes.settings.setBackupConfig);
router.post('/api/v1/settings/certificate', settingsScope, routes.settings.setCertificate);
router.post('/api/v1/settings/admin_certificate', settingsScope, routes.settings.setAdminCertificate);
// backup routes // backup routes
router.get ('/api/v1/backups', settingsScope, routes.backups.get); router.get ('/api/v1/backups', settingsScope, routes.backups.get);
@@ -231,8 +234,8 @@ function start(callback) {
async.series([ async.series([
auth.initialize, auth.initialize,
database.initialize, database.initialize,
cloudron.initialize, // keep this here because it reads activation state that others depend on
taskmanager.initialize, taskmanager.initialize,
cloudron.initialize,
mailer.initialize, mailer.initialize,
cron.initialize, cron.initialize,
gHttpServer.listen.bind(gHttpServer, config.get('port'), '127.0.0.1'), gHttpServer.listen.bind(gHttpServer, config.get('port'), '127.0.0.1'),
+198
View File
@@ -20,25 +20,43 @@ exports = module.exports = {
getDeveloperMode: getDeveloperMode, getDeveloperMode: getDeveloperMode,
setDeveloperMode: setDeveloperMode, setDeveloperMode: setDeveloperMode,
getDnsConfig: getDnsConfig,
setDnsConfig: setDnsConfig,
getBackupConfig: getBackupConfig,
setBackupConfig: setBackupConfig,
getDefaultSync: getDefaultSync, getDefaultSync: getDefaultSync,
getAll: getAll, getAll: getAll,
validateCertificate: validateCertificate,
setCertificate: setCertificate,
setAdminCertificate: setAdminCertificate,
AUTOUPDATE_PATTERN_KEY: 'autoupdate_pattern', AUTOUPDATE_PATTERN_KEY: 'autoupdate_pattern',
TIME_ZONE_KEY: 'time_zone', TIME_ZONE_KEY: 'time_zone',
CLOUDRON_NAME_KEY: 'cloudron_name', CLOUDRON_NAME_KEY: 'cloudron_name',
DEVELOPER_MODE_KEY: 'developer_mode', DEVELOPER_MODE_KEY: 'developer_mode',
DNS_CONFIG_KEY: 'dns_config',
BACKUP_CONFIG_KEY: 'backup_config',
events: new (require('events').EventEmitter)() events: new (require('events').EventEmitter)()
}; };
var assert = require('assert'), var assert = require('assert'),
config = require('./config.js'), config = require('./config.js'),
constants = require('./constants.js'),
CronJob = require('cron').CronJob, CronJob = require('cron').CronJob,
DatabaseError = require('./databaseerror.js'), DatabaseError = require('./databaseerror.js'),
ejs = require('ejs'),
fs = require('fs'),
path = require('path'),
paths = require('./paths.js'), paths = require('./paths.js'),
safe = require('safetydance'), safe = require('safetydance'),
settingsdb = require('./settingsdb.js'), settingsdb = require('./settingsdb.js'),
shell = require('./shell.js'),
util = require('util'), util = require('util'),
x509 = require('x509'),
_ = require('underscore'); _ = require('underscore');
var gDefaults = (function () { var gDefaults = (function () {
@@ -47,10 +65,15 @@ var gDefaults = (function () {
result[exports.TIME_ZONE_KEY] = 'America/Los_Angeles'; result[exports.TIME_ZONE_KEY] = 'America/Los_Angeles';
result[exports.CLOUDRON_NAME_KEY] = 'Cloudron'; result[exports.CLOUDRON_NAME_KEY] = 'Cloudron';
result[exports.DEVELOPER_MODE_KEY] = false; result[exports.DEVELOPER_MODE_KEY] = false;
result[exports.DNS_CONFIG_KEY] = { };
result[exports.BACKUP_CONFIG_KEY] = { };
return result; 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) { if (config.TEST) {
// avoid noisy warnings during npm test // avoid noisy warnings during npm test
exports.events.setMaxListeners(100); exports.events.setMaxListeners(100);
@@ -78,6 +101,7 @@ util.inherits(SettingsError, Error);
SettingsError.INTERNAL_ERROR = 'Internal Error'; SettingsError.INTERNAL_ERROR = 'Internal Error';
SettingsError.NOT_FOUND = 'Not Found'; SettingsError.NOT_FOUND = 'Not Found';
SettingsError.BAD_FIELD = 'Bad Field'; SettingsError.BAD_FIELD = 'Bad Field';
SettingsError.INVALID_CERT = 'Invalid certificate';
function setAutoupdatePattern(pattern, callback) { function setAutoupdatePattern(pattern, callback) {
assert.strictEqual(typeof pattern, 'string'); assert.strictEqual(typeof pattern, 'string');
@@ -207,6 +231,79 @@ 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 getBackupConfig(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.BACKUP_CONFIG_KEY, function (error, value) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.BACKUP_CONFIG_KEY]);
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
callback(null, JSON.parse(value)); // provider, token, key, region, prefix, bucket
});
}
function setBackupConfig(backupConfig, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof callback, 'function');
if (backupConfig.provider !== 'caas') {
return callback(new SettingsError(SettingsError.BAD_FIELD, 'provider must be caas'));
}
settingsdb.set(exports.BACKUP_CONFIG_KEY, JSON.stringify(backupConfig), function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
exports.events.emit(exports.BACKUP_CONFIG_KEY, backupConfig);
callback(null);
});
}
function getDefaultSync(name) { function getDefaultSync(name) {
assert.strictEqual(typeof name, 'string'); assert.strictEqual(typeof name, 'string');
@@ -225,3 +322,104 @@ function getAll(callback) {
callback(null, result); 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));
// backup the cert
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, 'host.cert'), cert)) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, 'host.key'), key)) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, safe.error.message));
// copy over fallback cert
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));
// backup the cert
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);
});
}
+129
View File
@@ -0,0 +1,129 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
getSignedUploadUrl: getSignedUploadUrl,
getSignedDownloadUrl: getSignedDownloadUrl,
copyObject: copyObject,
getAllPaged: getAllPaged
};
var assert = require('assert'),
AWS = require('aws-sdk'),
config = require('../config.js'),
superagent = require('superagent'),
util = require('util');
function getBackupCredentials(backupConfig, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof callback, 'function');
assert(backupConfig.token);
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/awscredentials';
superagent.post(url).query({ token: backupConfig.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 (backupConfig.endpoint) credentials.endpoint = new AWS.Endpoint(backupConfig.endpoint);
callback(null, credentials);
});
}
function getAllPaged(backupConfig, page, perPage, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/backups';
superagent.get(url).query({ token: backupConfig.token }).end(function (error, result) {
if (error) return callback(error);
if (result.statusCode !== 200) return callback(new Error(result.text));
if (!result.body || !util.isArray(result.body.backups)) return callback(new Error('Unexpected response'));
// [ { creationTime, boxVersion, restoreKey, dependsOn: [ ] } ] sorted by time (latest first)
return callback(null, result.body.backups);
});
}
function getSignedUploadUrl(backupConfig, filename, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof filename, 'string');
assert.strictEqual(typeof callback, 'function');
if (!backupConfig.bucket || !backupConfig.prefix) return new Error('Invalid configuration'); // prevent error in s3
getBackupCredentials(backupConfig, function (error, credentials) {
if (error) return callback(error);
var s3 = new AWS.S3(credentials);
var params = {
Bucket: backupConfig.bucket,
Key: backupConfig.prefix + '/' + filename,
Expires: 60 * 30 /* 30 minutes */
};
var url = s3.getSignedUrl('putObject', params);
callback(null, { url : url, sessionToken: credentials.sessionToken });
});
}
function getSignedDownloadUrl(backupConfig, filename, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof filename, 'string');
assert.strictEqual(typeof callback, 'function');
if (!backupConfig.bucket || !backupConfig.prefix) return new Error('Invalid configuration'); // prevent error in s3
getBackupCredentials(backupConfig, function (error, credentials) {
if (error) return callback(error);
var s3 = new AWS.S3(credentials);
var params = {
Bucket: backupConfig.bucket,
Key: backupConfig.prefix + '/' + filename,
Expires: 60 * 30 /* 30 minutes */
};
var url = s3.getSignedUrl('getObject', params);
callback(null, { url: url, sessionToken: credentials.sessionToken });
});
}
function copyObject(backupConfig, from, to, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof from, 'string');
assert.strictEqual(typeof to, 'string');
assert.strictEqual(typeof callback, 'function');
if (!backupConfig.bucket || !backupConfig.prefix) return new Error('Invalid configuration'); // prevent error in s3
getBackupCredentials(backupConfig, function (error, credentials) {
if (error) return callback(error);
var params = {
Bucket: backupConfig.bucket, // target bucket
Key: backupConfig.prefix + '/' + to, // target file
CopySource: backupConfig.bucket + '/' + backupConfig.prefix + '/' + from, // source
};
var s3 = new AWS.S3(credentials);
s3.copyObject(params, callback);
});
}
+104
View File
@@ -0,0 +1,104 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
getSignedUploadUrl: getSignedUploadUrl,
getSignedDownloadUrl: getSignedDownloadUrl,
copyObject: copyObject,
getAllPaged: getAllPaged
};
var assert = require('assert'),
AWS = require('aws-sdk');
function getBackupCredentials(backupConfig, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof callback, 'function');
assert(backupConfig.accessKeyId && backupConfig.secretAccessKey);
var credentials = {
accessKeyId: backupConfig.accessKeyId,
secretAccessKey: backupConfig.secretAccessKey,
region: 'us-east-1'
};
if (backupConfig.endpoint) credentials.endpoint = new AWS.Endpoint(backupConfig.endpoint);
callback(null, credentials);
}
function getAllPaged(backupConfig, page, perPage, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
return callback(new Error('Not implemented yet'));
}
function getSignedUploadUrl(backupConfig, filename, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof filename, 'string');
assert.strictEqual(typeof callback, 'function');
getBackupCredentials(backupConfig, function (error, credentials) {
if (error) return callback(error);
var s3 = new AWS.S3(credentials);
var params = {
Bucket: backupConfig.bucket,
Key: backupConfig.prefix + '/' + filename,
Expires: 60 * 30 /* 30 minutes */
};
var url = s3.getSignedUrl('putObject', params);
callback(null, { url : url, sessionToken: credentials.sessionToken });
});
}
function getSignedDownloadUrl(backupConfig, filename, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof filename, 'string');
assert.strictEqual(typeof callback, 'function');
getBackupCredentials(backupConfig, function (error, credentials) {
if (error) return callback(error);
var s3 = new AWS.S3(credentials);
var params = {
Bucket: backupConfig.bucket,
Key: backupConfig.prefix + '/' + filename,
Expires: 60 * 30 /* 30 minutes */
};
var url = s3.getSignedUrl('getObject', params);
callback(null, { url: url, sessionToken: credentials.sessionToken });
});
}
function copyObject(backupConfig, from, to, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof from, 'string');
assert.strictEqual(typeof to, 'string');
assert.strictEqual(typeof callback, 'function');
getBackupCredentials(backupConfig, function (error, credentials) {
if (error) return callback(error);
var params = {
Bucket: backupConfig.bucket, // target bucket
Key: backupConfig.prefix + '/' + to, // target file
CopySource: backupConfig.bucket + '/' + backupConfig.prefix + '/' + 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';
+90 -40
View File
@@ -2,86 +2,136 @@
'use strict'; '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 = { module.exports = exports = {
add: add, add: add,
addMany: addMany,
remove: remove, 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 var assert = require('assert'),
// for test purpose we use aws caas = require('./dns/caas.js'),
function api() { config = require('./config.js'),
return config.token() && !config.TEST ? caas : aws; route53 = require('./dns/route53.js'),
settings = require('./settings.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';
SubdomainError.INTERNAL_ERROR = 'Missing credentials';
// choose which subdomain backend we use for test purpose we use route53
function api(provider) {
assert.strictEqual(typeof provider, 'string');
switch (provider) {
case 'caas': return caas;
case 'route53': return route53;
default: return null;
}
} }
function add(record, callback) { function add(subdomain, type, values, callback) {
assert.strictEqual(typeof record, 'object'); assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof record.subdomain, 'string'); assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof record.type, 'string'); assert(util.isArray(values));
assert.strictEqual(typeof record.value, 'string');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
debug('add: ', record); settings.getDnsConfig(function (error, dnsConfig) {
if (error) return callback(new SubdomainError(SubdomainError.INTERNAL_ERROR, error));
api().addSubdomain(config.zoneName(), record.subdomain, record.type, record.value, function (error, changeId) { api(dnsConfig.provider).add(dnsConfig, config.zoneName(), subdomain, type, values, function (error, changeId) {
if (error) return callback(error); if (error) return callback(error);
callback(null, changeId); callback(null, changeId);
}); });
});
} }
function addMany(records, callback) { function get(subdomain, type, callback) {
assert(util.isArray(records)); assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
debug('addMany: ', records); settings.getDnsConfig(function (error, dnsConfig) {
if (error) return callback(new SubdomainError(SubdomainError.INTERNAL_ERROR, error));
var changeIds = []; api(dnsConfig.provider).get(dnsConfig, config.zoneName(), subdomain, type, function (error, values) {
async.eachSeries(records, function (record, callback) {
add(record, function (error, changeId) {
if (error) return callback(error); if (error) return callback(error);
changeIds.push(changeId); callback(null, values);
});
});
}
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');
settings.getDnsConfig(function (error, dnsConfig) {
if (error) return callback(new SubdomainError(SubdomainError.INTERNAL_ERROR, error));
api(dnsConfig.provider).update(dnsConfig, config.zoneName(), subdomain, type, values, function (error) {
if (error) return callback(error);
callback(null); callback(null);
}); });
}, function (error) {
if (error) return callback(error);
callback(null, changeIds);
}); });
} }
function remove(record, callback) { function remove(subdomain, type, values, callback) {
assert.strictEqual(typeof record, 'object'); assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
debug('remove: ', record); settings.getDnsConfig(function (error, dnsConfig) {
if (error) return callback(new SubdomainError(SubdomainError.INTERNAL_ERROR, error));
api().delSubdomain(config.zoneName(), record.subdomain, record.type, record.value, function (error) { api(dnsConfig.provider).del(dnsConfig, config.zoneName(), subdomain, type, values, function (error) {
if (error && error.reason !== SubdomainError.NOT_FOUND) return callback(error); if (error && error.reason !== SubdomainError.NOT_FOUND) return callback(error);
debug('deleteSubdomain: successfully deleted subdomain from aws.');
callback(null); callback(null);
}); });
});
} }
function status(changeId, callback) { function status(changeId, callback) {
assert.strictEqual(typeof changeId, 'string'); assert.strictEqual(typeof changeId, 'string');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
api().getChangeStatus(changeId, function (error, status) { settings.getDnsConfig(function (error, dnsConfig) {
if (error) return callback(new SubdomainError(SubdomainError.INTERNAL_ERROR, error));
api(dnsConfig.provider).getChangeStatus(dnsConfig, changeId, function (error, status) {
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error)); if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error));
callback(null, status === 'INSYNC' ? 'done' : 'pending'); callback(null, status === 'INSYNC' ? 'done' : 'pending');
}); });
});
} }
+54 -26
View File
@@ -9,7 +9,9 @@ exports = module.exports = {
var appdb = require('./appdb.js'), var appdb = require('./appdb.js'),
assert = require('assert'), assert = require('assert'),
async = require('async'),
child_process = require('child_process'), child_process = require('child_process'),
cloudron = require('./cloudron.js'),
debug = require('debug')('box:taskmanager'), debug = require('debug')('box:taskmanager'),
locker = require('./locker.js'), locker = require('./locker.js'),
_ = require('underscore'); _ = require('underscore');
@@ -18,12 +20,38 @@ var gActiveTasks = { };
var gPendingTasks = [ ]; var gPendingTasks = [ ];
var TASK_CONCURRENCY = 5; 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) { function initialize(callback) {
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
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
cloudron.events.removeListener(cloudron.EVENT_CONFIGURED, resumeTasks);
locker.removeListener('unlocked', startNextTask);
async.eachSeries(Object.keys(gActiveTasks), stopAppTask, callback);
}
// resume app installs and uninstalls // resume app installs and uninstalls
function resumeTasks(callback) {
callback = callback || NOOP_CALLBACK;
appdb.getAll(function (error, apps) { appdb.getAll(function (error, apps) {
if (error) return callback(error); if (error) return callback(error);
@@ -36,21 +64,6 @@ function initialize(callback) {
callback(null); 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() { function startNextTask() {
@@ -80,8 +93,12 @@ function startAppTask(appId) {
} }
gActiveTasks[appId] = child_process.fork(__dirname + '/apptask.js', [ appId ]); gActiveTasks[appId] = child_process.fork(__dirname + '/apptask.js', [ appId ]);
var pid = gActiveTasks[appId].pid;
debug('Started task of %s pid: %s', appId, pid);
gActiveTasks[appId].once('exit', function (code) { gActiveTasks[appId].once('exit', function (code) {
debug('Task for %s completed with status %s', appId, code); debug('Task for %s pid %s completed with status %s', appId, pid, code);
if (code && code !== 50) { // apptask crashed if (code && code !== 50) { // apptask crashed
appdb.update(appId, { installationState: appdb.ISTATE_ERROR, installationProgress: 'Apptask crashed with code ' + code }, NOOP_CALLBACK); appdb.update(appId, { installationState: appdb.ISTATE_ERROR, installationProgress: 'Apptask crashed with code ' + code }, NOOP_CALLBACK);
} }
@@ -90,21 +107,32 @@ function startAppTask(appId) {
}); });
} }
function stopAppTask(appId) { function stopAppTask(appId, callback) {
assert.strictEqual(typeof appId, 'string'); assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
if (gActiveTasks[appId]) { if (gActiveTasks[appId]) {
debug('stopAppTask : Killing existing task of %s with pid %s: ', appId, gActiveTasks[appId].pid); debug('stopAppTask : Killing existing task of %s with pid %s', appId, gActiveTasks[appId].pid);
gActiveTasks[appId].once('exit', function () { callback(); });
gActiveTasks[appId].kill(); // this will end up calling the 'exit' handler gActiveTasks[appId].kill(); // this will end up calling the 'exit' handler
delete gActiveTasks[appId]; return;
} else if (gPendingTasks.indexOf(appId) !== -1) { }
debug('stopAppTask: Removing existing pending task : %s', appId);
if (gPendingTasks.indexOf(appId) !== -1) {
debug('stopAppTask: Removing pending task : %s', appId);
gPendingTasks = _.without(gPendingTasks, appId); gPendingTasks = _.without(gPendingTasks, appId);
} } else {
debug('stopAppTask: no task for %s to be stopped', appId);
} }
function restartAppTask(appId) { callback();
stopAppTask(appId);
startAppTask(appId);
} }
function restartAppTask(appId, callback) {
callback = callback || NOOP_CALLBACK;
async.series([
stopAppTask.bind(null, appId),
startAppTask.bind(null, appId)
], callback);
}
+9 -16
View File
@@ -9,6 +9,7 @@
var addons = require('../addons.js'), var addons = require('../addons.js'),
appdb = require('../appdb.js'), appdb = require('../appdb.js'),
apptask = require('../apptask.js'), apptask = require('../apptask.js'),
async = require('async'),
config = require('../config.js'), config = require('../config.js'),
database = require('../database.js'), database = require('../database.js'),
expect = require('expect.js'), expect = require('expect.js'),
@@ -17,6 +18,7 @@ var addons = require('../addons.js'),
net = require('net'), net = require('net'),
nock = require('nock'), nock = require('nock'),
paths = require('../paths.js'), paths = require('../paths.js'),
settings = require('../settings.js'),
_ = require('underscore'); _ = require('underscore');
var MANIFEST = { var MANIFEST = {
@@ -80,10 +82,11 @@ var APP = {
describe('apptask', function () { describe('apptask', function () {
before(function (done) { before(function (done) {
config.set('version', '0.5.0'); config.set('version', '0.5.0');
database.initialize(function (error) { async.series([
expect(error).to.be(null); database.initialize,
appdb.add(APP.id, APP.appStoreId, APP.manifest, APP.location, APP.portBindings, APP.accessRestriction, APP.oauthProxy, done); 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) { after(function (done) {
@@ -200,12 +203,8 @@ describe('apptask', function () {
it('registers subdomain', function (done) { it('registers subdomain', function (done) {
nock.cleanAll(); 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') .get('/2013-04-01/hostedzone')
.reply(200, js2xml('ListHostedZonesResponse', awsHostedZones, { arrayMap: { HostedZones: 'HostedZone'} })) .reply(200, js2xml('ListHostedZonesResponse', awsHostedZones, { arrayMap: { HostedZones: 'HostedZone'} }))
.post('/2013-04-01/hostedzone/ZONEID/rrset/') .post('/2013-04-01/hostedzone/ZONEID/rrset/')
@@ -213,7 +212,6 @@ describe('apptask', function () {
apptask._registerSubdomain(APP, function (error) { apptask._registerSubdomain(APP, function (error) {
expect(error).to.be(null); expect(error).to.be(null);
expect(scope.isDone()).to.be.ok();
expect(awsScope.isDone()).to.be.ok(); expect(awsScope.isDone()).to.be.ok();
done(); done();
}); });
@@ -221,12 +219,8 @@ describe('apptask', function () {
it('unregisters subdomain', function (done) { it('unregisters subdomain', function (done) {
nock.cleanAll(); 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') .get('/2013-04-01/hostedzone')
.reply(200, js2xml('ListHostedZonesResponse', awsHostedZones, { arrayMap: { HostedZones: 'HostedZone'} })) .reply(200, js2xml('ListHostedZonesResponse', awsHostedZones, { arrayMap: { HostedZones: 'HostedZone'} }))
.post('/2013-04-01/hostedzone/ZONEID/rrset/') .post('/2013-04-01/hostedzone/ZONEID/rrset/')
@@ -234,7 +228,6 @@ describe('apptask', function () {
apptask._unregisterSubdomain(APP, APP.location, function (error) { apptask._unregisterSubdomain(APP, APP.location, function (error) {
expect(error).to.be(null); expect(error).to.be(null);
expect(scope.isDone()).to.be.ok();
expect(awsScope.isDone()).to.be.ok(); expect(awsScope.isDone()).to.be.ok();
done(); done();
}); });
+23 -10
View File
@@ -3,6 +3,7 @@
set -eu set -eu
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
readonly TEST_IMAGE="cloudron/test:10.0.0"
source ${SOURCE_DIR}/setup/INFRA_VERSION source ${SOURCE_DIR}/setup/INFRA_VERSION
@@ -34,28 +35,40 @@ for script in "${scripts[@]}"; do
fi fi
done done
if ! docker inspect cloudron/test:8.0.0 >/dev/null 2>/dev/null; then image_missing=""
echo "docker pull cloudron/test:8.0.0 for tests to run"
exit 1 if ! docker inspect "${TEST_IMAGE}" >/dev/null 2>/dev/null; then
echo "docker pull ${TEST_IMAGE}"
image_missing="true"
fi fi
if ! docker inspect "${REDIS_IMAGE}" >/dev/null 2>/dev/null; then if ! docker inspect "${REDIS_IMAGE}" >/dev/null 2>/dev/null; then
echo "docker pull ${REDIS_IMAGE} for tests to run" echo "docker pull ${REDIS_IMAGE}"
exit 1 image_missing="true"
fi fi
if ! docker inspect "${MYSQL_IMAGE}" >/dev/null 2>/dev/null; then if ! docker inspect "${MYSQL_IMAGE}" >/dev/null 2>/dev/null; then
echo "docker pull ${MYSQL_IMAGE} for tests to run" echo "docker pull ${MYSQL_IMAGE}"
exit 1 image_missing="true"
fi fi
if ! docker inspect "${POSTGRESQL_IMAGE}" >/dev/null 2>/dev/null; then if ! docker inspect "${POSTGRESQL_IMAGE}" >/dev/null 2>/dev/null; then
echo "docker pull ${POSTGRESQL_IMAGE} for tests to run" echo "docker pull ${POSTGRESQL_IMAGE}"
exit 1 image_missing="true"
fi fi
if ! docker inspect "${MONGODB_IMAGE}" >/dev/null 2>/dev/null; then if ! docker inspect "${MONGODB_IMAGE}" >/dev/null 2>/dev/null; then
echo "docker pull ${MONGODB_IMAGE} for tests to run" echo "docker pull ${MONGODB_IMAGE}"
image_missing="true"
fi
if ! docker inspect "${MAIL_IMAGE}" >/dev/null 2>/dev/null; then
echo "docker pull ${MAIL_IMAGE}"
image_missing="true"
fi
if [[ "${image_missing}" == "true" ]]; then
echo "Pull above images before running tests"
exit 1 exit 1
fi fi
+16 -1
View File
@@ -9,17 +9,21 @@
var constants = require('../constants.js'), var constants = require('../constants.js'),
expect = require('expect.js'), expect = require('expect.js'),
fs = require('fs'), fs = require('fs'),
path = require('path'); path = require('path'),
paths = require('../paths.js'),
safe = require('safetydance');
var config = null; var config = null;
describe('config', function () { describe('config', function () {
before(function () { before(function () {
safe.fs.unlinkSync(paths.DNS_IN_SYNC_FILE);
delete require.cache[require.resolve('../config.js')]; delete require.cache[require.resolve('../config.js')];
config = require('../config.js'); config = require('../config.js');
}); });
after(function () { after(function () {
safe.fs.unlinkSync(paths.DNS_IN_SYNC_FILE);
delete require.cache[require.resolve('../config.js')]; delete require.cache[require.resolve('../config.js')];
}); });
@@ -28,6 +32,17 @@ describe('config', function () {
done(); done();
}); });
it('dnsInSync() is unset', function (done) {
expect(config.dnsInSync()).to.not.be.ok();
done();
});
it('dnsInSync() is set', function (done) {
config.setDnsInSync();
expect(config.dnsInSync()).to.be.ok();
done();
});
it('cloudron.conf generated automatically', function (done) { it('cloudron.conf generated automatically', function (done) {
expect(fs.existsSync(path.join(config.baseDir(), 'configs/cloudron.conf'))).to.be.ok(); expect(fs.existsSync(path.join(config.baseDir(), 'configs/cloudron.conf'))).to.be.ok();
done(); done();
+117
View File
@@ -23,6 +23,7 @@ function cleanup(done) {
} }
describe('Settings', function () { describe('Settings', function () {
describe('values', function () {
before(setup); before(setup);
after(cleanup); after(cleanup);
@@ -81,6 +82,40 @@ describe('Settings', function () {
}); });
}); });
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 set backup config', function (done) {
settings.setBackupConfig({ provider: 'caas', token: 'TOKEN' }, function (error) {
expect(error).to.be(null);
done();
});
});
it('can get backup config', function (done) {
settings.getBackupConfig(function (error, dnsConfig) {
expect(error).to.be(null);
expect(dnsConfig.provider).to.be('caas');
expect(dnsConfig.token).to.be('TOKEN');
done();
});
});
it('can get all values', function (done) { it('can get all values', function (done) {
settings.getAll(function (error, allSettings) { settings.getAll(function (error, allSettings) {
expect(error).to.be(null); expect(error).to.be(null);
@@ -91,3 +126,85 @@ describe('Settings', function () {
}); });
}); });
}); });
describe('validateCertificate', function () {
before(setup);
after(cleanup);
/*
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
*/
// 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-----';
// 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('allows both null', function () {
expect(settings.validateCertificate(null, null, 'foobar.com')).to.be(null);
});
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 rm -rf $HOME/.cloudron_test
mkdir -p $HOME/.cloudron_test mkdir -p $HOME/.cloudron_test
cd $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_scopes="root,profile,users,apps,settings"
webadmin_origin="https://${ADMIN_LOCATION}-localhost" webadmin_origin="https://${ADMIN_LOCATION}-localhost"
-40
View File
@@ -1,40 +0,0 @@
'use strict';
// we can possibly remove this entire file and make our tests
// smarter to just use the host interface provided by boot2docker
// https://github.com/boot2docker/boot2docker#container-port-redirection
// https://github.com/boot2docker/boot2docker/pull/93
// https://github.com/docker/docker/issues/4007
exports = module.exports = {
forwardFromHostToVirtualBox: forwardFromHostToVirtualBox,
unforwardFromHostToVirtualBox: unforwardFromHostToVirtualBox
};
var assert = require('assert'),
child_process = require('child_process'),
debug = require('debug')('box:vbox'),
os = require('os');
function forwardFromHostToVirtualBox(rulename, port) {
assert.strictEqual(typeof rulename, 'string');
assert.strictEqual(typeof port, 'number');
if (os.platform() === 'darwin') {
debug('Setting up VirtualBox port forwarding for '+ rulename + ' at ' + port);
child_process.exec(
'VBoxManage controlvm boot2docker-vm natpf1 delete ' + rulename + ';' +
'VBoxManage controlvm boot2docker-vm natpf1 ' + rulename + ',tcp,127.0.0.1,' + port + ',,' + port);
}
}
function unforwardFromHostToVirtualBox(rulename) {
assert.strictEqual(typeof rulename, 'string');
if (os.platform() === 'darwin') {
debug('Removing VirtualBox port forwarding for '+ rulename);
child_process.exec('VBoxManage controlvm boot2docker-vm natpf1 delete ' + rulename);
}
}
-2
View File
@@ -1,2 +0,0 @@
{
}
+14 -38
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html ng-app="Application" ng-controller="Controller"> <html>
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" /> <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
@@ -13,41 +13,6 @@
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" type="text/css"> <link href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" type="text/css">
<link href="//fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet" type="text/css"> <link href="//fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet" type="text/css">
<!-- jQuery-->
<script src="3rdparty/js/jquery.min.js"></script>
<!-- Bootstrap Core JavaScript -->
<script src="3rdparty/js/bootstrap.min.js"></script>
<!-- Angularjs scripts -->
<script src="3rdparty/js/angular.min.js"></script>
<script src="3rdparty/js/angular-loader.min.js"></script>
<script>
'use strict';
// create main application module
var app = angular.module('Application', []);
app.controller('Controller', ['$scope', '$http', function ($scope, $http) {
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
$scope.cloudronName = 'Cloudron';
$scope.referrer = search.referrer || null;
// try to fetch cloudron status
$http.get('/api/v1/cloudron/status').success(function(data, status) {
if (status !== 200 || typeof data !== 'object') return console.error(status, data);
$scope.cloudronName = data.cloudronName;
document.title = $scope.cloudronName + ' App Error';
}).error(function (data, status) {
console.error(status, data);
});
}]);
</script>
</head> </head>
<body class="status-page"> <body class="status-page">
@@ -55,10 +20,9 @@
<div class="wrapper"> <div class="wrapper">
<div class="content"> <div class="content">
<img src="/img/logo_inverted_192.png"/> <img src="/img/logo_inverted_192.png"/>
<h1> {{cloudronName}} </h1>
<h3> <i class="fa fa-frown-o fa-fw text-danger"></i> Something has gone wrong </h3> <h3> <i class="fa fa-frown-o fa-fw text-danger"></i> Something has gone wrong </h3>
This app is currently not running. <a href="{{ referrer }}">Please retry later</a>. This app is currently not running. <a id="appLink" href="">Please retry later</a>.
<footer> <footer>
<span class="text-muted"><a href="mailto: support@cloudron.io">Contact Support</a> - Copyright &copy; Cloudron 2014-15</span> <span class="text-muted"><a href="mailto: support@cloudron.io">Contact Support</a> - Copyright &copy; Cloudron 2014-15</span>
@@ -66,5 +30,17 @@
</div> </div>
</div> </div>
<script>
(function () {
'use strict';
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
document.getElementById('appLink').href = search.referrer;
})();
</script>
</body> </body>
</html> </html>
+1 -11
View File
@@ -31,7 +31,6 @@
var app = angular.module('Application', []); var app = angular.module('Application', []);
app.controller('Controller', ['$scope', '$http', function ($scope, $http) { app.controller('Controller', ['$scope', '$http', function ($scope, $http) {
$scope.cloudronName = 'Cloudron';
$scope.webServerOriginLink = '/'; $scope.webServerOriginLink = '/';
$scope.errorMessage = ''; $scope.errorMessage = '';
@@ -44,15 +43,6 @@
else console.error(status, data); else console.error(status, data);
}); });
// try to fetch cloudron status
$http.get('/api/v1/cloudron/status').success(function(data, status) {
if (status !== 200 || typeof data !== 'object') return console.error(status, data);
$scope.cloudronName = data.cloudronName;
document.title = $scope.cloudronName + ' Error';
}).error(function (data, status) {
console.error(status, data);
});
var search = window.location.search.slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {}); var search = window.location.search.slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
$scope.errorCode = search.errorCode || 0; $scope.errorCode = search.errorCode || 0;
@@ -68,7 +58,7 @@
<div class="wrapper"> <div class="wrapper">
<div class="content"> <div class="content">
<img src="/api/v1/cloudron/avatar" onerror="this.src = '/img/logo_inverted_192.png'"/> <img src="/api/v1/cloudron/avatar" onerror="this.src = '/img/logo_inverted_192.png'"/>
<h1> {{cloudronName}} </h1> <h1> Cloudron </h1>
<div ng-show="errorCode == 0"> <div ng-show="errorCode == 0">
<h3> <i class="fa fa-frown-o fa-fw text-danger"></i> Something has gone wrong </h3> <h3> <i class="fa fa-frown-o fa-fw text-danger"></i> Something has gone wrong </h3>
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>
<span class="icon-bar"></span> <span class="icon-bar"></span>
</button> </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 navbar-brand-icon" href="#/"><img src="/api/v1/cloudron/avatar" width="40" height="40"/></a>
<a class="navbar-brand" href="index.html">{{config.cloudronName || 'Cloudron'}}</a> <a class="navbar-brand" href="#/">Cloudron</a>
</div> </div>
<!-- /.navbar-header --> <!-- /.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> <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"> <ul class="dropdown-menu" role="menu">
<li><a href="#/account"><i class="fa fa-user fa-fw"></i> Account</a></li> <li><a href="#/account"><i class="fa fa-user fa-fw"></i> Account</a></li>
<li ng-show="user.admin && config.isDev"><a href="#/dns"><i class="fa fa-wrench fa-fw"></i> DNS Management</a></li>
<li ng-show="user.admin"><a href="#/graphs"><i class="fa fa-bar-chart fa-fw"></i> Graphs</a></li> <li ng-show="user.admin"><a href="#/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><a href="#/support"><i class="fa fa-comment fa-fw"></i> Support</a></li>
<li ng-show="user.admin && config.isCustomDomain"><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 ng-show="user.admin"><a href="#/settings"><i class="fa fa-wrench fa-fw"></i> Settings</a></li>
<li class="divider"></li> <li class="divider"></li>
<li><a href="" ng-click="logout($event)"><i class="fa fa-sign-out fa-fw"></i> Logout</a></li> <li><a href="" ng-click="logout($event)"><i class="fa fa-sign-out fa-fw"></i> Logout</a></li>
+59 -29
View File
@@ -6,6 +6,9 @@
angular.module('Application').service('Client', ['$http', 'md5', 'Notification', function ($http, md5, Notification) { angular.module('Application').service('Client', ['$http', 'md5', 'Notification', function ($http, md5, Notification) {
var client = null; var client = null;
// Keep this in sync with docs and docker.js
var DEFAULT_MEMORY_LIMIT = 1024 * 1024 * 200;
function ClientError(statusCode, messageOrObject) { function ClientError(statusCode, messageOrObject) {
Error.call(this); Error.call(this);
this.name = this.constructor.name; this.name = this.constructor.name;
@@ -58,6 +61,7 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
this._configListener = []; this._configListener = [];
this._readyListener = []; this._readyListener = [];
this._userInfo = { this._userInfo = {
id: null,
username: null, username: null,
email: null, email: null,
admin: false admin: false
@@ -76,7 +80,7 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
developerMode: false, developerMode: false,
region: null, region: null,
size: null, size: null,
cloudronName: null memory: 0
}; };
this._installedApps = []; this._installedApps = [];
this._clientId = '<%= oauth.clientId %>'; this._clientId = '<%= oauth.clientId %>';
@@ -119,6 +123,7 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
Client.prototype.setUserInfo = function (userInfo) { Client.prototype.setUserInfo = function (userInfo) {
// In order to keep the angular bindings alive, set each property individually // In order to keep the angular bindings alive, set each property individually
this._userInfo.id = userInfo.id;
this._userInfo.username = userInfo.username; this._userInfo.username = userInfo.username;
this._userInfo.email = userInfo.email; this._userInfo.email = userInfo.email;
this._userInfo.admin = !!userInfo.admin; this._userInfo.admin = !!userInfo.admin;
@@ -186,20 +191,6 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
}).error(defaultErrorHandler(callback)); }).error(defaultErrorHandler(callback));
}; };
Client.prototype.changeCloudronName = function (name, callback) {
var that = this;
var data = { name: name };
$http.post(client.apiOrigin + '/api/v1/settings/cloudron_name', data).success(function (data, status) {
if (status !== 200) return callback(new ClientError(status, data));
// will get overriden after polling for config, but ensures quick UI update
that._config.cloudronName = name;
callback(null);
}).error(defaultErrorHandler(callback));
};
Client.prototype.changeCloudronAvatar = function (avatarFile, callback) { Client.prototype.changeCloudronAvatar = function (avatarFile, callback) {
var fd = new FormData(); var fd = new FormData();
fd.append('avatar', avatarFile); fd.append('avatar', avatarFile);
@@ -215,7 +206,17 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
Client.prototype.installApp = function (id, manifest, title, config, callback) { Client.prototype.installApp = function (id, manifest, title, config, callback) {
var that = this; 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) { $http.post(client.apiOrigin + '/api/v1/apps/install', data).success(function (data, status) {
if (status !== 202 || typeof data !== 'object') return defaultErrorHandler(callback); if (status !== 202 || typeof data !== 'object') return defaultErrorHandler(callback);
@@ -249,7 +250,17 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
}; };
Client.prototype.configureApp = function (id, password, config, callback) { 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) { $http.post(client.apiOrigin + '/api/v1/apps/' + id + '/configure', data).success(function (data, status) {
if (status !== 202) return callback(new ClientError(status, data)); if (status !== 202) return callback(new ClientError(status, data));
callback(null); callback(null);
@@ -303,6 +314,20 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
}).error(defaultErrorHandler(callback)); }).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) { Client.prototype.getBackups = function (callback) {
$http.get(client.apiOrigin + '/api/v1/backups').success(function (data, status) { $http.get(client.apiOrigin + '/api/v1/backups').success(function (data, status) {
if (status !== 200 || typeof data !== 'object') return callback(new ClientError(status, data)); if (status !== 200 || typeof data !== 'object') return callback(new ClientError(status, data));
@@ -383,12 +408,11 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
}).error(defaultErrorHandler(callback)); }).error(defaultErrorHandler(callback));
}; };
Client.prototype.createAdmin = function (username, password, email, name, setupToken, callback) { Client.prototype.createAdmin = function (username, password, email, setupToken, callback) {
var payload = { var payload = {
username: username, username: username,
password: password, password: password,
email: email, email: email
name: name
}; };
var that = this; var that = this;
@@ -446,16 +470,14 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
}; };
Client.prototype.setCertificate = function (certificateFile, keyFile, callback) { 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(); Client.prototype.setAdminCertificate = function (certificateFile, keyFile, callback) {
fd.append('certificate', certificateFile); $http.post(client.apiOrigin + '/api/v1/settings/admin_certificate', { cert: certificateFile, key: keyFile }).success(function(data, status) {
fd.append('key', keyFile);
$http.post(client.apiOrigin + '/api/v1/cloudron/certificate', fd, {
headers: { 'Content-Type': undefined },
transformRequest: angular.identity
}).success(function(data, status) {
if (status !== 202) return callback(new ClientError(status, data)); if (status !== 202) return callback(new ClientError(status, data));
callback(null); callback(null);
}).error(defaultErrorHandler(callback)); }).error(defaultErrorHandler(callback));
@@ -654,6 +676,14 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
}).error(defaultErrorHandler(callback)); }).error(defaultErrorHandler(callback));
}; };
Client.prototype.enoughResourcesAvailable = function (app) {
var needed = app.manifest.memoryLimit || DEFAULT_MEMORY_LIMIT;
var used = this.getInstalledApps().reduce(function (prev, cur) { return prev + (cur.manifest.memoryLimit || DEFAULT_MEMORY_LIMIT); }, 0);
var available = (this.getConfig().memory || 0) - used;
return (available - needed) > 0;
};
client = new Client(); client = new Client();
return client; return client;
}]); }]);
+3 -3
View File
@@ -25,15 +25,15 @@ app.config(['$routeProvider', function ($routeProvider) {
}).when('/apps', { }).when('/apps', {
controller: 'AppsController', controller: 'AppsController',
templateUrl: 'views/apps.html' templateUrl: 'views/apps.html'
}).when('/dns', {
controller: 'DnsController',
templateUrl: 'views/dns.html'
}).when('/account', { }).when('/account', {
controller: 'AccountController', controller: 'AccountController',
templateUrl: 'views/account.html' templateUrl: 'views/account.html'
}).when('/graphs', { }).when('/graphs', {
controller: 'GraphsController', controller: 'GraphsController',
templateUrl: 'views/graphs.html' templateUrl: 'views/graphs.html'
}).when('/certs', {
controller: 'CertsController',
templateUrl: 'views/certs.html'
}).when('/settings', { }).when('/settings', {
controller: 'SettingsController', controller: 'SettingsController',
templateUrl: 'views/settings.html' templateUrl: 'views/settings.html'
-4
View File
@@ -112,10 +112,6 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
if (config.progress.update && config.progress.update.percent !== -1) { if (config.progress.update && config.progress.update.percent !== -1) {
window.location.href = '/update.html'; window.location.href = '/update.html';
} }
if (config.cloudronName) {
document.title = config.cloudronName;
}
}); });
// setup all the dialog focus handling // setup all the dialog focus handling
+43 -7
View File
@@ -28,8 +28,11 @@ app.config(['$routeProvider', function ($routeProvider) {
controller: 'StepController', controller: 'StepController',
templateUrl: 'views/setup/step2.html' templateUrl: 'views/setup/step2.html'
}).when('/step3', { }).when('/step3', {
controller: 'FinishController', controller: 'StepController',
templateUrl: 'views/setup/step3.html' templateUrl: 'views/setup/step3.html'
}).when('/step4', {
controller: 'FinishController',
templateUrl: 'views/setup/step4.html'
}).otherwise({ redirectTo: '/'}); }).otherwise({ redirectTo: '/'});
}]); }]);
@@ -40,7 +43,6 @@ app.service('Wizard', [ function () {
this.username = ''; this.username = '';
this.email = ''; this.email = '';
this.password = ''; this.password = '';
this.name = '';
this.availableAvatars = [{ this.availableAvatars = [{
file: null, file: null,
data: null, data: null,
@@ -96,6 +98,7 @@ app.service('Wizard', [ function () {
}]; }];
this.avatar = {}; this.avatar = {};
this.avatarBlob = null; this.avatarBlob = null;
this.dnsConfig = null;
} }
Wizard.prototype.setPreviewAvatar = function (avatar) { Wizard.prototype.setPreviewAvatar = function (avatar) {
@@ -147,8 +150,24 @@ app.service('Wizard', [ function () {
app.controller('StepController', ['$scope', '$route', '$location', 'Wizard', function ($scope, $route, $location, Wizard) { app.controller('StepController', ['$scope', '$route', '$location', 'Wizard', function ($scope, $route, $location, Wizard) {
$scope.wizard = Wizard; $scope.wizard = Wizard;
$scope.next = function (page, bad) { $scope.next = function (bad) {
if (!bad) $location.path(page); if (bad) return;
var current = $location.path();
var next = '';
if (current === '/step1') {
next = '/step2';
} else if (current === '/step2') {
if (Wizard.dnsConfig === null) next = '/step4';
else next = '/step3';
} else if (current === '/step3') {
next = '/step4';
} else {
next = '/step1';
}
$location.path(next);
}; };
$scope.focusNext = function (elemId, bad) { $scope.focusNext = function (elemId, bad) {
@@ -191,14 +210,16 @@ app.controller('StepController', ['$scope', '$route', '$location', 'Wizard', fun
image = null; image = null;
}; };
image.src = $scope.wizard.availableAvatars[randomIndex].data || $scope.wizard.availableAvatars[randomIndex].url; image.src = $scope.wizard.availableAvatars[randomIndex].data || $scope.wizard.availableAvatars[randomIndex].url;
} else if ($route.current.templateUrl === 'views/setup/step3.html' && Wizard.dnsConfig === null) {
$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; $scope.wizard = Wizard;
Client.createAdmin($scope.wizard.username, $scope.wizard.password, $scope.wizard.email, $scope.wizard.name, $scope.setupToken, function (error) { Client.createAdmin($scope.wizard.username, $scope.wizard.password, $scope.wizard.email, $scope.setupToken, function (error) {
if (error) { if (error) {
console.error('Internal error', error); console.error('Internal error', error);
window.location.href = '/error.html'; window.location.href = '/error.html';
@@ -208,7 +229,16 @@ app.controller('FinishController', ['$scope', '$location', '$timeout', 'Wizard',
Client.changeCloudronAvatar($scope.wizard.avatarBlob, function (error) { Client.changeCloudronAvatar($scope.wizard.avatarBlob, function (error) {
if (error) return console.error('Unable to set avatar.', error); if (error) return console.error('Unable to set avatar.', error);
if ($scope.wizard.dnsConfig === null) {
window.location.href = '/'; window.location.href = '/';
return;
}
Client.setDnsConfig($scope.wizard.dnsConfig, function (error) {
if (error) return console.error('Unable to set dns config.', error);
window.location.href = '/';
});
}); });
}); });
}]); }]);
@@ -225,7 +255,13 @@ app.controller('SetupController', ['$scope', '$location', 'Client', 'Wizard', fu
if (!search.email) return window.location.href = '/error.html?errorCode=3'; if (!search.email) return window.location.href = '/error.html?errorCode=3';
Wizard.email = search.email; 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
};
}
Client.isServerFirstTime(function (error, isFirstTime) { Client.isServerFirstTime(function (error, isFirstTime) {
if (error) { if (error) {
+2 -11
View File
@@ -38,18 +38,9 @@
else return 'https://my' + tmp.slice(tmp.indexOf('-')) + host.slice(tmp.length); else return 'https://my' + tmp.slice(tmp.indexOf('-')) + host.slice(tmp.length);
} }
app.controller('Controller', ['$scope', '$http', function ($scope, $http) { app.controller('Controller', ['$scope', function ($scope) {
$scope.apiOrigin = detectApiOrigin(); $scope.apiOrigin = detectApiOrigin();
$scope.cloudronAvatar = $scope.apiOrigin + '/api/v1/cloudron/avatar'; $scope.cloudronAvatar = $scope.apiOrigin + '/api/v1/cloudron/avatar';
$scope.cloudronName = 'Cloudron';
$http.get($scope.apiOrigin + '/api/v1/cloudron/status').success(function(data, status) {
if (status !== 200 || typeof data !== 'object') return console.error(status, data);
$scope.cloudronName = data.cloudronName;
document.title = $scope.cloudronName;
}).error(function (data, status) {
console.error(status, data);
});
}]); }]);
</script> </script>
@@ -60,7 +51,7 @@
<div class="wrapper"> <div class="wrapper">
<div class="content"> <div class="content">
<img ng-src="{{ cloudronAvatar || '/img/logo_inverted_192.png' }}" onerror="this.src = '/img/logo_inverted_192.png'"/> <img ng-src="{{ cloudronAvatar || '/img/logo_inverted_192.png' }}" onerror="this.src = '/img/logo_inverted_192.png'"/>
<h1> {{cloudronName}} </h1> <h1> Cloudron </h1>
<p> <p>
There is no app configured for this domain. If you want to put an app at this location,<br/> There is no app configured for this domain. If you want to put an app at this location,<br/>
please reconfigure the app in the <a ng-href="{{apiOrigin}}">settings panel</a> and leave the location empty. please reconfigure the app in the <a ng-href="{{apiOrigin}}">settings panel</a> and leave the location empty.
+32 -10
View File
@@ -36,19 +36,41 @@
</div> </div>
</ng-form> </ng-form>
</div> </div>
<div class="form-group" ng-show="appConfigure.app.manifest.singleUser">
<label class="control-label">User</label>
<p>This is a single user application. Access is granted to <b>{{appConfigure.app.accessRestriction.users[0]}}</b>.</p>
</div>
<div class="form-group"> <div class="form-group">
<label class="control-label" for="accessRestriction">Website Visibility</label> <label class="control-label" for="oauthProxy">Website Visibility</label>
<div class="checkbox"> <select class="form-control" id="oauthProxy" ng-model="appConfigure.oauthProxy">
<label>
<input type="checkbox" ng-model="appConfigure.oauthProxy"> Cloudron users only
</label>
</div>
<!-- <label class="control-label" for="accessRestriction">Website Visibility</label>
<select class="form-control" id="accessRestriction" ng-model="appConfigure.accessRestriction">
<option value="">Visible to all</option> <option value="">Visible to all</option>
<option value="roleUser">Visible only to Cloudron users</option> <option value="1">Visible only to Cloudron users</option>
</select> --> </select>
</div> </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> <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/>
<br/> <br/>
+58 -10
View File
@@ -19,8 +19,11 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
portBindings: {}, portBindings: {},
portBindingsEnabled: {}, portBindingsEnabled: {},
portBindingsInfo: {}, portBindingsInfo: {},
accessRestriction: null, oauthProxy: '',
oauthProxy: false certificateFile: null,
certificateFileName: '',
keyFile: null,
keyFileName: ''
}; };
$scope.appUninstall = { $scope.appUninstall = {
@@ -52,9 +55,13 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
$scope.appConfigure.app = {}; $scope.appConfigure.app = {};
$scope.appConfigure.location = ''; $scope.appConfigure.location = '';
$scope.appConfigure.password = ''; $scope.appConfigure.password = '';
$scope.appConfigure.portBindings = {}; $scope.appConfigure.portBindings = {}; // This is the actual model holding the env:port pair
$scope.appConfigure.accessRestriction = null; $scope.appConfigure.portBindingsEnabled = {}; // This is the actual model holding the enabled/disabled flag
$scope.appConfigure.oauthProxy = false; $scope.appConfigure.oauthProxy = '';
$scope.appConfigure.certificateFile = null;
$scope.appConfigure.certificateFileName = '';
$scope.appConfigure.keyFile = null;
$scope.appConfigure.keyFileName = '';
$scope.appConfigureForm.$setPristine(); $scope.appConfigureForm.$setPristine();
$scope.appConfigureForm.$setUntouched(); $scope.appConfigureForm.$setUntouched();
@@ -86,16 +93,42 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
$scope.appRestoreForm.$setUntouched(); $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.showConfigure = function (app) {
$scope.reset(); $scope.reset();
// fill relevant info from the app
$scope.appConfigure.app = app; $scope.appConfigure.app = app;
$scope.appConfigure.location = app.location; $scope.appConfigure.location = app.location;
$scope.appConfigure.accessRestriction = app.accessRestriction || null; $scope.appConfigure.oauthProxy = app.oauthProxy ? '1' : '';
$scope.appConfigure.oauthProxy = app.oauthProxy;
$scope.appConfigure.portBindingsInfo = app.manifest.tcpPorts || {}; // Portbinding map only for information $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 // fill the portBinding structures. There might be holes in the app.portBindings, which signalizes a disabled port
for (var env in $scope.appConfigure.portBindingsInfo) { for (var env in $scope.appConfigure.portBindingsInfo) {
@@ -125,7 +158,16 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
} }
} }
Client.configureApp($scope.appConfigure.app.id, $scope.appConfigure.password, { location: $scope.appConfigure.location || '', portBindings: finalPortBindings, accessRestriction: $scope.appConfigure.accessRestriction, oauthProxy: $scope.appConfigure.oauthProxy }, function (error) { var data = {
location: $scope.appConfigure.location || '',
portBindings: finalPortBindings,
oauthProxy: !!$scope.appConfigure.oauthProxy,
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) {
if (error) { if (error) {
if (error.statusCode === 409 && (error.message.indexOf('is reserved') !== -1 || error.message.indexOf('is already in use') !== -1)) { if (error.statusCode === 409 && (error.message.indexOf('is reserved') !== -1 || error.message.indexOf('is already in use') !== -1)) {
$scope.appConfigure.error.port = error.message; $scope.appConfigure.error.port = error.message;
@@ -137,6 +179,12 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
$scope.appConfigure.error.password = 'Wrong password provided.'; $scope.appConfigure.error.password = 'Wrong password provided.';
$scope.appConfigure.password = ''; $scope.appConfigure.password = '';
$('#appConfigurePasswordInput').focus(); $('#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 { } else {
$scope.appConfigure.error.other = error.message; $scope.appConfigure.error.other = error.message;
} }
+34 -4
View File
@@ -24,6 +24,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="has-error text-center" ng-show="appInstall.error.port">{{ appInstall.error.port }}</div> <div class="has-error text-center" ng-show="appInstall.error.port">{{ appInstall.error.port }}</div>
<div ng-repeat="(env, info) in appInstall.portBindingsInfo"> <div ng-repeat="(env, info) in appInstall.portBindingsInfo">
<ng-form name="portInfo_form"> <ng-form name="portInfo_form">
@@ -33,12 +34,37 @@
</div> </div>
</ng-form> </ng-form>
</div> </div>
<div class="form-group" ng-show="appInstall.app.manifest.singleUser"> <div class="form-group" ng-show="appInstall.app.manifest.singleUser">
<label class="control-label" for="accessRestriction">User</label> <label class="control-label" for="accessRestriction">User</label>
<select class="form-control" id="accessRestriction" ng-model="appInstall.accessRestriction" ng-required="appInstall.app.manifest.singleUser"> <p>This is a single user application.</p>
<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> </select>
</div> </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"/> <input class="ng-hide" type="submit" ng-disabled="appInstallForm.$invalid || busy"/>
</form> </form>
</div> </div>
@@ -51,11 +77,15 @@
<div ng-bind-html="appInstall.app.manifest.description | markdown2html"></div> <div ng-bind-html="appInstall.app.manifest.description | markdown2html"></div>
</div> </div>
</div> </div>
<div class="collapse" id="collapseResourceConstraint" data-toggle="false">
<h4 class="text-danger">Not enough resources left to install this application.</h4>
<p>The Cloudron's resources can be extended with a model upgrade or available resources may be freed up by uninstalling unused applications.</p>
</div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button> <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-success" ng-show="!appInstall.installFormVisible && user.admin" ng-click="showInstallForm()">Install</button> <button type="button" class="btn btn-success" ng-show="!appInstall.installFormVisible && user.admin && !appInstall.resourceConstraintVisible" ng-click="showInstallForm()">Install</button>
<button type="button" class="btn btn-success" ng-show="appInstall.installFormVisible && user.admin" ng-click="doInstall()" ng-disabled="appInstallForm.$invalid || appInstall.busy"><i class="fa fa-spinner fa-pulse" ng-show="appInstall.busy"></i> Install</button> <button type="button" class="btn btn-success" ng-show="appInstall.installFormVisible && user.admin && !appInstall.resourceConstraintVisible" ng-click="doInstall()" ng-disabled="appInstallForm.$invalid || appInstall.busy"><i class="fa fa-spinner fa-pulse" ng-show="appInstall.busy"></i> Install</button>
</div> </div>
</div> </div>
</div> </div>
+64 -6
View File
@@ -19,7 +19,11 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
portBindings: {}, portBindings: {},
accessRestriction: null, accessRestriction: null,
oauthProxy: false, oauthProxy: false,
mediaLinks: [] mediaLinks: [],
certificateFile: null,
certificateFileName: '',
keyFile: null,
keyFileName: ''
}; };
$scope.appNotFound = { $scope.appNotFound = {
@@ -140,8 +144,15 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
$scope.appInstall.accessRestriction = null; $scope.appInstall.accessRestriction = null;
$scope.appInstall.oauthProxy = false; $scope.appInstall.oauthProxy = false;
$scope.appInstall.installFormVisible = false; $scope.appInstall.installFormVisible = false;
$scope.appInstall.resourceConstraintVisible = false;
$scope.appInstall.mediaLinks = []; $scope.appInstall.mediaLinks = [];
$scope.appInstall.certificateFile = null;
$scope.appInstall.certificateFileName = '';
$scope.appInstall.keyFile = null;
$scope.appInstall.keyFileName = '';
$('#collapseInstallForm').collapse('hide'); $('#collapseInstallForm').collapse('hide');
$('#collapseResourceConstraint').collapse('hide');
$('#collapseMediaLinksCarousel').collapse('show'); $('#collapseMediaLinksCarousel').collapse('show');
$scope.appInstallForm.$setPristine(); $scope.appInstallForm.$setPristine();
@@ -149,10 +160,44 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
}; };
$scope.showInstallForm = function () { $scope.showInstallForm = function () {
if (Client.enoughResourcesAvailable($scope.appInstall.app)) {
$scope.appInstall.installFormVisible = true; $scope.appInstall.installFormVisible = true;
$('#collapseMediaLinksCarousel').collapse('hide'); $('#collapseMediaLinksCarousel').collapse('hide');
$('#collapseInstallForm').collapse('show'); $('#collapseInstallForm').collapse('show');
$('#appInstallLocationInput').focus(); $('#appInstallLocationInput').focus();
} else {
$scope.appInstall.resourceConstraintVisible = true;
$('#collapseMediaLinksCarousel').collapse('hide');
$('#collapseResourceConstraint').collapse('show');
}
};
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.showInstall = function (app) {
@@ -162,14 +207,12 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
angular.copy(app, $scope.appInstall.app); angular.copy(app, $scope.appInstall.app);
$('#appInstallModal').modal('show'); $('#appInstallModal').modal('show');
console.log(app)
$scope.appInstall.mediaLinks = $scope.appInstall.app.manifest.mediaLinks || []; $scope.appInstall.mediaLinks = $scope.appInstall.app.manifest.mediaLinks || [];
$scope.appInstall.location = app.location; $scope.appInstall.location = app.location;
$scope.appInstall.portBindingsInfo = $scope.appInstall.app.manifest.tcpPorts || {}; // Portbinding map only for information $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.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.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; $scope.appInstall.oauthProxy = false;
// set default ports // set default ports
@@ -202,10 +245,19 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
// translate to accessRestriction object // translate to accessRestriction object
var accessRestriction = $scope.appInstall.app.manifest.singleUser ? { var accessRestriction = $scope.appInstall.app.manifest.singleUser ? {
users: [ $scope.appInstall.accessRestriction ] users: [ $scope.appInstall.accessRestriction.id ]
} : null; } : 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) {
if (error.statusCode === 409 && (error.message.indexOf('is reserved') !== -1 || error.message.indexOf('is already in use') !== -1)) { if (error.statusCode === 409 && (error.message.indexOf('is reserved') !== -1 || error.message.indexOf('is already in use') !== -1)) {
$scope.appInstall.error.port = error.message; $scope.appInstall.error.port = error.message;
@@ -215,6 +267,12 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
$('#appInstallLocationInput').focus(); $('#appInstallLocationInput').focus();
} else if (error.statusCode === 402) { } 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>'; $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 { } else {
$scope.appInstall.error.other = error.message; $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;">
<div class="text-left">
<h3>DNS Credentials</h3>
</div>
</div>
<div class="card" style="margin-bottom: 15px;">
<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;">
<div class="text-left">
<h3>SSL Certificates</h3>
</div>
</div>
<div class="card" style="margin-bottom: 15px;">
<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 || !Client.getConfig().isCustomDomain) $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 -62
View File
@@ -32,36 +32,6 @@
</div> </div>
</div> </div>
<!-- Modal change name -->
<div class="modal fade" id="nameChangeModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Change the Cloudron Name</h4>
</div>
<div class="modal-body">
<form name="nameChangeForm" class="form-signin" role="form" novalidate ng-submit="doChangeName()" autocomplete="off">
<fieldset>
<div class="form-group" ng-class="{ 'has-error': (nameChangeForm.name.$dirty && nameChangeForm.name.$invalid) }">
<label class="control-label" for="inputNameChangeName">New Cloudron Name</label>
<div class="control-label" ng-show="(!nameChangeForm.name.$dirty && nameChange.error.name) || (nameChangeForm.name.$dirty && nameChangeForm.name.$invalid)">
<small ng-show="nameChangeForm.name.$error.required">A valid name is required</small>
<small ng-show="(nameChangeForm.name.$dirty && nameChangeForm.name.$invalid) && !nameChangeForm.name.$error.required">The name is not valid</small>
</div>
<input type="text" class="form-control" ng-model="nameChange.name" id="inputNameChangeName" name="name" ng-maxlength="512" ng-minlength="1" required autofocus>
</div>
<input class="ng-hide" type="submit" ng-disabled="nameChangeForm.$invalid"/>
</fieldset>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-success" ng-click="doChangeName()" ng-disabled="nameChangeForm.$invalid || nameChange.busy"><i class="fa fa-spinner fa-pulse" ng-show="nameChange.busy"></i> Change</button>
</div>
</div>
</div>
</div>
<!-- Modal change avatar --> <!-- Modal change avatar -->
<div class="modal fade" id="avatarChangeModal" tabindex="-1" role="dialog"> <div class="modal fade" id="avatarChangeModal" tabindex="-1" role="dialog">
<div class="modal-dialog"> <div class="modal-dialog">
@@ -115,6 +85,34 @@
</div> </div>
</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 style="max-width: 600px; margin: 0 auto;" ng-show="user.admin">
<div class="text-left"> <div class="text-left">
<h3>Backups</h3> <h3>Backups</h3>
@@ -145,38 +143,6 @@
</div> </div>
</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;">Name</td>
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ config.cloudronName }} <a href="" ng-click="showChangeName()"><i class="fa fa-pencil text-small"></i></a></td>
</tr>
<tr>
<td class="text-muted" style="vertical-align: top;">Model</td>
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ config.size }} - {{ config.region }}</td>
</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 style="max-width: 600px; margin: 0 auto;" ng-show="user.admin">
<div class="text-left"> <div class="text-left">
<h3>Developer Mode</h3> <h3>Developer Mode</h3>
+3 -37
View File
@@ -5,6 +5,7 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
$scope.user = Client.getUserInfo(); $scope.user = Client.getUserInfo();
$scope.config = Client.getConfig(); $scope.config = Client.getConfig();
$scope.dnsConfig = {};
$scope.lastBackup = null; $scope.lastBackup = null;
$scope.backups = []; $scope.backups = [];
@@ -24,12 +25,6 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
percent: 100 percent: 100
}; };
$scope.nameChange = {
busy: false,
error: {},
name: ''
};
$scope.avatarChange = { $scope.avatarChange = {
busy: false, busy: false,
error: {}, error: {},
@@ -97,14 +92,6 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
$('#avatarFileInput').click(); $('#avatarFileInput').click();
}; };
function nameChangeReset() {
$scope.nameChange.error.name = null;
$scope.nameChange.name = '';
$scope.nameChangeForm.$setPristine();
$scope.nameChangeForm.$setUntouched();
}
function avatarChangeReset() { function avatarChangeReset() {
$scope.avatarChange.error.avatar = null; $scope.avatarChange.error.avatar = null;
$scope.avatarChange.avatar = null; $scope.avatarChange.avatar = null;
@@ -156,22 +143,6 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
}); });
}; };
$scope.doChangeName = function () {
$scope.nameChange.error.name = null;
$scope.nameChange.busy = true;
Client.changeCloudronName($scope.nameChange.name, function (error) {
if (error) {
console.error('Unable to change name.', error);
} else {
nameChangeReset();
$('#nameChangeModal').modal('hide');
}
$scope.nameChange.busy = false;
});
};
function getBlobFromImg(img, callback) { function getBlobFromImg(img, callback) {
var size = 256; var size = 256;
@@ -263,11 +234,6 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
$('#developerModeChangeModal').modal('show'); $('#developerModeChangeModal').modal('show');
}; };
$scope.showChangeName = function () {
nameChangeReset();
$('#nameChangeModal').modal('show');
};
$scope.showCreateBackup = function () { $scope.showCreateBackup = function () {
$('#createBackupModal').modal('show'); $('#createBackupModal').modal('show');
}; };
@@ -297,11 +263,11 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
Client.onReady(function () { Client.onReady(function () {
fetchBackups(); 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 // setup all the dialog focus handling
['developerModeChangeModal', 'nameChangeModal'].forEach(function (id) { ['developerModeChangeModal'].forEach(function (id) {
$('#' + id).on('shown.bs.modal', function () { $('#' + id).on('shown.bs.modal', function () {
$(this).find("[autofocus]:first").focus(); $(this).find("[autofocus]:first").focus();
}); });
+2 -12
View File
@@ -3,7 +3,7 @@
<h1>Welcome to your Cloudron!</h1> <h1>Welcome to your Cloudron!</h1>
<hr/> <hr/>
<h3 class=""> <h3 class="">
Choose a name and avatar for your Cloudron Choose an avatar
</h3> </h3>
</div> </div>
</div> </div>
@@ -19,16 +19,6 @@
<br/> <br/>
<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.name.$dirty && setup_form.name.$invalid }">
<!-- <label class="control-label" for="inputName">Name</label> -->
<input type="text" class="form-control" ng-model="wizard.name" id="inputName" name="name" placeholder="Name" ng-enter="next('/step2', setup_form.name.$invalid)" ng-maxlength="512" ng-minlength="1" autofocus required autocomplete="off">
</div>
</div>
</div>
<div class="row"> <div class="row">
<div class="col-md-12 settings-avatar-selector"> <div class="col-md-12 settings-avatar-selector">
<input type="file" id="avatarFileInput" style="display: none" accept="image/png"/> <input type="file" id="avatarFileInput" style="display: none" accept="image/png"/>
@@ -48,6 +38,6 @@
<div class="row"> <div class="row">
<div class="col-md-12 text-center"> <div class="col-md-12 text-center">
<a class="btn btn-primary" href="#/step2" ng-disabled="setup_form.name.$invalid">Next</a> <button class="btn btn-primary" ng-click="next()">Next</button>
</div> </div>
</div> </div>
+3 -3
View File
@@ -1,6 +1,6 @@
<div class="row"> <div class="row">
<div class="col-md-12 text-center"> <div class="col-md-12 text-center">
<h1>Create an Administrator for <b>{{ wizard.name }}</b></h1> <h1>Create an Administrator for your Cloudron</h1>
<h4 class=""> <h4 class="">
This admin account is separate from your <a href="https://cloudron.io">cloudron.io</a> account. This admin account is separate from your <a href="https://cloudron.io">cloudron.io</a> account.
</h4> </h4>
@@ -16,12 +16,12 @@
</div> </div>
<div class="form-group" ng-class="{ 'has-error': setup_form.password.$dirty && setup_form.password.$invalid }"> <div class="form-group" ng-class="{ 'has-error': setup_form.password.$dirty && setup_form.password.$invalid }">
<!-- <label class="control-label" for="inputPassword">Password</label> --> <!-- <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>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-12 text-center"> <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>
</div> </div>
+25 -6
View File
@@ -1,8 +1,27 @@
<center> <div class="row">
<h1>All done!</h1> <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/>
<br/> <br/>
<i class="fa fa-spinner fa-pulse fa-5x"></i> <div class="row">
<br/> <div class="col-md-4 col-md-offset-4 text-center">
<br/> <div class="form-group" ng-class="{ 'has-error': setup_form.accessKeyId.$dirty && setup_form.accessKeyId.$invalid }">
</center> <!-- <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>